// ================================================================================================= // Copyright 2011 Twitter, Inc. // ------------------------------------------------------------------------------------------------- // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this work except in compliance with the License. // You may obtain a copy of the License in the LICENSE file, or 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.twitter.common.zookeeper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Functions; import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import com.twitter.common.base.*; import com.twitter.common.util.BackoffHelper; import com.twitter.common.zookeeper.ZooKeeperClient.ZooKeeperConnectionException; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.data.Stat; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; /** * An implementation of {@link Supplier} that offers a readonly view of a * zookeeper data node. This class is thread-safe. * * Instances of this class each maintain a zookeeper watch for the remote data node. Instances * of this class should be created via the {@link #create} factory method. * * Please see zookeeper documentation and talk to your cluster administrator for guidance on * appropriate node size and total number of nodes you should be using. * * @param <T> the type of data this node stores * * @author Adam Samet */ public class ZooKeeperNode<T> implements Supplier<T> { /** * Deserializer for the constructor if you want to simply store the zookeeper byte[] data * as-is. */ public static final Function<byte[], byte[]> BYTE_ARRAY_VALUE = Functions.identity(); private static final Logger LOG = Logger.getLogger(ZooKeeperNode.class.getName()); private final ZooKeeperClient zkClient; private final String nodePath; private final NodeDeserializer<T> deserializer; private final BackoffHelper backoffHelper; // Whether it's safe to re-establish watches if our zookeeper session has expired. private final Object safeToRewatchLock; private volatile boolean safeToRewatch; private final T NO_DATA = null; @Nullable private volatile T nodeData; private final Closure<T> dataUpdateListener; /** * Returns an initialized ZooKeeperNode. The given node must exist at the time of object * creation or a {@link KeeperException} will be thrown. * * @param zkClient a zookeeper client * @param nodePath path to a node whose data will be watched * @param deserializer a function that converts byte[] data from a zk node to this supplier's * type T * * @throws InterruptedException if the underlying zookeeper server transaction is interrupted * @throws KeeperException.NoNodeException if the given nodePath doesn't exist * @throws KeeperException if the server signals an error * @throws ZooKeeperConnectionException if there was a problem connecting to the zookeeper * cluster */ public static <T> ZooKeeperNode<T> create(ZooKeeperClient zkClient, String nodePath, Function<byte[], T> deserializer) throws InterruptedException, KeeperException, ZooKeeperConnectionException { return create(zkClient, nodePath, deserializer, Closures.<T>noop()); } /** * Like the above, but optionally takes in a {@link Closure} that will get notified * whenever the data is updated from the remote node. * * @param dataUpdateListener a {@link Closure} to receive data update notifications. */ public static <T> ZooKeeperNode<T> create(ZooKeeperClient zkClient, String nodePath, Function<byte[], T> deserializer, Closure<T> dataUpdateListener) throws InterruptedException, KeeperException, ZooKeeperConnectionException { return create(zkClient, nodePath, new FunctionWrapper<T>(deserializer), dataUpdateListener); } /** * Returns an initialized ZooKeeperNode. The given node must exist at the time of object * creation or a {@link KeeperException} will be thrown. * * @param zkClient a zookeeper client * @param nodePath path to a node whose data will be watched * @param deserializer an implentation of {@link NodeDeserializer} that converts a byte[] from a * zk node to this supplier's type T. Also supplies a {@link Stat} object which is useful for * doing versioned updates. * * @throws InterruptedException if the underlying zookeeper server transaction is interrupted * @throws KeeperException.NoNodeException if the given nodePath doesn't exist * @throws KeeperException if the server signals an error * @throws ZooKeeperConnectionException if there was a problem connecting to the zookeeper * cluster */ public static <T> ZooKeeperNode<T> create(ZooKeeperClient zkClient, String nodePath, NodeDeserializer<T> deserializer) throws InterruptedException, KeeperException, ZooKeeperConnectionException { return create(zkClient, nodePath, deserializer, Closures.<T>noop()); } /** * Like the above, but optionally takes in a {@link Closure} that will get notified * whenever the data is updated from the remote node. * * @param dataUpdateListener a {@link Closure} to receive data update notifications. */ public static <T> ZooKeeperNode<T> create(ZooKeeperClient zkClient, String nodePath, NodeDeserializer<T> deserializer, Closure<T> dataUpdateListener) throws InterruptedException, KeeperException, ZooKeeperConnectionException { ZooKeeperNode<T> zkNode = new ZooKeeperNode<T>(zkClient, nodePath, deserializer, dataUpdateListener); zkNode.init(); return zkNode; } /** * Initializes a ZooKeeperNode. The given node must exist at the time of object creation or * a {@link KeeperException} will be thrown. * * Please note that this object will not track any remote zookeeper data until {@link #init()} * is successfully called. After construction and before that call, this {@link Supplier} will * return null. * * @param zkClient a zookeeper client * @param nodePath path to a node whose data will be watched * @param deserializer an implementation of {@link NodeDeserializer} that converts byte[] data * from a zk node to this supplier's type T * @param dataUpdateListener a {@link Closure} to receive data update notifications. */ @VisibleForTesting ZooKeeperNode(ZooKeeperClient zkClient, String nodePath, NodeDeserializer<T> deserializer, Closure<T> dataUpdateListener) { this.zkClient = Preconditions.checkNotNull(zkClient); this.nodePath = MorePreconditions.checkNotBlank(nodePath); this.deserializer = Preconditions.checkNotNull(deserializer); this.dataUpdateListener = Preconditions.checkNotNull(dataUpdateListener); backoffHelper = new BackoffHelper(); safeToRewatchLock = new Object(); safeToRewatch = false; nodeData = NO_DATA; } /** * Initialize zookeeper tracking for this {@link Supplier}. Once this call returns, this object * will be tracking data in zookeeper. * * @throws InterruptedException if the underlying zookeeper server transaction is interrupted * @throws KeeperException if the server signals an error * @throws ZooKeeperConnectionException if there was a problem connecting to the zookeeper * cluster */ @VisibleForTesting void init() throws InterruptedException, KeeperException, ZooKeeperConnectionException { Watcher watcher = zkClient.registerExpirationHandler(new Command() { @Override public void execute() { try { synchronized (safeToRewatchLock) { if (safeToRewatch) { tryWatchDataNode(); } } } catch (InterruptedException e) { LOG.log(Level.WARNING, "Interrupted while trying to re-establish watch.", e); } } }); try { /* * Synchronize to prevent the race of watchDataNode completing and then the session expiring * before we update safeToRewatch. */ synchronized (safeToRewatchLock) { watchDataNode(); safeToRewatch = true; } } catch (InterruptedException e) { zkClient.unregister(watcher); throw e; } catch (KeeperException e) { zkClient.unregister(watcher); throw e; } catch (ZooKeeperConnectionException e) { zkClient.unregister(watcher); throw e; } } /** * Returns the data corresponding to a byte array in a remote zookeeper node. This data is * cached locally and updated in the background on watch notifications. * * @return the data currently cached locally or null if {@link #init()} hasn't been called * or the backing node has no data or does not exist anymore. */ @Override public @Nullable T get() { return nodeData; } @VisibleForTesting void updateData(@Nullable T newData) { nodeData = newData; dataUpdateListener.execute(newData); } private void tryWatchDataNode() throws InterruptedException { backoffHelper.doUntilSuccess(new ExceptionalSupplier<Boolean, InterruptedException>() { @Override public Boolean get() throws InterruptedException { try { watchDataNode(); return true; } catch (KeeperException e) { return false; } catch (ZooKeeperConnectionException e) { return false; } } }); } private void watchDataNode() throws InterruptedException, KeeperException, ZooKeeperConnectionException { final Watcher nodeWatcher = new Watcher() { @Override public void process(WatchedEvent event) { if (event.getType() == Watcher.Event.EventType.NodeDataChanged || event.getType() == Watcher.Event.EventType.NodeDeleted) { try { tryWatchDataNode(); } catch (InterruptedException e) { LOG.log(Level.WARNING, "Interrupted while trying to watch a data node.", e); } } } }; try { Stat stat = new Stat(); byte[] rawData = zkClient.get().getData(nodePath, nodeWatcher, stat); T newData = deserializer.deserialize(rawData, stat); updateData(newData); } catch (KeeperException.NoNodeException e) { /* * This node doesn't exist right now, reflect that locally and then create a watch to wait * for its recreation. */ updateData(NO_DATA); watchForExistence(); } } private void watchForExistence() throws InterruptedException, KeeperException, ZooKeeperConnectionException { final Watcher watcher = new Watcher() { @Override public void process(WatchedEvent event) { if (event.getType() == Watcher.Event.EventType.NodeCreated) { try { tryWatchDataNode(); } catch (InterruptedException e) { LOG.log(Level.WARNING, "Interrupted while trying to watch a data node.", e); } } } }; /* * If the node was created between the getData call and this call, just try watching it. * We'll have an extra exists watch on it that goes off on its next deletion, which will * be a no-op. * Otherwise, just let the exists watch wait for its creation. */ if (zkClient.get().exists(nodePath, watcher) != null) { tryWatchDataNode(); } } /** * Interface for defining zookeeper node data deserialization. * * @param <T> the type of data associated with this node */ public interface NodeDeserializer<T> { /** * @param data the byte array returned from ZooKeeper when a watch is triggered. * @param stat a ZooKeeper {@link Stat} object. Populated by * {@link ZooKeeper#getData(String, boolean, Stat)}. */ T deserialize(byte[] data, Stat stat); } // wrapper for backwards compatibility with older create() methods with Function parameter private static final class FunctionWrapper<T> implements NodeDeserializer<T> { private final Function<byte[], T> func; private FunctionWrapper(Function<byte[], T> func) { Preconditions.checkNotNull(func); this.func = func; } public T deserialize(byte[] rawData, Stat stat) { return func.apply(rawData); } } }