package com.kostbot.zoodirector.zookeepersync; import com.netflix.curator.framework.CuratorFramework; import com.netflix.curator.framework.api.CuratorWatcher; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.common.PathUtils; import org.apache.zookeeper.data.Stat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Class used to synchronize all node created, deleted, child changed and updated events for a Zookeeper cluster. It * reduces all of these Zookeeper events into 3 simple cases node creation, node deletion and node update for all * nodes in a Zookeeper cluster. */ public class ZookeeperSync { private static final Logger logger = LoggerFactory.getLogger(ZookeeperSync.class); public static interface Listener { public void process(ZookeeperSync.Event e); } public static class Event { public final Type type; public final String path; private Event(Type type, String path) { this.type = type; this.path = path; } private static Event Add(String path) { return new Event(Type.add, path); } private static Event Update(String path) { return new Event(Type.update, path); } private static Event Delete(String path) { return new Event(Type.delete, path); } @Override public String toString() { return type + " " + path; } public static enum Type { add, update, delete } } /** * Get the parent path for the given path. Assumes path is a valid path format. * * @param path * @return parent path, null if path is root "/" */ public static String getParent(String path) { if ("/".equals(path)) { return null; } int lastSegmentIndex = path.lastIndexOf("/"); if (lastSegmentIndex == 0) { return "/"; } return path.substring(0, lastSegmentIndex); } public static boolean isValidPath(String path, boolean allowSubPaths) { if (allowSubPaths && !path.startsWith("/") && !path.equals("")) { path = "/" + path; } try { PathUtils.validatePath(path); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidSubPath(String path) { return isValidPath(path, true); } public static boolean isValidPath(String path) { return isValidPath(path, false); } /** * Simple watcher used to map Zookeeper events to only 3 node event types: creates, updates, and deletes. */ private class NodeWatcher implements CuratorWatcher { ZookeeperSync zookeeperSync; NodeWatcher(ZookeeperSync zookeeperSync) { this.zookeeperSync = zookeeperSync; } @Override public void process(WatchedEvent event) throws Exception { String path = event.getPath(); switch (event.getType()) { case NodeDeleted: zookeeperSync.handleNodeDeletedEvent(path); break; case NodeCreated: zookeeperSync.handleNodeCreatedEvent(path); break; case NodeDataChanged: // Note: updates are missed if they occur immediately after node creation because of the latency // required for setting up the data watcher. zookeeperSync.handleNodeDataChangedEvent(path); break; case NodeChildrenChanged: zookeeperSync.handleNodeChildrenChangedEvent(path); break; } } } private final List<Listener> listeners; // Need to synchronize access private final Set<String> nodes; // Need to synchronize access private final NodeWatcher watcher; private final CuratorFramework client; public ZookeeperSync(CuratorFramework client) { this.client = client; watcher = new NodeWatcher(this); nodes = new HashSet<String>(100); listeners = new ArrayList<Listener>(); } /** * Add listener for sync events. * * @param listener */ public void addListener(Listener listener) { synchronized (listeners) { if (!listeners.contains(listener)) { listeners.add(listener); } } } /** * Get the set of all current nodes. * * @return a read-only copy of the node set */ public Set<String> getNodes() { synchronized (nodes) { return Collections.unmodifiableSet(nodes); } } /** * Handle NodeCreated event for the given path. * * @param path * @throws Exception */ private void handleNodeCreatedEvent(String path) throws Exception { try { client.checkExists().usingWatcher(watcher).forPath(path); synchronized (nodes) { if (nodes.add(path)) { notify(Event.Add(path)); } } } catch (KeeperException.NoNodeException e) { logger.error("{} deleted before its time", path); } handleNodeChildrenChangedEvent(path); } /** * Handle NodeChildrenChanged event for the given path. * * @param path * @throws Exception */ private void handleNodeChildrenChangedEvent(String path) throws Exception { try { for (String child : client.getChildren().usingWatcher(watcher).forPath(path)) { handleNodeCreatedEvent((path.equals("/") ? "/" : path + "/") + child); } } catch (KeeperException.NoNodeException e) { // node may have been deleted } catch (KeeperException.NoAuthException e) { logger.error("Ignoring no auth: " + e); // No stack trace. } } /** * Handle NodeDataChanged event for the given path. * * @param path */ private void handleNodeDataChangedEvent(String path) throws Exception { try { client.checkExists().usingWatcher(watcher).forPath(path); } catch (KeeperException.NoNodeException e) { // node may have been deleted } notify(Event.Update(path)); } /** * Handle NodeDeleted event for the given path. * * @param path */ private void handleNodeDeletedEvent(String path) { synchronized (nodes) { if (nodes.remove(path)) { notify(Event.Delete(path)); } } } /** * Generic notification interface. * * @param event */ private void notify(Event event) { synchronized (listeners) { logger.debug("notify [{}] {}", event.type, event.path); for (Listener listener : listeners) { listener.process(event); } } } /** * Create the given zookeeper path including all non-existent parent nodes. * * @param path * @param createMode * @return true if path was created, false otherwise * @throws Exception */ public boolean create(String path, CreateMode createMode) throws Exception { if (client.checkExists().forPath(path) == null) { client.create().creatingParentsIfNeeded().withMode(createMode).forPath(path); return true; } return false; } /** * Create the given zookeeper path including all non-existent parent nodes. * * @param path * @return true if path was created, false otherwise * @throws Exception */ public boolean create(String path) throws Exception { return create(path, CreateMode.PERSISTENT); } /** * Delete the given node, its descendants, and any node ancestors with only a single child. * * @param path * @throws Exception */ public String prune(String path) throws Exception { if ("/".equals(path)) { throw new IllegalArgumentException("cannot prune root node"); } if (client.checkExists().forPath(path) == null) { return null; } String parent; // Determine oldest lonely ancestor. while (!"/".equals(parent = ZookeeperSync.getParent(path)) && client.getChildren().forPath(parent).size() == 1) { path = parent; } delete(path); return parent; } /** * Delete the given node and all its descendants. * * @param path * @throws Exception */ public void delete(String path) throws Exception { if ("/".equals(path)) { throw new IllegalArgumentException("cannot delete root node"); } // Delete children trim(path); if (client.checkExists() != null) { client.delete().forPath(path); } } /** * Delete all of the node's children and their ancestors. * * @param path * @throws Exception */ public void trim(String path) throws Exception { for (String child : client.getChildren().forPath(path)) { try { delete(("/".equals(path) ? "/" : path + "/") + child); } catch (KeeperException.BadArgumentsException e) { logger.error(e.getMessage()); } } } /** * Get the Stat object of the given path. * * @param path * @return * @throws Exception */ public Stat getStat(String path) throws Exception { return client.checkExists().forPath(path); } /** * Get data for the given path. * * @param path * @return * @throws Exception */ public byte[] getData(String path) throws Exception { return client.getData().forPath(path); } /** * Set data for the given path. * * @param path * @param data * @throws Exception */ public void setData(String path, int version, byte[] data) throws Exception { client.setData().withVersion(version).forPath(path, data); } /** * Begin watching the zookeeper cluster. Starts by loading the cluster from its root. This will trigger add events * for each node found while initializing the cluster sync. * * @throws Exception */ public void watch() throws Exception { synchronized (nodes) { nodes.clear(); } handleNodeCreatedEvent("/"); } }