/** * Copyright 2011 LiveRamp * * 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 com.liveramp.hank.zookeeper; import com.liveramp.hank.util.ExponentialBackoff; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.Watcher.Event.KeeperState; import org.apache.zookeeper.data.Stat; import java.util.HashSet; import java.util.Set; public abstract class WatchedNode<T> { private static final Logger LOG = LoggerFactory.getLogger(WatchedNode.class); private T value; private Long previousVersion = null; protected final String nodePath; private Stat stat = new Stat(); protected final ZooKeeperPlus zk; private final Set<WatchedNodeListener<T>> listeners = new HashSet<WatchedNodeListener<T>>(); private boolean cancelled = false; protected final T initialValue; protected final T emptyValue; private final Watcher watcher = new Watcher() { @Override public void process(WatchedEvent event) { if (!cancelled) { if (event.getState() == KeeperState.SyncConnected) { // If connected update data and notify listeners try { switch (event.getType()) { case NodeCreated: watchForData(); break; case NodeDeleted: // Previous version notified is null, and we will notify with null previousVersion = null; watchForCreation(); break; case NodeDataChanged: watchForData(); break; } } catch (KeeperException e) { LOG.error("Exception while trying to update our cached value for " + nodePath, e); } catch (InterruptedException e) { if (LOG.isTraceEnabled()) { LOG.trace("Interrupted while trying to update our cached value for " + nodePath, e); } } // Notify of new value if either we didn't notify of any value, or the node has changed long currentVersion = stat.getCtime() + stat.getMtime(); if (previousVersion == null || !previousVersion.equals(currentVersion)) { try { synchronized (listeners) { for (WatchedNodeListener<T> listener : listeners) { listener.onWatchedNodeChange(value); } } } finally { previousVersion = currentVersion; } } } else { // Not sync connected, do nothing if (LOG.isDebugEnabled()) { LOG.debug("Not sync connected anymore for watched node " + nodePath); } } } } }; // Start watching a node, optionnaly waiting for it to be created protected WatchedNode(final ZooKeeperPlus zk, final String nodePath, final boolean waitForCreation, final CreateMode createMode, final T initialValue, final T emptyValue) throws KeeperException, InterruptedException { this.zk = zk; this.nodePath = nodePath; this.initialValue = initialValue; this.emptyValue = emptyValue; // Immediately try to load the data, if it fails, then optionally create and wait if (!watchForData()) { // Optionally create the node if (createMode != null) { zk.create(nodePath, encode(initialValue), createMode); } // Optionally wait for the node's creation if (waitForCreation) { // Cannot wait for creation of sequential nodes (path is not yet known) if (createMode == CreateMode.EPHEMERAL_SEQUENTIAL || createMode == CreateMode.PERSISTENT_SEQUENTIAL) { throw new RuntimeException("Cannot wait for creation of sequential nodes"); } NodeCreationBarrier.block(zk, nodePath); watchForData(); } } } public WatchedNode(ZooKeeperPlus zk, String nodePath, boolean waitForCreation) throws InterruptedException, KeeperException { this(zk, nodePath, waitForCreation, null, null, null); } protected abstract T decode(byte[] data); protected abstract byte[] encode(T v); public void addListener(WatchedNodeListener<T> listener) { synchronized (listeners) { listeners.add(listener); } } public boolean removeListener(WatchedNodeListener<T> listener) { synchronized (listeners) { return listeners.remove(listener); } } private void watchForCreation() throws InterruptedException, KeeperException { synchronized (this) { value = null; stat = new Stat(); } if (zk.exists(nodePath, watcher) != null) { watchForData(); } } private boolean watchForData() throws InterruptedException, KeeperException { if (LOG.isTraceEnabled()) { LOG.trace(String.format("Getting value for %s", nodePath)); } try { synchronized (this) { value = decode(zk.getData(nodePath, watcher, stat)); return true; } } catch (KeeperException.NoNodeException e) { watchForCreation(); return false; } } public T get() { return value; } public void set(T v) throws KeeperException, InterruptedException { zk.setData(nodePath, encode(v), -1); } public void update(WatchedNodeUpdater<T> updater) throws InterruptedException, KeeperException { ExponentialBackoff backoff = new ExponentialBackoff(); while (true) { try { synchronized (this) { zk.setData(nodePath, encode(updater.update(value)), stat.getVersion()); } } catch (KeeperException.BadVersionException e) { // If we did not update from the latest version, backoff and retry. if (LOG.isDebugEnabled()) { LOG.debug("Did not have latest version to update node " + nodePath + ". Backing off for " + backoff.getBackoffMs() + " ms"); } backoff.backoff(); continue; } break; } } public String getPath() { return nodePath; } public void cancelWatch() { cancelled = true; } public void ensureCreated(CreateMode createMode) throws InterruptedException, KeeperException { zk.ensureCreated(nodePath, encode(initialValue), createMode); } }