com.bigdata.zookeeper.HierarchicalZNodeWatcher.java Source code

Java tutorial

Introduction

Here is the source code for com.bigdata.zookeeper.HierarchicalZNodeWatcher.java

Source

/*
    
Copyright (C) SYSTAP, LLC 2006-2008.  All rights reserved.
    
Contact:
 SYSTAP, LLC
 4501 Tower Road
 Greensboro, NC 27410
 licenses@bigdata.com
    
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.
    
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
    
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
/*
 * Created on Jan 11, 2009
 */

package com.bigdata.zookeeper;

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import org.apache.log4j.Logger;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.KeeperException.ConnectionLossException;
import org.apache.zookeeper.KeeperException.NoNodeException;
import org.apache.zookeeper.KeeperException.SessionExpiredException;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.data.Stat;

import com.bigdata.btree.IRangeQuery;

/**
 * This class accepts a dynamic set of watch criteria and pump events into a
 * queue when any watch was triggered. Hooks are defined that you can use to
 * identify new watch criteria based on {@link WatchedEvent}s. By default, this
 * will extend a watch over the transitive closure of the children of some
 * znode. However, you can limit the set of watch criteria using some hook
 * methods. For example, only following certain children for a given path.
 * <p>
 * {@link WatchedEvent}s are received in the zookeeper event thread. In order to
 * minimize the work performed in that thread, events are triaged. The triage
 * determines the "type" of the znode for which the event was received. Types
 * reflect application considerations. For example, a change in the #of children
 * of a logical service versus a change in the data for a service configuration.
 * If the typed event adds or removes children that are of interest (by default,
 * all children are of interest, but you may place restrictions on that), then
 * new watch criteria are added for the newly identified children. The typed
 * event is then placed on an unbounded {@link BlockingQueue}. The application
 * is responsible for draining the queue. The constructor establishes the
 * initial watch on the root of the hierarchy of interest and then returns
 * control to the application. All other work by this class happens in the
 * zookeeper event thread.
 * <p>
 * Zookeeper DOES NOT provide a guarantee that we will see all events of
 * interest since state changes may occur after a watch has been triggered and
 * before the {@link WatchedEvent} is received and before we have the chance to
 * reset the watch. By queuing events we reduce the time in the zookeeper event
 * thread and thus increase responsiveness and reduce the opportunity for missed
 * events, but we DO NOT guarantee that the application will see all events.
 * 
 * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
 * @version $Id$
 * 
 * @todo This re-establishes the watcher when the event is handled in the
 *       zookeeper thread. That is probably not a good idea as it increases the
 *       demand on the zookeeper thread. Instead, when the application sees the
 *       wrapped event it should read the data, re-establishing the watch as a
 *       side-effect, and then issue whatever actions are necessary based on the
 *       delta between the historical state of the znode/children and their
 *       current state. This pattern guarantees that no state changes are
 *       missed, but requires the application to track the old state of each
 *       znode/children of interest.
 */
abstract public class HierarchicalZNodeWatcher implements Watcher, HierarchicalZNodeWatcherFlags {

    final static protected Logger log = Logger.getLogger(UnknownChildrenWatcher.class);

    private ZooKeeper zookeeper;

    /**
     * The root of the hierarchy of watched znodes.
     */
    final protected String zroot;

    /**
     * New events are not accepted once this flag has been set.
     * 
     * @see #cancel()
     */
    private volatile boolean cancelled = false;

    /**
     * This is true until the startup scan is complete.
     */
    private volatile boolean pumpMockEvents = true;

    /**
     * Queue to be drained. It is populated by the {@link Watcher} in the event
     * thread and must be drained in an application thread. All events for
     * watched znodes are included.
     */
    public final BlockingQueue<WatchedEvent> queue = new LinkedBlockingQueue<WatchedEvent>();

    /**
     * The set of znodes that are being watched.
     * <p>
     * Note: {@link #process(WatchedEvent)} runs in the zookeeper event thread.
     * It is marked as <code>synchronized</code> so that any methods which
     * access this collection outside of that thread may be mutex with the
     * event thread simply by synchronizing on <i>this</i>.
     */
    private final LinkedHashMap<String/* zpath */, Integer/* flags */> watched = new LinkedHashMap<String, Integer>();

    /**
     * Sets a watch on the <i>zroot</i> and any descendants selected by
     * <i>flags</i> and {@link #watch(String, String)}.
     * <p>
     * If {@link HierarchicalZNodeWatcherFlags#CHILDREN} is set, then watches
     * are set using {@link #setWatch(String, int)} for any child for which
     * {@link #watch(String, String)} returns non-zero. This process is
     * recursive if {@link HierarchicalZNodeWatcherFlags#CHILDREN} is set for
     * any of those children.
     * 
     * @param zookeeper
     * @param zroot
     *            The root of the watched hierarchy.
     * @param flags
     *            The flags for the watches to be set on the zroot. The
     *            {@link HierarchicalZNodeWatcherFlags#EXITS} and
     *            {@link HierarchicalZNodeWatcherFlags#CHILDREN} are
     *            automatically included, but you must specify
     *            {@link HierarchicalZNodeWatcherFlags#DATA} if you want to
     *            watch the data on the zroot as well.
     * 
     * @throws InterruptedException
     * @throws KeeperException
     */
    public HierarchicalZNodeWatcher(final ZooKeeper zookeeper, final String zroot, int flags)
            throws InterruptedException, KeeperException {

        this(zookeeper, zroot, flags, false/* pumpMockEventsDuringStartup */);

    }

    /**
     * 
     * @param zookeeper
     * @param zroot
     * @param flags
     * @param pumpMockEventsDuringStartup
     *            When true, mock events are placed in the {@link #queue} during
     *            the startup scan. This gives the application pre-order
     *            traversal events for the hierarchy. The children of a given
     *            not will appear in whatever order zookeeper reports (they are
     *            not sorted).
     * 
     * @throws InterruptedException
     * @throws KeeperException
     */
    public HierarchicalZNodeWatcher(final ZooKeeper zookeeper, final String zroot, int flags,
            final boolean pumpMockEventsDuringStartup) throws InterruptedException, KeeperException {

        if (zookeeper == null)
            throw new IllegalArgumentException();

        if (zroot == null)
            throw new IllegalArgumentException();

        flags |= EXISTS | CHILDREN;

        //        if ((flags & ALL) == 0)
        //            throw new IllegalArgumentException();

        this.zookeeper = zookeeper;

        this.zroot = zroot;

        if (log.isInfoEnabled())
            log.info("zroot=" + zroot + ", flags=" + flagString(flags));

        /*
         * Set the initial watches.
         * 
         * @todo If we are going to pump events into the queue during startup
         * then we might consider making start mutex with the event handler.
         * 
         * synchronized(this) does the trick.
         */
        pumpMockEvents = pumpMockEventsDuringStartup;

        synchronized (this) {

            if (pumpMockEvents)
                placeMockEventInQueue(zroot, flags);

            setWatch(zroot, flags);

            addedWatch(zroot, flags);

        }

        pumpMockEvents = false;

    }

    /**
     * Note: Synchronized for mutex with {@link #cancel()}.
     */
    synchronized public void process(final WatchedEvent event) {

        if (cancelled) {

            // ignore events once cancelled.

            return;

        }

        if (log.isInfoEnabled())
            log.info(event.toString());

        /*
         * Put ALL events in the queue.
         * 
         * Note: This does NOT mean that the application will see all state
         * changes for watched znodes. Zookeeper DOES NOT provide that
         * guarantee. The application CAN miss events between the time that a
         * state change triggers a WatchedEvent and the time that the
         * application handles the event and resets the watch.
         */

        queue.add(event);

        switch (event.getState()) {
        case Disconnected:
            return;
        default:
            break;
        }

        final String path = event.getPath();

        switch (event.getType()) {

        case NodeCreated:

            /*
             * We always reset the watch when we see a NodeCreated event for a
             * znode that we are already watching.
             * 
             * This event type can occur for the root of the watched hierarchy
             * since we set the watch regardless of the existence of the zroot.
             * 
             * If the event occurs for a different znode then it may have been
             * deleted and re-created and we may have missed the delete event.
             */

            try {
                zookeeper.exists(path, this);
            } catch (KeeperException e1) {
                log.error("path=" + path, e1);
            } catch (InterruptedException e1) {
                if (log.isInfoEnabled())
                    log.info("path=" + path);
            }

            return;

        case NodeDeleted:

            /*
             * A watched znode was deleted. Unless this is the zroot, we will
             * remove our watch on the znode (the expectation is that the watch
             * will be re-established if the child is re-created since we will
             * notice the NodeChildrenChanged event).
             */

            if (zroot.equals(path)) {

                try {
                    zookeeper.exists(path, this);
                } catch (KeeperException e1) {
                    log.error("path=" + path, e1);
                } catch (InterruptedException e1) {
                    if (log.isInfoEnabled())
                        log.info("path=" + path);
                }

            } else {

                watched.remove(path);

                removedWatch(path);

            }

            return;

        case NodeDataChanged:

            try {
                zookeeper.getData(path, this, new Stat());
            } catch (KeeperException e1) {
                log.error("path=" + path, e1);
            } catch (InterruptedException e1) {
                if (log.isInfoEnabled())
                    log.info("path=" + path);
            }

            return;

        case NodeChildrenChanged:
            /*
             * Get the children (and reset our watch on the path).
             */
            try {
                acceptChildren(path, zookeeper.getChildren(event.getPath(), this));
            } catch (KeeperException e) {
                log.error(this, e);
            } catch (InterruptedException e1) {
                if (log.isInfoEnabled())
                    log.info("path=" + path);
            }
            return;
        default:
            return;
        }

    }

    /**
     * Sets watches for the path identified by the flags. If the
     * {@link #CHILDREN} flag is set, then watches are set recursively for any
     * child for which {@link #watch(String, String)} returns non-zero.
     * 
     * @param path
     * @param flags
     * 
     * @throws KeeperException
     * @throws InterruptedException
     */
    protected void setWatch(final String path, final int flags) throws KeeperException, InterruptedException {

        if (log.isInfoEnabled())
            log.info("zpath=" + path + ", flags=" + flagString(flags));

        watched.put(path, flags);

        if ((flags & EXISTS) != 0) {

            zookeeper.exists(path, this);

        }

        if ((flags & DATA) != 0) {

            zookeeper.getData(path, this, new Stat());

        }

        if ((flags & CHILDREN) != 0) {

            try {

                acceptChildren(path, zookeeper.getChildren(path, this));

            } catch (NoNodeException ex) {

                /*
                 * If there are no children now, then that is Ok. If they are
                 * created later then we will notice it if EXISTS was specified
                 * and we will handle them then.
                 */

                if (log.isInfoEnabled())
                    log.info("No children: " + path);

            }

        }

    }

    /**
     * Return <code>true</code> if the path is being watched.
     * 
     * @param path
     * 
     * @return
     */
    synchronized public boolean isWatched(final String path) {

        if (path == null)
            throw new IllegalArgumentException();

        return watched.containsKey(path);

    }

    /**
     * Return the flags used to watch the path.
     * 
     * @param path The path.
     * 
     * @return The flags -or- {@link HierarchicalZNodeWatcherFlags#NONE} if the
     *         path is not watched.
     */
    synchronized public int getFlags(final String path) {

        if (path == null)
            throw new IllegalArgumentException();

        final Integer flags = watched.get(path);

        if (flags == null) {

            if (log.isDebugEnabled())
                log.debug("path=" + path + " : Not watched");

            return NONE;

        }

        if (log.isDebugEnabled())
            log.debug("path=" + path + " : " + flagString(flags));

        return flags;

    }

    /**
     * Returns a snapshot of the set of watched nodes.
     * 
     * @return The zpath to all nodes that are being watched.
     */
    synchronized public String[] getWatchedNodes() {

        return watched.keySet().toArray(new String[0]);

    }

    /**
     * Clears watches for the path identified by the flags, but does NOT
     * recursively clear watches for the children even if the {@link #CHILDREN}
     * flag is set.
     * 
     * @param path
     * @param flags
     * 
     * @throws KeeperException
     * @throws InterruptedException
     */
    private void clearWatch(final String path, final int flags) throws KeeperException, InterruptedException {

        if (log.isInfoEnabled())
            log.info("zpath=" + path + ", flags=" + flagString(flags));

        if ((flags & EXISTS) != 0) {

            zookeeper.exists(path, false);

        }

        if ((flags & DATA) != 0) {

            zookeeper.getData(path, false, new Stat());

        }

        if ((flags & CHILDREN) != 0) {

            zookeeper.getChildren(path, false);

        }

        removedWatch(path);

    }

    /**
     * Examines the list of children. If the child is not being watched and
     * {@link #watch(String, String)} returns non-zero, then sets the
     * appropriate watches.
     * <p>
     * Note: This can cause recursion through the znode hierarchy. That
     * recursion will occur in the zookeeper event thread. Therefor it is
     * important to focus recursion only on the znodes that are truely of
     * interest.
     * 
     * @param path
     * @param children
     * 
     * @throws KeeperException
     * @throws InterruptedException
     */
    private void acceptChildren(final String path, final List<String> children)
            throws KeeperException, InterruptedException {

        for (String child : children) {

            final String zpath = path + "/" + child;

            if (isWatched(zpath))
                continue;

            final int flags = watch(path, child);

            if (log.isInfoEnabled())
                log.info("watch? " + zpath + " : flags=" + flagString(flags));

            if (flags != 0) {

                if (pumpMockEvents)
                    placeMockEventInQueue(zpath, flags);

                setWatch(zpath, flags);

                addedWatch(zpath, flags);

            }

        }

    }

    /**
     * Notification method is invoked each time we add a znode to the set of
     * znodes that we are watching. <strong>This method is invoked in the
     * zookeeper event thread.</strong>
     * 
     * @param zpath
     *            The path to the znode.
     * @param flags
     *            The flags that will be used to watch the znode.
     */
    protected void addedWatch(String zpath, int flags) {

    }

    /**
     * This is used pump mock events into the {@link #queue} during startup.
     * 
     * @param path
     * @param flags
     */
    protected void placeMockEventInQueue(final String path, final int flags) {

        if (!pumpMockEvents) {

            /*
             * Note: If we are receiving real events then we don't want to do
             * this. It is just for producing fake events during startup.
             */

            throw new IllegalStateException();

        }

        if (log.isInfoEnabled())
            log.info("path=" + path + ", flags=" + flagString(flags));

        if ((flags & EXISTS) != 0) {

            queue.add(new WatchedEvent(Event.EventType.NodeCreated, Event.KeeperState.Unknown, path));

        }

        if ((flags & DATA) != 0) {

            queue.add(new WatchedEvent(Event.EventType.NodeDataChanged, Event.KeeperState.Unknown, path));

        }

        if ((flags & CHILDREN) != 0) {

            queue.add(new MockWatchedEvent(Event.EventType.NodeChildrenChanged, Event.KeeperState.Unknown, path));

        }

    }

    /**
     * A mock {@link WatchedEvent} used to pump mock events into the queue
     * during a startup scan.
     * 
     * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
     * @version $Id$
     */
    static private class MockWatchedEvent extends WatchedEvent {

        /**
         * @param eventType
         * @param keeperState
         * @param path
         */
        public MockWatchedEvent(EventType eventType, KeeperState keeperState, String path) {

            super(eventType, keeperState, path);

        }

    }

    /**
     * Notification method is invoked each time we remove a znode from the set
     * of znodes that we are watching. <strong>This method is invoked in the
     * zookeeper event thread.</strong>
     * 
     * @param zpath
     *            The path to the znode.
     */
    protected void removedWatch(String zpath) {

    }

    /**
     * Return flags indicating whether the child should be watched.
     * <p>
     * <strong>Applications SHOULD use this method to restrict the growth of the
     * watched znode hierarchy to ONLY those znodes of interest</strong>.
     * Failure to do this will cause processing in the zookeeper event thread
     * which grows with the #of znodes spanned by the watched hierarchy and can
     * seriously impact the responsiveness of the zookeeper event thread.
     * 
     * @param parent
     *            The zpath to the parent.
     * @param child
     *            The child znode.
     * 
     * @return The watches to establish for the child -or- {@link #NONE} to not
     *         watch the child.
     */
    abstract protected int watch(final String parent, final String child);

    /**
     * Cancel all watches. This clears all watches, including on the
     * {@link #zroot}, so that zookeeper will no longer deliver
     * {@link WatchedEvent}s to this class. The {@link #queue} is also cleared
     * and a flag is set such that such that subsequent events are ignored.
     * <p>
     * Note: This is <code>synchronized</code> so that it is mutex with
     * {@link #process(WatchedEvent)}.
     * 
     * @throws InterruptedException
     */
    synchronized public void cancel() throws InterruptedException {

        /*
         * Note: Once we set this flag we will not propagate any more events to
         * the application. If we are unable to clear some watches then those
         * events will just be discarded if they arrive.
         * 
         * Note: This allows us to safely ignore various errors from zookeeper
         * below.
         */
        cancelled = true;

        if (log.isInfoEnabled())
            log.info("Cancelling watches: #watched=" + watched.size());

        final Iterator<Map.Entry<String, Integer>> itr = watched.entrySet().iterator();

        while (itr.hasNext()) {

            final Map.Entry<String, Integer> entry = itr.next();

            final String path = entry.getKey();

            final int flags = entry.getValue();

            try {

                clearWatch(path, flags);

            } catch (ConnectionLossException e) {

                /*
                 * We can ignore these errors due to the cancelled flag above.
                 * The errors are logged at a low level because a modestly large
                 * number of such exceptions will occur if the application
                 * cancels this watcher it is NOT connected to zookeeper and
                 */

                if (log.isInfoEnabled())
                    log.info("path=" + path + " : " + e);

            } catch (SessionExpiredException e) {

                /*
                 * We can ignore these errors due to the cancelled flag above.
                 * The errors are logged at a low level because a modestly large
                 * number of such exceptions will occur if the application
                 * cancels this watcher it is NOT connected to zookeeper and
                 */

                if (log.isInfoEnabled())
                    log.info("path=" + path + " : " + e);

            } catch (KeeperException e) {

                log.error("path=" + path + " : " + e);

            }

            itr.remove();

        }

    }

    public String toString() {

        return getClass().getName() + "{zroot=" + zroot + ", watchedSize=" + getWatchedSize() + "}";

    }

    public int getWatchedSize() {

        /*
         * Note: Not synchronized since impl returns a member field.
         */

        // synchronized (this) {
        return watched.size();

        // }

    }

    /**
     * Externalizes the flags as a list of symbolic constants.
     * 
     * @param flags
     *            The {@link IRangeQuery} flags.
     * 
     * @return The list of symbolic constants.
     */
    public static String flagString(final int flags) {

        final StringBuilder sb = new StringBuilder();

        // #of flags that are turned on.
        int onCount = 0;

        sb.append("[");

        if ((flags & EXISTS) != 0) {

            if (onCount++ > 0)
                sb.append(",");

            sb.append("EXISTS");

        }

        if ((flags & DATA) != 0) {

            if (onCount++ > 0)
                sb.append(",");

            sb.append("DATA");

        }

        if ((flags & CHILDREN) != 0) {

            if (onCount++ > 0)
                sb.append(",");

            sb.append("CHILDREN");

        }

        sb.append("]");

        return sb.toString();

    }

}