All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.dubbo.common.config.configcenter.file.FileSystemDynamicConfiguration Maven / Gradle / Ivy

There is a newer version: 3.3.0-beta.3
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.dubbo.common.config.configcenter.file;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.config.configcenter.ConfigChangeType;
import org.apache.dubbo.common.config.configcenter.ConfigChangedEvent;
import org.apache.dubbo.common.config.configcenter.ConfigurationListener;
import org.apache.dubbo.common.config.configcenter.DynamicConfiguration;
import org.apache.dubbo.common.config.configcenter.TreePathDynamicConfiguration;
import org.apache.dubbo.common.function.ThrowableConsumer;
import org.apache.dubbo.common.function.ThrowableFunction;
import org.apache.dubbo.common.lang.ShutdownHookCallbacks;
import org.apache.dubbo.common.logger.ErrorTypeAwareLogger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.utils.NamedThreadFactory;
import org.apache.dubbo.common.utils.StringUtils;
import org.apache.dubbo.rpc.model.ScopeModel;
import org.apache.dubbo.rpc.model.ScopeModelUtil;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.lang.String.format;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.util.Collections.emptySet;
import static java.util.Collections.unmodifiableMap;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.commons.io.FileUtils.readFileToString;
import static org.apache.dubbo.common.constants.LoggerCodeConstants.CONFIG_ERROR_PROCESS_LISTENER;

/**
 * File-System based {@link DynamicConfiguration} implementation
 *
 * @since 2.7.5
 */
public class FileSystemDynamicConfiguration extends TreePathDynamicConfiguration {

    public static final String CONFIG_CENTER_DIR_PARAM_NAME = PARAM_NAME_PREFIX + "dir";

    public static final String CONFIG_CENTER_ENCODING_PARAM_NAME = PARAM_NAME_PREFIX + "encoding";

    public static final String DEFAULT_CONFIG_CENTER_DIR_PATH = System.getProperty("user.home") + File.separator
        + ".dubbo" + File.separator + "config-center";

    public static final int DEFAULT_THREAD_POOL_SIZE = 1;

    public static final String DEFAULT_CONFIG_CENTER_ENCODING = "UTF-8";

    private static final WatchEvent.Kind[] INTEREST_PATH_KINDS = of(ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);

    /**
     * The class name of {@linkplain sun.nio.fs.PollingWatchService}
     */
    private static final String POLLING_WATCH_SERVICE_CLASS_NAME = "sun.nio.fs.PollingWatchService";

    private static final int THREAD_POOL_SIZE = 1;

    /**
     * Logger
     */
    private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(FileSystemDynamicConfiguration.class);


    /**
     * The unmodifiable map for {@link ConfigChangeType} whose key is the {@link WatchEvent.Kind#name() name} of
     * {@link WatchEvent.Kind WatchEvent's Kind}
     */
    private static final Map CONFIG_CHANGE_TYPES_MAP =
        unmodifiableMap(new HashMap() {
            // Initializes the elements that is mapping ConfigChangeType
            {
                put(ENTRY_CREATE.name(), ConfigChangeType.ADDED);
                put(ENTRY_DELETE.name(), ConfigChangeType.DELETED);
                put(ENTRY_MODIFY.name(), ConfigChangeType.MODIFIED);
            }
        });

    private static final Optional watchService;

    /**
     * Is Pooling Based Watch Service
     *
     * @see #detectPoolingBasedWatchService(Optional)
     */
    private static final boolean BASED_POOLING_WATCH_SERVICE;

    private static final WatchEvent.Modifier[] MODIFIERS;

    /**
     * the delay to action in seconds. If null, execute indirectly
     */
    private static final Integer DELAY;

    /**
     * The thread pool for {@link WatchEvent WatchEvents} loop
     * It's optional if there is not any {@link ConfigurationListener} registration
     *
     * @see ThreadPoolExecutor
     */
    private static final ThreadPoolExecutor WATCH_EVENTS_LOOP_THREAD_POOL;

    // static initialization
    static {
        watchService = newWatchService();
        BASED_POOLING_WATCH_SERVICE = detectPoolingBasedWatchService(watchService);
        MODIFIERS = initWatchEventModifiers();
        DELAY = initDelay(MODIFIERS);
        WATCH_EVENTS_LOOP_THREAD_POOL = newWatchEventsLoopThreadPool();
    }

    /**
     * The Root Directory for config center
     */
    private final File rootDirectory;

    private final String encoding;

    /**
     * The {@link Set} of {@link #groupDirectory(String) directories} that may be processing,
     * 

* if {@link #isBasedPoolingWatchService()} is false, this properties will be * {@link Collections#emptySet() empty} * * @see #initProcessingDirectories() */ private final Set processingDirectories; private final Map> listenersRepository; private ScopeModel scopeModel; private AtomicBoolean hasRegisteredShutdownHook = new AtomicBoolean(); public FileSystemDynamicConfiguration() { this(new File(DEFAULT_CONFIG_CENTER_DIR_PATH)); } public FileSystemDynamicConfiguration(File rootDirectory) { this(rootDirectory, DEFAULT_CONFIG_CENTER_ENCODING); } public FileSystemDynamicConfiguration(File rootDirectory, String encoding) { this(rootDirectory, encoding, DEFAULT_THREAD_POOL_PREFIX); } public FileSystemDynamicConfiguration(File rootDirectory, String encoding, String threadPoolPrefixName) { this(rootDirectory, encoding, threadPoolPrefixName, DEFAULT_THREAD_POOL_SIZE); } public FileSystemDynamicConfiguration(File rootDirectory, String encoding, String threadPoolPrefixName, int threadPoolSize) { this(rootDirectory, encoding, threadPoolPrefixName, threadPoolSize, DEFAULT_THREAD_POOL_KEEP_ALIVE_TIME); } public FileSystemDynamicConfiguration(File rootDirectory, String encoding, String threadPoolPrefixName, int threadPoolSize, long keepAliveTime) { super(rootDirectory.getAbsolutePath(), threadPoolPrefixName, threadPoolSize, keepAliveTime, DEFAULT_GROUP, -1L); this.rootDirectory = rootDirectory; this.encoding = encoding; this.processingDirectories = initProcessingDirectories(); this.listenersRepository = new HashMap<>(); registerDubboShutdownHook(); } public FileSystemDynamicConfiguration(File rootDirectory, String encoding, String threadPoolPrefixName, int threadPoolSize, long keepAliveTime, ScopeModel scopeModel) { super(rootDirectory.getAbsolutePath(), threadPoolPrefixName, threadPoolSize, keepAliveTime, DEFAULT_GROUP, -1L); this.rootDirectory = rootDirectory; this.encoding = encoding; this.processingDirectories = initProcessingDirectories(); this.listenersRepository = new HashMap<>(); this.scopeModel = scopeModel; registerDubboShutdownHook(); } public FileSystemDynamicConfiguration(URL url) { this(initDirectory(url), getEncoding(url), getThreadPoolPrefixName(url), getThreadPoolSize(url), getThreadPoolKeepAliveTime(url), url.getScopeModel()); } private Set initProcessingDirectories() { return isBasedPoolingWatchService() ? new LinkedHashSet<>() : emptySet(); } public File configFile(String key, String group) { return new File(buildPathKey(group, key)); } private void doInListener(String configFilePath, BiConsumer> consumer) { watchService.ifPresent(watchService -> { File configFile = new File(configFilePath); executeMutually(configFile.getParentFile(), () -> { // process the WatchEvents if not start if (!isProcessingWatchEvents()) { processWatchEvents(watchService); } List listeners = getListeners(configFile); consumer.accept(configFile, listeners); // Nothing to return return null; }); }); } /** * Register the Dubbo ShutdownHook * * @since 2.7.8 */ private void registerDubboShutdownHook() { if (!hasRegisteredShutdownHook.compareAndSet(false, true)) { return; } ShutdownHookCallbacks shutdownHookCallbacks = ScopeModelUtil.getApplicationModel(scopeModel).getBeanFactory().getBean(ShutdownHookCallbacks.class); shutdownHookCallbacks.addCallback(() -> { watchService.ifPresent(w -> { try { w.close(); } catch (IOException e) { throw new RuntimeException(e); } }); getWatchEventsLoopThreadPool().shutdown(); }); } private static boolean isProcessingWatchEvents() { return getWatchEventsLoopThreadPool().getActiveCount() > 0; } /** * Process the {@link WatchEvent WatchEvents} loop in async execution * * @param watchService {@link WatchService} */ private void processWatchEvents(WatchService watchService) { getWatchEventsLoopThreadPool().execute(() -> { // WatchEvents Loop while (true) { WatchKey watchKey = null; try { watchKey = watchService.take(); if (!watchKey.isValid()) { continue; } for (WatchEvent event : watchKey.pollEvents()) { WatchEvent.Kind kind = event.kind(); // configChangeType's key to match WatchEvent's Kind ConfigChangeType configChangeType = CONFIG_CHANGE_TYPES_MAP.get(kind.name()); if (configChangeType == null) { continue; } Path configDirectoryPath = (Path) watchKey.watchable(); Path currentPath = (Path) event.context(); Path configFilePath = configDirectoryPath.resolve(currentPath); File configDirectory = configDirectoryPath.toFile(); executeMutually(configDirectory, () -> { fireConfigChangeEvent(configDirectory, configFilePath.toFile(), configChangeType); signalConfigDirectory(configDirectory); return null; }); } } catch (Exception e) { return; } finally { if (watchKey != null) { // reset watchKey.reset(); } } } }); } private void signalConfigDirectory(File configDirectory) { if (isBasedPoolingWatchService()) { // remove configDirectory from processing set because it's done removeProcessingDirectory(configDirectory); // notify configDirectory notifyProcessingDirectory(configDirectory); if (logger.isDebugEnabled()) { logger.debug(format("The config rootDirectory[%s] is signalled...", configDirectory.getName())); } } } private void removeProcessingDirectory(File configDirectory) { processingDirectories.remove(configDirectory); } private void notifyProcessingDirectory(File configDirectory) { configDirectory.notifyAll(); } private List getListeners(File configFile) { return listenersRepository.computeIfAbsent(configFile, p -> new LinkedList<>()); } private void fireConfigChangeEvent(File configDirectory, File configFile, ConfigChangeType configChangeType) { String key = configFile.getName(); String value = getConfig(configFile); // fire ConfigChangeEvent one by one getListeners(configFile).forEach(listener -> { try { listener.process(new ConfigChangedEvent(key, configDirectory.getName(), value, configChangeType)); } catch (Throwable e) { if (logger.isErrorEnabled()) { logger.error(CONFIG_ERROR_PROCESS_LISTENER, "", "", e.getMessage(), e); } } }); } private boolean canRead(File file) { return file.exists() && file.canRead(); } @Override public Object getInternalProperty(String key) { return null; } @Override protected boolean doPublishConfig(String pathKey, String content) throws Exception { return delay(pathKey, configFile -> { FileUtils.write(configFile, content, getEncoding()); return true; }); } @Override protected String doGetConfig(String pathKey) throws Exception { File configFile = new File(pathKey); return getConfig(configFile); } @Override protected boolean doRemoveConfig(String pathKey) throws Exception { delay(pathKey, configFile -> { String content = getConfig(configFile); FileUtils.deleteQuietly(configFile); return content; }); return true; } @Override protected Collection doGetConfigKeys(String groupPath) { File[] files = new File(groupPath).listFiles(File::isFile); if (files == null) { return new TreeSet<>(); } else { return Stream.of(files) .map(File::getName) .collect(Collectors.toList()); } } @Override protected void doAddListener(String pathKey, ConfigurationListener listener, String key, String group) { doInListener(pathKey, (configFilePath, listeners) -> { if (listeners.isEmpty()) { // If no element, it indicates watchService was registered before ThrowableConsumer.execute(configFilePath, configFile -> { FileUtils.forceMkdirParent(configFile); // A rootDirectory to be watched File configDirectory = configFile.getParentFile(); if (configDirectory != null) { // Register the configDirectory configDirectory.toPath().register(watchService.get(), INTEREST_PATH_KINDS, MODIFIERS); } }); } // Add into cache listeners.add(listener); }); } @Override protected void doRemoveListener(String pathKey, ConfigurationListener listener) { doInListener(pathKey, (file, listeners) -> { // Remove into cache listeners.remove(listener); }); } /** * Delay action for {@link #configFile(String, String) config file} * * @param configFilePath the key to represent a configuration * @param function the customized {@link Function function} with {@link File} * @param the computed value * @return */ protected V delay(String configFilePath, ThrowableFunction function) { File configFile = new File(configFilePath); // Must be based on PoolingWatchService and has listeners under config file if (isBasedPoolingWatchService()) { File configDirectory = configFile.getParentFile(); executeMutually(configDirectory, () -> { if (hasListeners(configFile) && isProcessing(configDirectory)) { Integer delay = getDelay(); if (delay != null) { // wait for delay in seconds long timeout = SECONDS.toMillis(delay); if (logger.isDebugEnabled()) { logger.debug(format("The config[path : %s] is about to delay in %d ms.", configFilePath, timeout)); } configDirectory.wait(timeout); } } addProcessing(configDirectory); return null; }); } V value = null; try { value = function.apply(configFile); } catch (Throwable e) { if (logger.isErrorEnabled()) { logger.error(CONFIG_ERROR_PROCESS_LISTENER, "", "", e.getMessage(), e); } } return value; } private boolean hasListeners(File configFile) { return getListeners(configFile).size() > 0; } /** * Is processing on {@link #buildGroupPath(String) config rootDirectory} * * @param configDirectory {@link #buildGroupPath(String) config rootDirectory} * @return if processing , return true, or false */ private boolean isProcessing(File configDirectory) { return processingDirectories.contains(configDirectory); } private void addProcessing(File configDirectory) { processingDirectories.add(configDirectory); } public Set getConfigGroups() { return Stream.of(getRootDirectory().listFiles()) .filter(File::isDirectory) .map(File::getName) .collect(Collectors.toSet()); } protected String getConfig(File configFile) { return ThrowableFunction.execute(configFile, file -> canRead(configFile) ? readFileToString(configFile, getEncoding()) : null); } @Override protected void doClose() throws Exception { } public File getRootDirectory() { return rootDirectory; } public String getEncoding() { return encoding; } protected Integer getDelay() { return DELAY; } /** * It's whether the implementation of {@link WatchService} is based on {@linkplain sun.nio.fs.PollingWatchService} * or not. *

* * @return if based, return true, or false * @see #detectPoolingBasedWatchService(Optional) */ protected static boolean isBasedPoolingWatchService() { return BASED_POOLING_WATCH_SERVICE; } protected static ThreadPoolExecutor getWatchEventsLoopThreadPool() { return WATCH_EVENTS_LOOP_THREAD_POOL; } protected ThreadPoolExecutor getWorkersThreadPool() { return super.getWorkersThreadPool(); } private V executeMutually(final Object mutex, Callable callable) { V value = null; synchronized (mutex) { try { value = callable.call(); } catch (Exception e) { if (logger.isErrorEnabled()) { logger.error(CONFIG_ERROR_PROCESS_LISTENER, "", "", e.getMessage(), e); } } } return value; } private static T[] of(T... values) { return values; } private static Integer initDelay(WatchEvent.Modifier[] modifiers) { if (isBasedPoolingWatchService()) { return 2; } else { return null; } } private static WatchEvent.Modifier[] initWatchEventModifiers() { return of(); } /** * Detect the argument of {@link WatchService} is based on {@linkplain sun.nio.fs.PollingWatchService} * or not. *

* Some platforms do not provide the native implementation of {@link WatchService}, just use * {@linkplain sun.nio.fs.PollingWatchService} in periodic poll file modifications. * * @param watchService the instance of {@link WatchService} * @return if based, return true, or false */ private static boolean detectPoolingBasedWatchService(Optional watchService) { String className = watchService.map(Object::getClass).map(Class::getName).orElse(null); return POLLING_WATCH_SERVICE_CLASS_NAME.equals(className); } private static Optional newWatchService() { Optional watchService = null; FileSystem fileSystem = FileSystems.getDefault(); try { watchService = Optional.of(fileSystem.newWatchService()); } catch (IOException e) { if (logger.isErrorEnabled()) { logger.error(CONFIG_ERROR_PROCESS_LISTENER, "", "", e.getMessage(), e); } watchService = Optional.empty(); } return watchService; } protected static File initDirectory(URL url) { String directoryPath = getParameter(url, CONFIG_CENTER_DIR_PARAM_NAME, url == null ? null : url.getPath()); File rootDirectory = null; if (!StringUtils.isBlank(directoryPath)) { rootDirectory = new File("/" + directoryPath); } if (directoryPath == null || !rootDirectory.exists()) { // If the directory does not exist rootDirectory = new File(DEFAULT_CONFIG_CENTER_DIR_PATH); } if (!rootDirectory.exists() && !rootDirectory.mkdirs()) { throw new IllegalStateException(format("Dubbo config center rootDirectory[%s] can't be created!", rootDirectory.getAbsolutePath())); } return rootDirectory; } protected static String getEncoding(URL url) { return getParameter(url, CONFIG_CENTER_ENCODING_PARAM_NAME, DEFAULT_CONFIG_CENTER_ENCODING); } private static ThreadPoolExecutor newWatchEventsLoopThreadPool() { return new ThreadPoolExecutor(THREAD_POOL_SIZE, THREAD_POOL_SIZE, 0L, MILLISECONDS, new SynchronousQueue(), new NamedThreadFactory("dubbo-config-center-watch-events-loop", true)); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy