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

org.apache.accumulo.fate.zookeeper.ZooCache Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.accumulo.fate.zookeeper;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.KeeperException.Code;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;

/**
 * A cache for values stored in ZooKeeper. Values are kept up to date as they change.
 */
public class ZooCache {
  private static final Logger log = LoggerFactory.getLogger(ZooCache.class);

  private final ZCacheWatcher watcher = new ZCacheWatcher();
  private final Watcher externalWatcher;

  private final ReadWriteLock cacheLock = new ReentrantReadWriteLock(false);
  private final Lock cacheWriteLock = cacheLock.writeLock();
  private final Lock cacheReadLock = cacheLock.readLock();

  private final HashMap cache;
  private final HashMap statCache;
  private final HashMap> childrenCache;

  private final ZooReader zReader;

  public static class ZcStat {
    private long ephemeralOwner;

    public ZcStat() {

    }

    private ZcStat(Stat stat) {
      this.ephemeralOwner = stat.getEphemeralOwner();
    }

    public long getEphemeralOwner() {
      return ephemeralOwner;
    }

    private void set(ZcStat cachedStat) {
      this.ephemeralOwner = cachedStat.ephemeralOwner;
    }

    @VisibleForTesting
    public void setEphemeralOwner(long ephemeralOwner) {
      this.ephemeralOwner = ephemeralOwner;
    }
  }

  private static class ImmutableCacheCopies {
    final Map cache;
    final Map statCache;
    final Map> childrenCache;
    final long updateCount;

    ImmutableCacheCopies(long updateCount) {
      this.updateCount = updateCount;
      cache = Collections.emptyMap();
      statCache = Collections.emptyMap();
      childrenCache = Collections.emptyMap();
    }

    ImmutableCacheCopies(long updateCount, Map cache, Map statCache,
        Map> childrenCache) {
      this.updateCount = updateCount;
      this.cache = Collections.unmodifiableMap(new HashMap<>(cache));
      this.statCache = Collections.unmodifiableMap(new HashMap<>(statCache));
      this.childrenCache = Collections.unmodifiableMap(new HashMap<>(childrenCache));
    }

    ImmutableCacheCopies(long updateCount, ImmutableCacheCopies prev,
        Map> childrenCache) {
      this.updateCount = updateCount;
      this.cache = prev.cache;
      this.statCache = prev.statCache;
      this.childrenCache = Collections.unmodifiableMap(new HashMap<>(childrenCache));
    }

    ImmutableCacheCopies(long updateCount, Map cache, Map statCache,
        ImmutableCacheCopies prev) {
      this.updateCount = updateCount;
      this.cache = Collections.unmodifiableMap(new HashMap<>(cache));
      this.statCache = Collections.unmodifiableMap(new HashMap<>(statCache));
      this.childrenCache = prev.childrenCache;
    }
  }

  private volatile ImmutableCacheCopies immutableCache = new ImmutableCacheCopies(0);
  private long updateCount = 0;

  /**
   * Returns a ZooKeeper session. Calls should be made within run of ZooRunnable after caches are
   * checked. This will be performed at each retry of the run method. Calls to
   * {@link #getZooKeeper()} should be made, ideally, after cache checks since other threads may
   * have succeeded when updating the cache. Doing this will ensure that we don't pay the cost of
   * retrieving a ZooKeeper session on each retry until we've ensured the caches aren't populated
   * for a given node.
   *
   * @return ZooKeeper session.
   */
  private ZooKeeper getZooKeeper() {
    return zReader.getZooKeeper();
  }

  private class ZCacheWatcher implements Watcher {
    @Override
    public void process(WatchedEvent event) {

      if (log.isTraceEnabled()) {
        log.trace("{}", event);
      }

      switch (event.getType()) {
        case NodeDataChanged:
        case NodeChildrenChanged:
        case NodeCreated:
        case NodeDeleted:
          remove(event.getPath());
          break;
        case None:
          switch (event.getState()) {
            case Disconnected:
              if (log.isTraceEnabled())
                log.trace("Zoo keeper connection disconnected, clearing cache");
              clear();
              break;
            case SyncConnected:
              break;
            case Expired:
              if (log.isTraceEnabled())
                log.trace("Zoo keeper connection expired, clearing cache");
              clear();
              break;
            default:
              log.warn("Unhandled: " + event);
              break;
          }
          break;
        default:
          log.warn("Unhandled: " + event);
          break;
      }

      if (externalWatcher != null) {
        externalWatcher.process(event);
      }
    }
  }

  /**
   * Creates a new cache.
   *
   * @param zooKeepers
   *          comma-separated list of ZooKeeper host[:port]s
   * @param sessionTimeout
   *          ZooKeeper session timeout
   */
  public ZooCache(String zooKeepers, int sessionTimeout) {
    this(zooKeepers, sessionTimeout, null);
  }

  /**
   * Creates a new cache. The given watcher is called whenever a watched node changes.
   *
   * @param zooKeepers
   *          comma-separated list of ZooKeeper host[:port]s
   * @param sessionTimeout
   *          ZooKeeper session timeout
   * @param watcher
   *          watcher object
   */
  public ZooCache(String zooKeepers, int sessionTimeout, Watcher watcher) {
    this(new ZooReader(zooKeepers, sessionTimeout), watcher);
  }

  /**
   * Creates a new cache. The given watcher is called whenever a watched node changes.
   *
   * @param reader
   *          ZooKeeper reader
   * @param watcher
   *          watcher object
   */
  public ZooCache(ZooReader reader, Watcher watcher) {
    this.zReader = reader;
    this.cache = new HashMap<>();
    this.statCache = new HashMap<>();
    this.childrenCache = new HashMap<>();
    this.externalWatcher = watcher;
  }

  private abstract class ZooRunnable {
    /**
     * Runs an operation against ZooKeeper. Retries are performed by the retry method when
     * KeeperExceptions occur.
     *
     * Changes were made in ACCUMULO-4388 so that the run method no longer accepts Zookeeper as an
     * argument, and instead relies on the ZooRunnable implementation to call
     * {@link #getZooKeeper()}. Performing the call to retrieving a ZooKeeper Session after caches
     * are checked has the benefit of limiting ZK connections and blocking as a result of obtaining
     * these sessions.
     *
     * @return T the result of the runnable
     */
    abstract T run() throws KeeperException, InterruptedException;

    /**
     * Retry will attempt to call the run method. Run should make a call to {@link #getZooKeeper()}
     * after checks to cached information are made. This change, per ACCUMULO-4388 ensures that we
     * don't create a ZooKeeper session when information is cached, and access to ZooKeeper is
     * unnecessary.
     *
     * @return result of the runnable access success ( i.e. no exceptions ).
     */
    public T retry() {

      int sleepTime = 100;

      while (true) {

        try {
          return run();
        } catch (KeeperException e) {
          final Code code = e.code();
          if (code == Code.NONODE) {
            log.error("Looked up non-existent node in cache " + e.getPath(), e);
          } else if (code == Code.CONNECTIONLOSS || code == Code.OPERATIONTIMEOUT
              || code == Code.SESSIONEXPIRED) {
            log.warn("Saw (possibly) transient exception communicating with ZooKeeper, will retry",
                e);
          } else {
            log.warn("Zookeeper error, will retry", e);
          }
        } catch (InterruptedException e) {
          log.info("Zookeeper error, will retry", e);
        } catch (ConcurrentModificationException e) {
          log.debug("Zookeeper was modified, will retry");
        }

        try {
          // do not hold lock while sleeping
          Thread.sleep(sleepTime);
        } catch (InterruptedException e) {
          log.debug("Wait in retry() was interrupted.", e);
        }
        LockSupport.parkNanos(sleepTime);
        if (sleepTime < 10_000) {
          sleepTime = (int) (sleepTime + sleepTime * Math.random());
        }
      }
    }

  }

  /**
   * Gets the children of the given node. A watch is established by this call.
   *
   * @param zPath
   *          path of node
   * @return children list, or null if node has no children or does not exist
   */
  public List getChildren(final String zPath) {

    ZooRunnable> zr = new ZooRunnable>() {

      @Override
      public List run() throws KeeperException, InterruptedException {

        // only read volatile once for consistency
        ImmutableCacheCopies lic = immutableCache;
        if (lic.childrenCache.containsKey(zPath)) {
          return lic.childrenCache.get(zPath);
        }

        cacheWriteLock.lock();
        try {
          if (childrenCache.containsKey(zPath)) {
            return childrenCache.get(zPath);
          }

          final ZooKeeper zooKeeper = getZooKeeper();

          List children = zooKeeper.getChildren(zPath, watcher);
          if (children != null) {
            children = ImmutableList.copyOf(children);
          }
          childrenCache.put(zPath, children);
          immutableCache = new ImmutableCacheCopies(++updateCount, immutableCache, childrenCache);
          return children;
        } catch (KeeperException ke) {
          if (ke.code() != Code.NONODE) {
            throw ke;
          }
        } finally {
          cacheWriteLock.unlock();
        }
        return null;
      }

    };

    return zr.retry();
  }

  /**
   * Gets data at the given path. Status information is not returned. A watch is established by this
   * call.
   *
   * @param zPath
   *          path to get
   * @return path data, or null if non-existent
   */
  public byte[] get(final String zPath) {
    return get(zPath, null);
  }

  /**
   * Gets data at the given path, filling status information into the given Stat
   * object. A watch is established by this call.
   *
   * @param zPath
   *          path to get
   * @param status
   *          status object to populate
   * @return path data, or null if non-existent
   */
  public byte[] get(final String zPath, final ZcStat status) {
    ZooRunnable zr = new ZooRunnable() {

      @Override
      public byte[] run() throws KeeperException, InterruptedException {
        ZcStat zstat = null;

        // only read volatile once so following code works with a consistent snapshot
        ImmutableCacheCopies lic = immutableCache;
        byte[] val = lic.cache.get(zPath);
        if (val != null || lic.cache.containsKey(zPath)) {
          if (status != null) {
            zstat = lic.statCache.get(zPath);
            copyStats(status, zstat);
          }
          return val;
        }

        /*
         * The following call to exists() is important, since we are caching that a node does not
         * exist. Once the node comes into existence, it will be added to the cache. But this
         * notification of a node coming into existence will only be given if exists() was
         * previously called. If the call to exists() is bypassed and only getData() is called with
         * a special case that looks for Code.NONODE in the KeeperException, then non-existence can
         * not be cached.
         */
        cacheWriteLock.lock();
        try {
          final ZooKeeper zooKeeper = getZooKeeper();
          Stat stat = zooKeeper.exists(zPath, watcher);
          byte[] data = null;
          if (stat == null) {
            if (log.isTraceEnabled()) {
              log.trace("zookeeper did not contain " + zPath);
            }
          } else {
            try {
              data = zooKeeper.getData(zPath, watcher, stat);
              zstat = new ZcStat(stat);
            } catch (KeeperException.BadVersionException e1) {
              throw new ConcurrentModificationException();
            } catch (KeeperException.NoNodeException e2) {
              throw new ConcurrentModificationException();
            }
            if (log.isTraceEnabled()) {
              log.trace("zookeeper contained " + zPath + " "
                  + (data == null ? null : new String(data, UTF_8)));
            }
          }
          put(zPath, data, zstat);
          copyStats(status, zstat);
          return data;
        } finally {
          cacheWriteLock.unlock();
        }
      }
    };

    return zr.retry();
  }

  /**
   * Helper method to copy stats from the cached stat into userStat
   *
   * @param userStat
   *          user Stat object
   * @param cachedStat
   *          cached statistic, that is or will be cached
   */
  protected void copyStats(ZcStat userStat, ZcStat cachedStat) {
    if (userStat != null && cachedStat != null) {
      userStat.set(cachedStat);
    }
  }

  private void put(String zPath, byte[] data, ZcStat stat) {
    cacheWriteLock.lock();
    try {
      cache.put(zPath, data);
      statCache.put(zPath, stat);

      immutableCache = new ImmutableCacheCopies(++updateCount, cache, statCache, immutableCache);
    } finally {
      cacheWriteLock.unlock();
    }
  }

  private void remove(String zPath) {
    cacheWriteLock.lock();
    try {
      cache.remove(zPath);
      childrenCache.remove(zPath);
      statCache.remove(zPath);

      immutableCache = new ImmutableCacheCopies(++updateCount, cache, statCache, childrenCache);
    } finally {
      cacheWriteLock.unlock();
    }
  }

  /**
   * Clears this cache.
   */
  public void clear() {
    cacheWriteLock.lock();
    try {
      cache.clear();
      childrenCache.clear();
      statCache.clear();

      immutableCache = new ImmutableCacheCopies(++updateCount);
    } finally {
      cacheWriteLock.unlock();
    }
  }

  /**
   * Returns a monotonically increasing count of the number of time the cache was updated. If the
   * count is the same, then it means cache did not change.
   */
  public long getUpdateCount() {
    return immutableCache.updateCount;
  }

  /**
   * Checks if a data value (or lack of one) is cached.
   *
   * @param zPath
   *          path of node
   * @return true if data value is cached
   */
  @VisibleForTesting
  boolean dataCached(String zPath) {
    cacheReadLock.lock();
    try {
      return immutableCache.cache.containsKey(zPath) && cache.containsKey(zPath);
    } finally {
      cacheReadLock.unlock();
    }

  }

  /**
   * Checks if children of a node (or lack of them) are cached.
   *
   * @param zPath
   *          path of node
   * @return true if children are cached
   */
  @VisibleForTesting
  boolean childrenCached(String zPath) {
    cacheReadLock.lock();
    try {
      return immutableCache.childrenCache.containsKey(zPath) && childrenCache.containsKey(zPath);
    } finally {
      cacheReadLock.unlock();
    }
  }

  /**
   * Clears this cache of all information about nodes rooted at the given path.
   *
   * @param zPath
   *          path of top node
   */
  public void clear(String zPath) {
    cacheWriteLock.lock();
    try {
      for (Iterator i = cache.keySet().iterator(); i.hasNext();) {
        String path = i.next();
        if (path.startsWith(zPath))
          i.remove();
      }

      for (Iterator i = childrenCache.keySet().iterator(); i.hasNext();) {
        String path = i.next();
        if (path.startsWith(zPath))
          i.remove();
      }

      for (Iterator i = statCache.keySet().iterator(); i.hasNext();) {
        String path = i.next();
        if (path.startsWith(zPath))
          i.remove();
      }

      immutableCache = new ImmutableCacheCopies(++updateCount, cache, statCache, childrenCache);
    } finally {
      cacheWriteLock.unlock();
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy