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

org.apache.zookeeper.common.FileChangeWatcher Maven / Gradle / Ivy

There is a newer version: 3.9.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.zookeeper.common;

import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.function.Consumer;
import org.apache.zookeeper.server.ZooKeeperThread;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Instances of this class can be used to watch a directory for file changes. When a file is added to, deleted from,
 * or is modified in the given directory, the callback provided by the user will be called from a background thread.
 * Some things to keep in mind:
 * 
    *
  • The callback should be thread-safe.
  • *
  • Changes that happen around the time the thread is started may be missed.
  • *
  • There is a delay between a file changing and the callback firing.
  • *
  • The watch is not recursive - changes to subdirectories will not trigger a callback.
  • *
*/ public final class FileChangeWatcher { private static final Logger LOG = LoggerFactory.getLogger(FileChangeWatcher.class); public enum State { NEW, // object created but start() not called yet STARTING, // start() called but background thread has not entered main loop RUNNING, // background thread is running STOPPING, // stop() called but background thread has not exited main loop STOPPED // stop() called and background thread has exited, or background thread crashed } private final WatcherThread watcherThread; private State state; // protected by synchronized(this) /** * Creates a watcher that watches dirPath and invokes callback on changes. * * @param dirPath the directory to watch. * @param callback the callback to invoke with events. event.kind() will return the type of event, * and event.context() will return the filename relative to dirPath. * @throws IOException if there is an error creating the WatchService. */ public FileChangeWatcher(Path dirPath, Consumer> callback) throws IOException { FileSystem fs = dirPath.getFileSystem(); WatchService watchService = fs.newWatchService(); LOG.debug("Registering with watch service: {}", dirPath); dirPath.register(watchService, new WatchEvent.Kind[]{StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.OVERFLOW}); state = State.NEW; this.watcherThread = new WatcherThread(watchService, callback); this.watcherThread.setDaemon(true); } /** * Returns the current {@link FileChangeWatcher.State}. * @return the current state. */ public synchronized State getState() { return state; } /** * Blocks until the current state becomes desiredState. * Currently only used by tests, thus package-private. * @param desiredState the desired state. * @throws InterruptedException if the current thread gets interrupted. */ synchronized void waitForState(State desiredState) throws InterruptedException { while (this.state != desiredState) { this.wait(); } } /** * Sets the state to newState. * @param newState the new state. */ private synchronized void setState(State newState) { state = newState; this.notifyAll(); } /** * Atomically sets the state to update if and only if the * state is currently expected. * @param expected the expected state. * @param update the new state. * @return true if the update succeeds, or false if the current state * does not equal expected. */ private synchronized boolean compareAndSetState(State expected, State update) { if (state == expected) { setState(update); return true; } else { return false; } } /** * Atomically sets the state to update if and only if the * state is currently one of expectedStates. * @param expectedStates the expected states. * @param update the new state. * @return true if the update succeeds, or false if the current state * does not equal any of the expectedStates. */ private synchronized boolean compareAndSetState(State[] expectedStates, State update) { for (State expected : expectedStates) { if (state == expected) { setState(update); return true; } } return false; } /** * Tells the background thread to start. Does not wait for it to be running. * Calling this method more than once has no effect. */ public void start() { if (!compareAndSetState(State.NEW, State.STARTING)) { // If previous state was not NEW, start() has already been called. return; } this.watcherThread.start(); } /** * Tells the background thread to stop. Does not wait for it to exit. */ public void stop() { if (compareAndSetState(new State[]{State.RUNNING, State.STARTING}, State.STOPPING)) { watcherThread.interrupt(); } } /** * Inner class that implements the watcher thread logic. */ private class WatcherThread extends ZooKeeperThread { private static final String THREAD_NAME = "FileChangeWatcher"; final WatchService watchService; final Consumer> callback; WatcherThread(WatchService watchService, Consumer> callback) { super(THREAD_NAME); this.watchService = watchService; this.callback = callback; } @Override public void run() { try { LOG.info("{} thread started", getName()); if (!compareAndSetState(FileChangeWatcher.State.STARTING, FileChangeWatcher.State.RUNNING)) { // stop() called shortly after start(), before // this thread started running. FileChangeWatcher.State state = FileChangeWatcher.this.getState(); if (state != FileChangeWatcher.State.STOPPING) { throw new IllegalStateException("Unexpected state: " + state); } return; } runLoop(); } catch (Exception e) { LOG.warn("Error in runLoop()", e); throw e; } finally { try { watchService.close(); } catch (IOException e) { LOG.warn("Error closing watch service", e); } LOG.info("{} thread finished", getName()); FileChangeWatcher.this.setState(FileChangeWatcher.State.STOPPED); } } private void runLoop() { while (FileChangeWatcher.this.getState() == FileChangeWatcher.State.RUNNING) { WatchKey key; try { key = watchService.take(); } catch (InterruptedException | ClosedWatchServiceException e) { LOG.debug("{} was interrupted and is shutting down...", getName()); break; } for (WatchEvent event : key.pollEvents()) { LOG.debug("Got file changed event: {} with context: {}", event.kind(), event.context()); try { callback.accept(event); } catch (Throwable e) { LOG.error("Error from callback", e); } } boolean isKeyValid = key.reset(); if (!isKeyValid) { // This is likely a problem, it means that file reloading is broken, probably because the // directory we are watching was deleted or otherwise became inaccessible (unmounted, permissions // changed, ???). // For now, we log an error and exit the watcher thread. LOG.error("Watch key no longer valid, maybe the directory is inaccessible?"); break; } } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy