/*
* Autopsy Forensic Browser
*
* Copyright 2015 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed 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.sleuthkit.autopsy.experimental.coordinationservice;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.curator.RetryPolicy;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.framework.recipes.locks.InterProcessReadWriteLock;
import org.apache.zookeeper.KeeperException;
import org.sleuthkit.autopsy.core.UserPreferences;
import java.io.IOException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.KeeperException.NoNodeException;
/**
* A centralized service for maintaining configuration information and providing
* distributed synchronization using a shared hierarchical namespace of nodes.
*/
public final class CoordinationService {
/**
* Category nodes are the immediate children of the root node of a shared
* hierarchical namespace managed by the coordination service.
*/
public enum CategoryNode { // RJCTODO: Move this to CoordinationServiceNamespace
CASES("cases"),
MANIFESTS("manifests"),
CONFIG("config");
private final String displayName;
private CategoryNode(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}
/**
* Exception type thrown by the coordination service.
*/
public final static class CoordinationServiceException extends Exception {
private static final long serialVersionUID = 1L;
private CoordinationServiceException(String message) {
super(message);
}
private CoordinationServiceException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* An opaque encapsulation of a lock for use in distributed synchronization.
* Instances are obtained by calling a get lock method and must be passed to
* a release lock method.
*/
public static class Lock implements AutoCloseable {
/**
* This implementation uses the Curator read/write lock. see
* http://curator.apache.org/curator-recipes/shared-reentrant-read-write-lock.html
*/
private final InterProcessMutex interProcessLock;
private final String nodePath;
private Lock(String nodePath, InterProcessMutex lock) {
this.nodePath = nodePath;
this.interProcessLock = lock;
}
public String getNodePath() {
return nodePath;
}
public void release() throws CoordinationServiceException {
try {
this.interProcessLock.release();
} catch (Exception ex) {
throw new CoordinationServiceException(String.format("Failed to release the lock on %s", nodePath), ex);
}
}
@Override
public void close() throws CoordinationServiceException {
release();
}
}
private static CuratorFramework curator = null;
private static final Map<String, CoordinationService> rootNodesToServices = new HashMap<>();
private final Map<String, String> categoryNodeToPath = new HashMap<>();
private static final int SESSION_TIMEOUT_MILLISECONDS = 300000;
private static final int CONNECTION_TIMEOUT_MILLISECONDS = 300000;
private static final int ZOOKEEPER_SESSION_TIMEOUT_MILLIS = 3000;
private static final int ZOOKEEPER_CONNECTION_TIMEOUT_MILLIS = 15000;
private static final int PORT_OFFSET = 1000;
/**
* Gets an instance of the centralized coordination service for a specific
* namespace.
*
* @param rootNode The name of the root node that defines the namespace.
*
* @return The service for the namespace defined by the root node name.
*
* @throws CoordinationServiceException If an instaNce of the coordination
* service cannot be created.
*/
public static synchronized CoordinationService getInstance(String rootNode) throws CoordinationServiceException {
if (null == curator) {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
// When run in Solr, ZooKeeper defaults to Solr port + 1000
int zooKeeperServerPort = Integer.valueOf(UserPreferences.getIndexingServerPort()) + PORT_OFFSET;
String connectString = UserPreferences.getIndexingServerHost() + ":" + zooKeeperServerPort;
curator = CuratorFrameworkFactory.newClient(connectString, SESSION_TIMEOUT_MILLISECONDS, CONNECTION_TIMEOUT_MILLISECONDS, retryPolicy);
curator.start();
}
/*
* Get or create a coordination service for the namespace defined by the
* specified root node.
*/
if (rootNodesToServices.containsKey(rootNode)) {
return rootNodesToServices.get(rootNode);
} else {
CoordinationService service;
try {
service = new CoordinationService(rootNode);
} catch (Exception ex) {
curator = null;
throw new CoordinationServiceException("Failed to create coordination service", ex);
}
rootNodesToServices.put(rootNode, service);
return service;
}
}
/**
* Constructs an instance of the centralized coordination service for a
* specific namespace.
*
* @param rootNodeName The name of the root node that defines the namespace.
*/
private CoordinationService(String rootNodeName) throws Exception {
if (false == isZooKeeperAccessible()) {
throw new Exception("Unable to access ZooKeeper");
}
String rootNode = rootNodeName;
if (!rootNode.startsWith("/")) {
rootNode = "/" + rootNode;
}
for (CategoryNode node : CategoryNode.values()) {
String nodePath = rootNode + "/" + node.getDisplayName();
try {
curator.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE).forPath(nodePath);
} catch (KeeperException ex) {
if (ex.code() != KeeperException.Code.NODEEXISTS) {
throw ex;
}
}
categoryNodeToPath.put(node.getDisplayName(), nodePath);
}
}
/**
* Tries to get an exclusive lock on a node path appended to a category path
* in the namespace managed by this coordination service. Blocks until the
* lock is obtained or the time out expires.
*
* @param category The desired category in the namespace.
* @param nodePath The node path to use as the basis for the lock.
* @param timeOut Length of the time out.
* @param timeUnit Time unit for the time out.
*
* @return The lock, or null if lock acquisition timed out.
*
* @throws CoordinationServiceException If there is an error during lock
* acquisition.
* @throws InterruptedException If interrupted while blocked during
* lock acquisition.
*/
public Lock tryGetExclusiveLock(CategoryNode category, String nodePath, int timeOut, TimeUnit timeUnit) throws CoordinationServiceException, InterruptedException {
String fullNodePath = getFullyQualifiedNodePath(category, nodePath);
try {
InterProcessReadWriteLock lock = new InterProcessReadWriteLock(curator, fullNodePath);
if (lock.writeLock().acquire(timeOut, timeUnit)) {
return new Lock(nodePath, lock.writeLock());
} else {
return null;
}
} catch (Exception ex) {
if (ex instanceof InterruptedException) {
throw (InterruptedException) ex;
} else {
throw new CoordinationServiceException(String.format("Failed to get exclusive lock for %s", fullNodePath), ex);
}
}
}
/**
* Tries to get an exclusive lock on a node path appended to a category path
* in the namespace managed by this coordination service. Returns
* immediately if the lock can not be acquired.
*
* @param category The desired category in the namespace.
* @param nodePath The node path to use as the basis for the lock.
*
* @return The lock, or null if the lock could not be obtained.
*
* @throws CoordinationServiceException If there is an error during lock
* acquisition.
*/
public Lock tryGetExclusiveLock(CategoryNode category, String nodePath) throws CoordinationServiceException {
String fullNodePath = getFullyQualifiedNodePath(category, nodePath);
try {
InterProcessReadWriteLock lock = new InterProcessReadWriteLock(curator, fullNodePath);
if (!lock.writeLock().acquire(0, TimeUnit.SECONDS)) {
return null;
}
return new Lock(nodePath, lock.writeLock());
} catch (Exception ex) {
throw new CoordinationServiceException(String.format("Failed to get exclusive lock for %s", fullNodePath), ex);
}
}
/**
* Tries to get a shared lock on a node path appended to a category path in
* the namespace managed by this coordination service. Blocks until the lock
* is obtained or the time out expires.
*
* @param category The desired category in the namespace.
* @param nodePath The node path to use as the basis for the lock.
* @param timeOut Length of the time out.
* @param timeUnit Time unit for the time out.
*
* @return The lock, or null if lock acquisition timed out.
*
* @throws CoordinationServiceException If there is an error during lock
* acquisition.
* @throws InterruptedException If interrupted while blocked during
* lock acquisition.
*/
public Lock tryGetSharedLock(CategoryNode category, String nodePath, int timeOut, TimeUnit timeUnit) throws CoordinationServiceException, InterruptedException {
String fullNodePath = getFullyQualifiedNodePath(category, nodePath);
try {
InterProcessReadWriteLock lock = new InterProcessReadWriteLock(curator, fullNodePath);
if (lock.readLock().acquire(timeOut, timeUnit)) {
return new Lock(nodePath, lock.readLock());
} else {
return null;
}
} catch (Exception ex) {
if (ex instanceof InterruptedException) {
throw (InterruptedException) ex;
} else {
throw new CoordinationServiceException(String.format("Failed to get shared lock for %s", fullNodePath), ex);
}
}
}
/**
* Tries to get a shared lock on a node path appended to a category path in
* the namespace managed by this coordination service. Returns immediately
* if the lock can not be acquired.
*
* @param category The desired category in the namespace.
* @param nodePath The node path to use as the basis for the lock.
*
* @return The lock, or null if the lock could not be obtained.
*
* @throws CoordinationServiceException If there is an error during lock
* acquisition.
*/
public Lock tryGetSharedLock(CategoryNode category, String nodePath) throws CoordinationServiceException {
String fullNodePath = getFullyQualifiedNodePath(category, nodePath);
try {
InterProcessReadWriteLock lock = new InterProcessReadWriteLock(curator, fullNodePath);
if (!lock.readLock().acquire(0, TimeUnit.SECONDS)) {
return null;
}
return new Lock(nodePath, lock.readLock());
} catch (Exception ex) {
throw new CoordinationServiceException(String.format("Failed to get shared lock for %s", fullNodePath), ex);
}
}
/**
* Retrieve the data associated with the specified node.
*
* @param category The desired category in the namespace.
* @param nodePath The node to retrieve the data for.
*
* @return The data associated with the node, if any, or null if the node
* has not been created yet.
*
* @throws CoordinationServiceException If there is an error setting the
* node data.
* @throws InterruptedException If interrupted while blocked during
* setting of node data.
*/
public byte[] getNodeData(CategoryNode category, String nodePath) throws CoordinationServiceException, InterruptedException {
String fullNodePath = getFullyQualifiedNodePath(category, nodePath);
try {
return curator.getData().forPath(fullNodePath);
} catch (NoNodeException ex) {
return null;
} catch (Exception ex) {
if (ex instanceof InterruptedException) {
throw (InterruptedException) ex;
} else {
throw new CoordinationServiceException(String.format("Failed to get data for %s", fullNodePath), ex);
}
}
}
/**
* Store the given data with the specified node.
*
* @param category The desired category in the namespace.
* @param nodePath The node to associate the data with.
* @param data The data to store with the node.
*
* @throws CoordinationServiceException If there is an error setting the
* node data.
* @throws InterruptedException If interrupted while blocked during
* setting of node data.
*/
public void setNodeData(CategoryNode category, String nodePath, byte[] data) throws CoordinationServiceException, InterruptedException {
String fullNodePath = getFullyQualifiedNodePath(category, nodePath);
try {
curator.setData().forPath(fullNodePath, data);
} catch (Exception ex) {
if (ex instanceof InterruptedException) {
throw (InterruptedException) ex;
} else {
throw new CoordinationServiceException(String.format("Failed to set data for %s", fullNodePath), ex);
}
}
}
/**
* Creates a node path within a given category.
*
* @param category A category node.
* @param nodePath A node path relative to a category node path.
*
* @return
*/
private String getFullyQualifiedNodePath(CategoryNode category, String nodePath) {
return categoryNodeToPath.get(category.getDisplayName()) + "/" + nodePath.toUpperCase();
}
/**
* Determines if ZooKeeper is accessible with the current settings. Closes
* the connection prior to returning.
*
* @return true if a connection was achieved, false otherwise
*/
private static boolean isZooKeeperAccessible() {
boolean result = false;
Object workerThreadWaitNotifyLock = new Object();
int zooKeeperServerPort = Integer.valueOf(UserPreferences.getIndexingServerPort()) + PORT_OFFSET;
String connectString = UserPreferences.getIndexingServerHost() + ":" + zooKeeperServerPort;
try {
ZooKeeper zooKeeper = new ZooKeeper(connectString, ZOOKEEPER_SESSION_TIMEOUT_MILLIS,
(WatchedEvent event) -> {
synchronized (workerThreadWaitNotifyLock) {
workerThreadWaitNotifyLock.notify();
}
});
synchronized (workerThreadWaitNotifyLock) {
workerThreadWaitNotifyLock.wait(ZOOKEEPER_CONNECTION_TIMEOUT_MILLIS);
}
ZooKeeper.States state = zooKeeper.getState();
if (state == ZooKeeper.States.CONNECTED || state == ZooKeeper.States.CONNECTEDREADONLY) {
result = true;
}
zooKeeper.close();
} catch (InterruptedException | IOException ignored) {
}
return result;
}
}