com.twitter.common.zookeeper.ZooKeeperMap.java Source code

Java tutorial

Introduction

Here is the source code for com.twitter.common.zookeeper.ZooKeeperMap.java

Source

// =================================================================================================
// 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 java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;

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.collect.ForwardingMap;
import com.google.common.collect.Sets;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;

import com.twitter.common.base.Command;
import com.twitter.common.base.ExceptionalSupplier;
import com.twitter.common.base.MorePreconditions;
import com.twitter.common.util.BackoffHelper;
import com.twitter.common.zookeeper.ZooKeeperClient.ZooKeeperConnectionException;

/**
 * A ZooKeeper backed {@link Map}.  Initialized with a node path, this map represents child nodes
 * under that path as keys, with the data in those nodes as values.  This map is readonly from
 * clients of this class, and only can be modified via direct zookeeper operations.
 *
 * Note that instances of this class maintain a zookeeper watch for each zookeeper node under the
 * parent, as well as on the parent itself.  Instances of this class should be created via the
 * {@link #create} factory method.
 *
 * As of ZooKeeper Version 3.1, the maximum allowable size of a data node is 1 MB.  A single
 * client should be able to hold up to maintain several thousand watches, but this depends on rate
 * of data change as well.
 *
 * Talk to your zookeeper cluster administrator if you expect number of map entries times number
 * of live clients to exceed a thousand, as a zookeeper cluster is limited by total number of
 * server-side watches enabled.
 *
 * For an example of a set of tools to maintain one of these maps, please see
 * src/scripts/HenAccess.py in the hen repository.
 *
 * @param <V> the type of values this map stores
 */
public class ZooKeeperMap<V> extends ForwardingMap<String, V> {

    /**
     * An optional listener which can be supplied and triggered when entries in a ZooKeeperMap
     * are added, changed or removed. For a ZooKeeperMap of type <V>, the listener will fire a
     * "nodeChanged" event with the name of the ZNode that changed, and its resulting value as
     * interpreted by the provided deserializer. Removal of child nodes triggers the "nodeRemoved"
     * method indicating the name of the ZNode which is no longer present in the map.
     */
    public interface Listener<V> {

        /**
         * Fired when a node is added to the ZooKeeperMap or changed.
         *
         * @param nodeName indicates the name of the ZNode that was added or changed.
         * @param value is the new value of the node after passing through your supplied deserializer.
         */
        void nodeChanged(String nodeName, V value);

        /**
         * Fired when a node is removed from the ZooKeeperMap.
         *
         * @param nodeName indicates the name of the ZNode that was removed from the ZooKeeperMap.
         */
        void nodeRemoved(String nodeName);
    }

    /**
     * Default deserializer for the constructor if you want to simply store the zookeeper byte[] data
     * in this map.
     */
    public static final Function<byte[], byte[]> BYTE_ARRAY_VALUES = Functions.identity();

    /**
     * A listener that ignores all events.
     */
    public static <T> Listener<T> noopListener() {
        return new Listener<T>() {
            @Override
            public void nodeChanged(String nodeName, T value) {
            }

            @Override
            public void nodeRemoved(String nodeName) {
            }
        };
    }

    private static final Logger LOG = Logger.getLogger(ZooKeeperMap.class.getName());

    private final ZooKeeperClient zkClient;
    private final String nodePath;
    private final Function<byte[], V> deserializer;

    private final ConcurrentMap<String, V> localMap;
    private final Map<String, V> unmodifiableLocalMap;
    private final BackoffHelper backoffHelper;

    private final Listener<V> mapListener;

    // Whether it's safe to re-establish watches if our zookeeper session has expired.
    private final Object safeToRewatchLock;
    private volatile boolean safeToRewatch;

    /**
     * Returns an initialized ZooKeeperMap.  The given path must exist at the time of
     * 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 map's
     *     value type V
     * @param listener is a Listener which fires when values are added, changed, or removed.
     *
     * @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 <V> ZooKeeperMap<V> create(ZooKeeperClient zkClient, String nodePath,
            Function<byte[], V> deserializer, Listener<V> listener)
            throws InterruptedException, KeeperException, ZooKeeperConnectionException {

        ZooKeeperMap<V> zkMap = new ZooKeeperMap<V>(zkClient, nodePath, deserializer, listener);
        zkMap.init();
        return zkMap;
    }

    /**
     * Returns an initialized ZooKeeperMap.  The given path must exist at the time of
     * 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 map's
     *     value type V
     *
     * @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 <V> ZooKeeperMap<V> create(ZooKeeperClient zkClient, String nodePath,
            Function<byte[], V> deserializer)
            throws InterruptedException, KeeperException, ZooKeeperConnectionException {

        return ZooKeeperMap.create(zkClient, nodePath, deserializer, ZooKeeperMap.<V>noopListener());
    }

    /**
     * Initializes a ZooKeeperMap.  The given path 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 Map} will
     * be empty.
     *
     * @param zkClient a zookeeper client
     * @param nodePath top-level node path under which the map data lives
     * @param deserializer a function that converts byte[] data from a zk node to this map's
     *     value type V
     * @param mapListener is a Listener which fires when values are added, changed, or removed.
     *
     * @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
     */
    @VisibleForTesting
    ZooKeeperMap(ZooKeeperClient zkClient, String nodePath, Function<byte[], V> deserializer,
            Listener<V> mapListener) throws InterruptedException, KeeperException, ZooKeeperConnectionException {

        super();

        this.mapListener = Preconditions.checkNotNull(mapListener);
        this.zkClient = Preconditions.checkNotNull(zkClient);
        this.nodePath = MorePreconditions.checkNotBlank(nodePath);
        this.deserializer = Preconditions.checkNotNull(deserializer);

        localMap = new ConcurrentHashMap<String, V>();
        unmodifiableLocalMap = Collections.unmodifiableMap(localMap);
        backoffHelper = new BackoffHelper();
        safeToRewatchLock = new Object();
        safeToRewatch = false;

        if (zkClient.get().exists(nodePath, null) == null) {
            throw new KeeperException.NoNodeException();
        }
    }

    /**
     * Initialize zookeeper tracking for this {@link Map}.  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() {
                /*
                 * First rewatch all of our locally cached children.  Some of them may not exist anymore,
                 * which will lead to caught KeeperException.NoNode whereafter we'll remove that child
                 * from the cached map.
                 *
                 * Next, we'll establish our top level child watch and add any new nodes that might exist.
                 */
                try {
                    synchronized (safeToRewatchLock) {
                        if (safeToRewatch) {
                            rewatchDataNodes();
                            tryWatchChildren();
                        }
                    }
                } catch (InterruptedException e) {
                    LOG.log(Level.WARNING, "Interrupted while trying to re-establish watch.", e);
                    Thread.currentThread().interrupt();
                }
            }
        });

        try {
            // Synchronize to prevent the race of watchChildren completing and then the session expiring
            // before we update safeToRewatch.
            synchronized (safeToRewatchLock) {
                watchChildren();
                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;
        }
    }

    @Override
    protected Map<String, V> delegate() {
        return unmodifiableLocalMap;
    }

    private void tryWatchChildren() throws InterruptedException {
        backoffHelper.doUntilSuccess(new ExceptionalSupplier<Boolean, InterruptedException>() {
            @Override
            public Boolean get() throws InterruptedException {
                try {
                    watchChildren();
                    return true;
                } catch (KeeperException e) {
                    return false;
                } catch (ZooKeeperConnectionException e) {
                    return false;
                }
            }
        });
    }

    private synchronized void watchChildren()
            throws InterruptedException, KeeperException, ZooKeeperConnectionException {

        /*
         * Add a watch on the parent node itself, and attempt to rewatch if it
         * gets deleted
         */
        zkClient.get().exists(nodePath, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                if (event.getType() == Watcher.Event.EventType.NodeDeleted) {
                    // If the parent node no longer exists
                    localMap.clear();
                    try {
                        tryWatchChildren();
                    } catch (InterruptedException e) {
                        LOG.log(Level.WARNING, "Interrupted while trying to watch children.", e);
                        Thread.currentThread().interrupt();
                    }
                }
            }
        });

        final Watcher childWatcher = new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                if (event.getType() == Watcher.Event.EventType.NodeChildrenChanged) {
                    try {
                        tryWatchChildren();
                    } catch (InterruptedException e) {
                        LOG.log(Level.WARNING, "Interrupted while trying to watch children.", e);
                        Thread.currentThread().interrupt();
                    }
                }
            }
        };

        List<String> children = zkClient.get().getChildren(nodePath, childWatcher);
        updateChildren(Sets.newHashSet(children));
    }

    private void tryAddChild(final String child) throws InterruptedException {
        backoffHelper.doUntilSuccess(new ExceptionalSupplier<Boolean, InterruptedException>() {
            @Override
            public Boolean get() throws InterruptedException {
                try {
                    addChild(child);
                    return true;
                } catch (KeeperException e) {
                    return false;
                } catch (ZooKeeperConnectionException e) {
                    return false;
                }
            }
        });
    }

    // TODO(Adam Samet) - Make this use the ZooKeeperNode class.
    private void addChild(final String child)
            throws InterruptedException, KeeperException, ZooKeeperConnectionException {

        final Watcher nodeWatcher = new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                if (event.getType() == Watcher.Event.EventType.NodeDataChanged) {
                    try {
                        tryAddChild(child);
                    } catch (InterruptedException e) {
                        LOG.log(Level.WARNING, "Interrupted while trying to add a child.", e);
                        Thread.currentThread().interrupt();
                    }
                } else if (event.getType() == Watcher.Event.EventType.NodeDeleted) {
                    removeEntry(child);
                }
            }
        };

        try {
            V value = deserializer.apply(zkClient.get().getData(makePath(child), nodeWatcher, null));
            putEntry(child, value);
        } catch (KeeperException.NoNodeException e) {
            // This node doesn't exist anymore, remove it from the map and we're done.
            removeEntry(child);
        }
    }

    @VisibleForTesting
    void removeEntry(String key) {
        localMap.remove(key);
        mapListener.nodeRemoved(key);
    }

    @VisibleForTesting
    void putEntry(String key, V value) {
        localMap.put(key, value);
        mapListener.nodeChanged(key, value);
    }

    private void rewatchDataNodes() throws InterruptedException {
        for (String child : keySet()) {
            tryAddChild(child);
        }
    }

    private String makePath(final String child) {
        return nodePath + "/" + child;
    }

    private void updateChildren(Set<String> zkChildren) throws InterruptedException {
        Set<String> addedChildren = Sets.difference(zkChildren, keySet());
        Set<String> removedChildren = Sets.difference(keySet(), zkChildren);
        for (String child : addedChildren) {
            tryAddChild(child);
        }
        for (String child : removedChildren) {
            removeEntry(child);
        }
    }
}