All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.linkedin.zookeeper.tracker.ZooKeeperTreeTracker Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2010-2010 LinkedIn, Inc
 *
 * 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 org.linkedin.zookeeper.tracker;

import org.slf4j.Logger;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.linkedin.util.annotations.Initializable;
import org.linkedin.util.clock.Clock;
import org.linkedin.util.clock.ClockUtils;
import org.linkedin.util.clock.SystemClock;
import org.linkedin.util.concurrent.ConcurrentUtils;
import org.linkedin.util.io.PathUtils;
import org.linkedin.util.lang.LangUtils;
import org.linkedin.util.lifecycle.Destroyable;
import org.linkedin.zookeeper.client.IZKClient;
import org.linkedin.zookeeper.client.ZKChildren;
import org.linkedin.zookeeper.client.ZKData;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeoutException;


/**
 * The purpose of this class is to essentially keep a replica of ZooKeeper data in memory and
 * be able to be notified when it changes.
 *
 * @author [email protected] */
public class ZooKeeperTreeTracker implements Destroyable
{
  public static final String MODULE = ZooKeeperTreeTracker.class.getName();
  public static final Logger log = org.slf4j.LoggerFactory.getLogger(MODULE);

  @Initializable
  public Clock clock = SystemClock.INSTANCE;

  private final IZKClient _zk;
  private final ZKDataReader _zkDataReader;
  private final String _root;
  private final int _depth;

  private final Set _errorListeners = new LinkedHashSet();
  private final Set> _eventsListeners = new LinkedHashSet>();

  private volatile Map> _tree = new LinkedHashMap>();
  private volatile boolean _destroyed = false;
  private long _lastZkTxId;
  private final Object _lock = new Object();
  private final int _rootDepth;
  private final Watcher _treeWacher = new TreeWatcher();

  private class TreeWatcher implements Watcher
  {
    @Override
    public void process(WatchedEvent event)
    {
      if(log.isDebugEnabled())
        log.debug(logString(event.getPath(),
                            "treeWatcher: type=" + event.getType()) + ", state=" + event.getState());

      Collection> events = new ArrayList>();

      try
      {
        synchronized(_lock)
        {
          if(!handleEvent(event))
            return;

          switch(event.getType())
          {
            case NodeDeleted:
              _tree = handleNodeDeleted(event.getPath(), events);
              break;

            case NodeCreated:
              throw new RuntimeException("getting node created event ? when ?");

            case NodeChildrenChanged:
              _tree = handleNodeChildrenChanged(event.getPath(), events);
              break;

            case NodeDataChanged:
              _tree = handleNodeDataChanged(event.getPath(), events);
              break;

            case None:
              // nothing to do
              break;
          }
        }

        raiseEvents(events);
      }
      catch (Throwable th)
      {
        log.warn(logString(event.getPath(), "Error in treeWatcher (ignored)"), th);
        raiseError(event, th);
      }
    }
  }


  public ZooKeeperTreeTracker(IZKClient zk, ZKDataReader zkDataReader, String root)
  {
    this(zk, zkDataReader, root, Integer.MAX_VALUE);
  }

  public ZooKeeperTreeTracker(IZKClient zk, ZKDataReader zkDataReader, String root, int depth)
  {
    _zk = zk;
    _zkDataReader = zkDataReader;
    _root = root;
    _rootDepth = computeAbsoluteDepth(_root);
    _depth = depth;
  }

  public static int computeAbsoluteDepth(String path)
  {
    if(path == null)
      return 0;

    int depth = 0;

    for(int i = 0; i < path.length(); i++)
    {
      char c = path.charAt(i);
      if(c == '/')
        depth++;
    }

    return depth;
  }

  public String getRoot()
  {
    return _root;
  }

  public IZKClient getZKCient()
  {
    return _zk;
  }

  public int getDepth()
  {
    return _depth;
  }

  public long getLastZkTxId()
  {
    synchronized(_lock)
    {
      return _lastZkTxId;
    }
  }

  /**
   * Waits no longer than the timeout provided (or forever if null) for this tracker
   * to have seen at least the provided zookeeper transaction id
   * @return the last known zkTxId (guaranteed to be >= to zkTxId)
   */
  public long waitForZkTxId(long zkTxId, Object timeout)
    throws TimeoutException, InterruptedException
  {
    long endTime = ClockUtils.toEndTime(clock, timeout);

    synchronized(_lock)
    {
      while(_lastZkTxId < zkTxId)
      {
        ConcurrentUtils.awaitUntil(clock, _lock, endTime);
      }

      return _lastZkTxId;
    }
  }

  /**
   * It is not possible to remove a watcher... so we just set the tracker in destroyed mode which
   * will simply ignore all subsequent event.
   */
  @Override
  public void destroy()
  {
    synchronized(_lock)
    {
      _destroyed = true;
    }
  }

  /**
   * @return the tree
   */
  public Map> getTree()
  {
    // note that there is no need for synchronization because:
    // 1. the attribute is volatile
    // 2. the map is never modified (a new one is created / replaced when needed)
    return _tree;
  }

  public void track(NodeEventsListener eventsListener)
    throws InterruptedException, KeeperException
  {
    registerListener(eventsListener);
    track();
  }

  public void track() throws InterruptedException, KeeperException
  {
    Collection> events = new ArrayList>();

    synchronized(_lock)
    {
      _tree = trackNode(_root, new LinkedHashMap>(), events, 0);
    }

    raiseEvents(events);
  }

  public void registerListener(NodeEventsListener eventsListener)
  {
    synchronized(_lock)
    {
      _eventsListeners.add(eventsListener);
    }
  }

  public void registerErrorListener(ErrorListener errorListener)
  {
    synchronized(_lock)
    {
      _errorListeners.add(errorListener);
    }
  }

  /**
   * Must be called from a synchronized section
   */
  private Map> trackNode(String path,
                                                Map> tree,
                                                Collection> events,
                                                int depth)
    throws InterruptedException, KeeperException
  {
    if(depth > _depth)
    {
      if(log.isDebugEnabled())
        log.debug(logString(path, "max depth reached ${depth}"));

      return tree;
    }

    TrackedNode oldTrackedNode = tree.get(path);

    try
    {
      ZKData res = _zkDataReader.readData(_zk, path, _treeWacher);

      TrackedNode newTrackedNode =
        new TrackedNode(path, res.getData(), res.getStat(), depth);
      if(oldTrackedNode != null)
      {
        if(!_zkDataReader.isEqual(oldTrackedNode.getData(), newTrackedNode.getData()))
        {
          events.add(new NodeEvent(NodeEventType.UPDATED,
                                      newTrackedNode));
        }
      }
      else
      {
        events.add(new NodeEvent(NodeEventType.ADDED,
                                    newTrackedNode));
      }

      tree.put(path, newTrackedNode);

      if(depth < _depth)
      {
        ZKChildren children = _zk.getZKChildren(path, _treeWacher);
        // the stat may change between the 2 calls
        if(!newTrackedNode.getStat().equals(children.getStat()))
          newTrackedNode.setStat(children.getStat());
        Collections.sort(children.getChildren());
        for(String child : children.getChildren())
        {
          String childPath = PathUtils.addPaths(path, child);
          if(!tree.containsKey(childPath))
            trackNode(childPath, tree, events, depth + 1);
        }
      }

      _lastZkTxId = Math.max(_lastZkTxId, newTrackedNode.getZkTxId());
      _lock.notifyAll();
      if(log.isDebugEnabled())
        log.debug(logString(path,
                            "start tracking " + (depth < _depth ? "": "leaf ") +
                            "node zkTxId=" + newTrackedNode.getZkTxId()));

    }
    catch (KeeperException.NoNodeException e)
    {
      // this is a race condition which could happen between the moment the event is received
      // and this call
      if(log.isDebugEnabled())
        log.debug(logString(path, "no such node"));
      // it means that the node has disappeared

      tree.remove(path);

      if(oldTrackedNode != null)
      {
        events.add(new NodeEvent(NodeEventType.DELETED, 
                                    oldTrackedNode));
      }
    }

    return tree;
  }

  private Map> handleNodeDeleted(String path,
                                                        Collection> events)
    throws InterruptedException, KeeperException
  {
    Map> tree = _tree;
    if(_tree.containsKey(path))
    {
      tree = new LinkedHashMap>(_tree);
      TrackedNode trackedNode = tree.remove(path);
      events.add(new NodeEvent(NodeEventType.DELETED,
                                  trackedNode));
      if(log.isDebugEnabled())
        log.debug(logString(path, "stop tracking node"));

      // after a delete event, we try to track the node again as a delete/add event could happen
      // and be undetected otherwise!
      trackNode(path, tree, events, trackedNode.getDepth());
    }

    return tree;
  }

  private Map> handleNodeDataChanged(String path,
                                                            Collection> events)
    throws InterruptedException, KeeperException
  {
    return trackNode(path,
                     new LinkedHashMap>(_tree),
                     events,
                     computeDepth(path));
  }

  private Map> handleNodeChildrenChanged(String path,
                                                                Collection> events)
    throws InterruptedException, KeeperException
  {
    return trackNode(path,
                     new LinkedHashMap>(_tree),
                     events,
                     computeDepth(path));
  }

  private int computeDepth(String path)
  {
    return computeAbsoluteDepth(path) - _rootDepth;
  }

  private void raiseEvents(Collection> events)
  {
    if(!events.isEmpty())
    {
      Set> listeners;
      synchronized(_lock)
      {
        listeners = new LinkedHashSet>(_eventsListeners);
      }

      for(NodeEventsListener listener : listeners)
      {
        listener.onEvents(events);
      }
    }
  }

  private boolean handleEvent(WatchedEvent event)
  {
    if(_destroyed)
    {
      return false;
    }

    switch(event.getState())
    {
      case SyncConnected:
        return true;

      case Disconnected:
        return false;

      case Expired:
        return false;

      default:
        return false;
    }
  }

  private String logString(String path, String msg)
  {
    StringBuilder sb = new StringBuilder();
    sb.append("[").append(path).append("] ");
    sb.append("[").append(LangUtils.shortIdentityString(this)).append("] ");
    sb.append("[").append(Thread.currentThread()).append("] ");
    sb.append(msg);
    return sb.toString();
  }

  private void raiseError(WatchedEvent event, Throwable th)
  {
    Set listeners;
    synchronized(_lock)
    {
      listeners = new LinkedHashSet(_errorListeners);
    }

    if(!listeners.isEmpty())
    {
      for(ErrorListener listener : listeners)
      {
        try
        {
          if(log.isDebugEnabled())
            log.debug(logString(event.getPath(), "Raising error to " +
                                                 LangUtils.identityString(listener)),
                      th);

          listener.onError(event, th);
        }
        catch(Throwable th2)
        {
          log.warn(logString(event.getPath(), "Error in watcher while executing listener " +
                                              LangUtils.identityString(listener) +
                                              " (ignored)"),
                   th2);
        }
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy