org.apache.accumulo.fate.zookeeper.ZooCache.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.accumulo.fate.zookeeper.ZooCache.java

Source

/*
 * 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.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
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;

/**
 * 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<String, byte[]> cache;
    private final HashMap<String, Stat> statCache;
    private final HashMap<String, List<String>> childrenCache;

    private final ZooReader zReader;

    /**
     * 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<T> {
        /**
         * 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 synchronized List<String> getChildren(final String zPath) {

        ZooRunnable<List<String>> zr = new ZooRunnable<List<String>>() {

            @Override
            public List<String> run() throws KeeperException, InterruptedException {
                try {
                    cacheReadLock.lock();
                    if (childrenCache.containsKey(zPath)) {
                        return childrenCache.get(zPath);
                    }
                } finally {
                    cacheReadLock.unlock();
                }

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

                    final ZooKeeper zooKeeper = getZooKeeper();

                    List<String> children = zooKeeper.getChildren(zPath, watcher);
                    childrenCache.put(zPath, children);
                    return children;
                } catch (KeeperException ke) {
                    if (ke.code() != Code.NONODE) {
                        throw ke;
                    }
                } finally {
                    cacheWriteLock.unlock();
                }
                return null;
            }

        };

        List<String> children = zr.retry();

        if (children == null) {
            return null;
        }
        return Collections.unmodifiableList(children);
    }

    /**
     * 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 <code>Stat</code> 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 Stat status) {
        ZooRunnable<byte[]> zr = new ZooRunnable<byte[]>() {

            @Override
            public byte[] run() throws KeeperException, InterruptedException {
                Stat stat = null;
                cacheReadLock.lock();
                try {
                    if (cache.containsKey(zPath)) {
                        stat = statCache.get(zPath);
                        copyStats(status, stat);
                        return cache.get(zPath);
                    }
                } finally {
                    cacheReadLock.unlock();
                }

                /*
                 * 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 = 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);
                        } 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, stat);
                    copyStats(status, stat);
                    statCache.put(zPath, stat);
                    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(Stat userStat, Stat cachedStat) {
        if (userStat != null && cachedStat != null) {
            try {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                DataOutputStream dos = new DataOutputStream(baos);
                cachedStat.write(dos);
                dos.close();

                ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
                DataInputStream dis = new DataInputStream(bais);
                userStat.readFields(dis);

                dis.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

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

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

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

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

    /**
     * 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 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<String> i = cache.keySet().iterator(); i.hasNext();) {
                String path = i.next();
                if (path.startsWith(zPath))
                    i.remove();
            }

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

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

}