package org.cloudname.backends.zookeeper;
import com.google.common.base.Charsets;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Monitor a set of child nodes for changes. Needs to do this with the ZooKeeper API since
* Curator doesn't provide the necessary interface and the PathChildrenCache is best effort
* (and not even a very good effort)
*
* <p>Watches are kept as usual and the mzxid for each node is kept. If that changes between
* watches it mens we've missed an event and the appropriate event is generated to the
* listener.
*
* <p>Note that this class only watches for changes one level down. Changes in children aren't
* monitored. The path must exist beforehand.
*
* @author stalehd@gmail.com
*/
public class NodeCollectionWatcher {
private static final Logger LOG = Logger.getLogger(NodeCollectionWatcher.class.getName());
private final Map<String, Long> childMzxid = new HashMap<>();
private final Object syncObject = new Object();
private final ZooKeeper zk;
private final String pathToWatch;
private final AtomicBoolean shuttingDown = new AtomicBoolean(false);
private final NodeWatcherListener listener;
/**
* Create and start the collection watcher. The supplied @link{ZooKeeper} instance is used to
* read nodes from the path <pre>pathToWatch</pre>. Changes are communicated with the
* supplied @link{NodeWatcherListener}.
*/
public NodeCollectionWatcher(
final ZooKeeper zk, final String pathToWatch, final NodeWatcherListener listener) {
this.pathToWatch = pathToWatch;
this.zk = zk;
this.listener = listener;
readChildNodes();
}
/**
* Shut down watchers. The listener won't get notified of changes after it has been shut down.
*/
public void shutdown() {
shuttingDown.set(true);
}
/**
* Watcher for node collections. Set by getChildren().
*/
private final Watcher nodeCollectionWatcher = (watchedEvent) -> {
switch (watchedEvent.getType()) {
case NodeChildrenChanged:
// Child values have changed, read children, generate events
readChildNodes();
break;
case None:
// Some zookeeper event. Watches might not apply anymore. Reapply.
switch (watchedEvent.getState()) {
case ConnectedReadOnly:
LOG.severe("Connected to readonly cluster");
// Connected to a cluster without quorum. Nodes might not be
// correct but re-read the nodes.
readChildNodes();
break;
case SyncConnected:
LOG.info("Connected to cluster");
// (re-)Connected to the cluster. Nodes must be re-read. Discard
// those that aren't found, keep unchanged ones.
readChildNodes();
break;
case Disconnected:
// Disconnected from the cluster. The nodes might not be
// up to date (but a reconnect might solve the issue)
LOG.log(Level.WARNING, "Disconnected from zk cluster");
break;
case Expired:
// Session has expired. Nodes are no longer available
removeAllChildNodes();
break;
default:
break;
}
break;
default:
break;
}
};
/**
* A watcher for the child nodes (set via getData()).
*/
private final Watcher changeWatcher = (watchedEvent) -> {
if (shuttingDown.get()) {
return;
}
switch (watchedEvent.getType()) {
case NodeDeleted:
removeChildNode(watchedEvent.getPath());
break;
case NodeDataChanged:
processNode(watchedEvent.getPath());
break;
default:
break;
}
};
/**
* Remove all nodes.
*/
private void removeAllChildNodes() {
System.out.println("Remove all child nodes");
final Set<String> nodesToRemove = new HashSet<>();
synchronized (syncObject) {
nodesToRemove.addAll(childMzxid.keySet());
}
for (final String node : nodesToRemove) {
removeChildNode(node);
}
}
/**
* Read nodes from ZooKeeper, generating events as necessary. If a node is missing from the
* result it will generate a remove notification, ditto with new nodes and changes in nodes.
*/
private void readChildNodes() {
try {
final List<String> childNodes = zk.getChildren(pathToWatch, nodeCollectionWatcher);
final Set<String> childrenToDelete = new HashSet<>();
synchronized (syncObject) {
childrenToDelete.addAll(childMzxid.keySet());
}
for (final String nodeName : childNodes) {
processNode(pathToWatch + "/" + nodeName);
childrenToDelete.remove(pathToWatch + "/" + nodeName);
}
for (final String nodePath : childrenToDelete) {
removeChildNode(nodePath);
}
} catch (final KeeperException.ConnectionLossException e) {
// We've been disconnected. Let the watcher deal with it
if (!shuttingDown.get()) {
LOG.info("Lost connection to ZooKeeper while reading child nodes.");
}
} catch (final KeeperException.NoNodeException e) {
// Node has been removed. Ignore the error?
removeChildNode(e.getPath());
} catch (final KeeperException | InterruptedException e) {
LOG.log(Level.WARNING, "Got exception reading child nodes", e);
}
}
/**
* Add a node, generate create or data change notification if needed.
*/
private void processNode(final String nodePath) {
if (shuttingDown.get()) {
return;
}
try {
final Stat stat = new Stat();
final byte[] nodeData = zk.getData(nodePath, changeWatcher, stat);
final String data = new String(nodeData, Charsets.UTF_8);
synchronized (syncObject) {
if (!childMzxid.containsKey(nodePath)) {
childMzxid.put(nodePath, stat.getMzxid());
generateCreateEvent(nodePath, data);
return;
}
final Long zxid = childMzxid.get(nodePath);
if (zxid != stat.getMzxid()) {
// the data have changed. Generate event
childMzxid.put(nodePath, stat.getMzxid());
generateDataChangeEvent(nodePath, data);
}
}
} catch (final KeeperException.ConnectionLossException e) {
// We've been disconnected. Let the watcher deal with it
if (!shuttingDown.get()) {
LOG.info("Lost connection to ZooKeeper while reading child nodes.");
}
} catch (final KeeperException.NoNodeException e) {
removeChildNode(e.getPath());
// Node has been removed before we got to do anything. Ignore error?
} catch (final KeeperException | InterruptedException e) {
LOG.log(Level.WARNING, "Got exception adding child node with path " + nodePath, e);
} catch (Exception ex) {
LOG.log(Level.SEVERE, "Pooop!", ex);
}
}
/**
* Remove node. Generate remove event if needed.
*/
private void removeChildNode(final String nodePath) {
synchronized (syncObject) {
if (childMzxid.containsKey(nodePath)) {
childMzxid.remove(nodePath);
generateRemoveEvent(nodePath);
}
}
}
/**
* Invoke nodeCreated on listener.
*/
private void generateCreateEvent(final String nodePath, final String data) {
try {
listener.nodeCreated(nodePath, data);
} catch (final Exception exception) {
LOG.log(Level.WARNING, "Got exception calling listener.nodeCreated", exception);
}
}
/**
* Invoke dataChanged on listener.
*/
private void generateDataChangeEvent(final String nodePath, final String data) {
try {
listener.dataChanged(nodePath, data);
} catch (final Exception exception) {
LOG.log(Level.WARNING, "Got exception calling listener.dataChanged", exception);
}
}
/**
* Invoke nodeRemoved on listener.
*/
private void generateRemoveEvent(final String nodePath) {
try {
listener.nodeRemoved(nodePath);
} catch (final Exception exception) {
LOG.log(Level.WARNING, "Got exception calling listener.nodeRemoved", exception);
}
}
}