/**
* diqube: Distributed Query Base.
*
* Copyright (C) 2015 Bastian Gloeckle
*
* This file is part of diqube.
*
* diqube is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diqube.server;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import org.diqube.config.Config;
import org.diqube.config.ConfigKey;
import org.diqube.config.DerivedConfigKey;
import org.diqube.context.AutoInstatiate;
import org.diqube.context.Profiles;
import org.diqube.listeners.ConsensusListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
/**
* Watches a directory for new data that should be loaded into the currently running diqube server (checks for .control
* files).
*
* <p>
* This bean will only be auto instantiated, if {@link Profiles#NEW_DATA_WATCHER} is enabled.
*
* <p>
* The implementation is somewhat tightly connected to {@link ControlFileManager}.
*
* @author Bastian Gloeckle
*/
@AutoInstatiate
@Profile(Profiles.NEW_DATA_WATCHER)
public class NewDataWatcher implements ConsensusListener {
private static final Logger logger = LoggerFactory.getLogger(NewDataWatcher.class);
@Config(DerivedConfigKey.FINAL_DATA_DIR)
private String directory;
@Inject
private ControlFileManager controlFileManager;
private NewDataWatchThread thread;
private Path watchPath;
@Override
public void consensusInitialized() {
// Start initializing as soon as we're ready to communicate with the cluster.
watchPath = Paths.get(directory).toAbsolutePath();
File f = watchPath.toFile();
if (!f.exists() || !f.isDirectory()) {
logger.error("{} is no valid directory.", watchPath);
throw new RuntimeException(watchPath + " is no valid directory.");
}
// delete all initial ready/failure files.
List<File> readyFiles = Arrays.asList(watchPath.toFile()
.listFiles((dir, fileName) -> fileName.toLowerCase().endsWith(ControlFileManager.READY_FILE_EXTENSION)
|| fileName.toLowerCase().endsWith(ControlFileManager.FAILURE_FILE_EXTENSION)));
for (File statusFile : readyFiles)
statusFile.delete();
thread = new NewDataWatchThread(() -> {
try {
WatchService watchService = watchPath.getFileSystem().newWatchService();
watchPath.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
logger.info("Will watch {} for new data.", watchPath);
return watchService;
} catch (Exception e) {
logger.warn("Could not install WatchService to watch dataDir {}. Will retry later.", watchPath, e);
return null;
}
});
thread.start();
}
@PreDestroy
public void destruct() {
thread.interrupt();
}
private void deployControlFile(File controlFile) {
logger.info("Found new control file {}.", controlFile.getAbsolutePath());
controlFileManager.deployControlFile(controlFile);
}
private void undeployControlFile(File controlFile) {
logger.info("Control file was deleted: {}", controlFile.getAbsolutePath());
controlFileManager.undeployControlFile(controlFile);
}
/**
* Thread that keeps polling the {@link WatchService} for updates.
*/
private class NewDataWatchThread extends Thread {
private Supplier<WatchService> watchServiceSupplier;
public NewDataWatchThread(Supplier<WatchService> watchServiceRegistrationFn) {
super(NewDataWatcher.class.getSimpleName());
this.watchServiceSupplier = watchServiceRegistrationFn;
this.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
logger.error("Uncaught exception in NewDataWatchThread. Will stop watching the directory. "
+ "Fix the issue and restart the server.", e);
}
});
}
@Override
public void run() {
WatchService watchService = null;
Object sync = new Object();
while (true) {
if (watchService == null) {
watchService = watchServiceSupplier.get();
File[] controlFiles = watchPath.toFile()
.listFiles((dir, fileName) -> fileName.toLowerCase().endsWith(ControlFileManager.CONTROL_FILE_EXTENSION));
// controlFiles is null if watchPath does not exist.
if (controlFiles != null) {
for (File controlFile : controlFiles)
deployControlFile(controlFile);
}
if (watchService == null) {
// still no watchService.
synchronized (sync) {
try {
sync.wait(500);
} catch (InterruptedException e) {
// interrupted, die quietly.
return;
}
}
continue;
}
}
WatchKey watchKey;
try {
watchKey = watchService.poll(500, TimeUnit.SECONDS);
} catch (InterruptedException e) {
logger.error("Interrupted while watching directory {} for changes. Monitoring stops.", watchPath);
return;
}
if (watchKey != null) {
for (WatchEvent<?> event : watchKey.pollEvents()) {
Path createdPath = ((Path) watchKey.watchable()).resolve((Path) event.context());
File createdFile = createdPath.toFile();
if (event.kind().equals(StandardWatchEventKinds.ENTRY_CREATE)) {
if (createdFile.isFile()
&& createdFile.getName().toLowerCase().endsWith(ControlFileManager.CONTROL_FILE_EXTENSION))
deployControlFile(createdFile);
} else if (event.kind().equals(StandardWatchEventKinds.ENTRY_DELETE)) {
if (createdFile.getName().toLowerCase().endsWith(ControlFileManager.CONTROL_FILE_EXTENSION))
undeployControlFile(createdFile);
}
}
}
if ((watchKey != null && !watchKey.reset()) || !watchPath.toFile().exists()) {
logger.warn("The WatchKey on the {} ({}) was unregistered/the directory was deleted.", ConfigKey.DATA_DIR,
watchPath);
try {
watchService.close();
} catch (IOException e) {
// swallow.
}
watchService = null;
}
}
}
}
}