com.netflix.curator.framework.recipes.cache.PathChildrenCache.java Source code

Java tutorial

Introduction

Here is the source code for com.netflix.curator.framework.recipes.cache.PathChildrenCache.java

Source

/*
 * Copyright 2012 Netflix, 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 com.netflix.curator.framework.recipes.cache;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.netflix.curator.framework.CuratorFramework;
import com.netflix.curator.framework.api.BackgroundCallback;
import com.netflix.curator.framework.api.CuratorEvent;
import com.netflix.curator.framework.listen.ListenerContainer;
import com.netflix.curator.framework.state.ConnectionState;
import com.netflix.curator.framework.state.ConnectionStateListener;
import com.netflix.curator.utils.EnsurePath;
import com.netflix.curator.utils.ThreadUtils;
import com.netflix.curator.utils.ZKPaths;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Exchanger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;

/**
 * <p>A utility that attempts to keep all data from all children of a ZK path locally cached. This class
 * will watch the ZK path, respond to update/create/delete events, pull down the data, etc. You can
 * register a listener that will get notified when changes occur.</p>
 *
 * <p><b>IMPORTANT</b> - it's not possible to stay transactionally in sync. Users of this class must
 * be prepared for false-positives and false-negatives. Additionally, always use the version number
 * when updating data to avoid overwriting another process' change.</p>
 */
public class PathChildrenCache implements Closeable {
    private final Logger log = LoggerFactory.getLogger(getClass());
    private final CuratorFramework client;
    private final String path;
    private final ExecutorService executorService;
    private final boolean cacheData;
    private final boolean dataIsCompressed;
    private final EnsurePath ensurePath;

    private final Watcher childrenWatcher = new Watcher() {
        @Override
        public void process(WatchedEvent event) {
            try {
                refresh(false);
            } catch (Exception e) {
                handleException(e);
            }
        }
    };

    private final Watcher dataWatcher = new Watcher() {
        @Override
        public void process(WatchedEvent event) {
            try {
                if (event.getType() == Event.EventType.NodeDeleted) {
                    remove(event.getPath());
                } else if (event.getType() == Event.EventType.NodeDataChanged) {
                    getDataAndStat(event.getPath());
                }
            } catch (Exception e) {
                handleException(e);
            }
        }
    };

    private final BlockingQueue<PathChildrenCacheEvent> listenerEvents = new LinkedBlockingQueue<PathChildrenCacheEvent>();
    private final ListenerContainer<PathChildrenCacheListener> listeners = new ListenerContainer<PathChildrenCacheListener>();
    private final ConcurrentMap<String, ChildData> currentData = Maps.newConcurrentMap();

    @VisibleForTesting
    volatile Exchanger<Object> rebuildTestExchanger;

    private final ConnectionStateListener connectionStateListener = new ConnectionStateListener() {
        @Override
        public void stateChanged(CuratorFramework client, ConnectionState newState) {
            handleStateChange(newState);
        }
    };
    private static final ThreadFactory defaultThreadFactory = ThreadUtils.newThreadFactory("PathChildrenCache");

    /**
     * @param client the client
     * @param path path to watch
     * @param mode caching mode
     *
     * @deprecated use {@link #PathChildrenCache(CuratorFramework, String, boolean)} instead
     */
    @SuppressWarnings("deprecation")
    public PathChildrenCache(CuratorFramework client, String path, PathChildrenCacheMode mode) {
        this(client, path, mode != PathChildrenCacheMode.CACHE_PATHS_ONLY, false, defaultThreadFactory);
    }

    /**
     * @param client the client
     * @param path path to watch
     * @param mode caching mode
     * @param threadFactory factory to use when creating internal threads
     *
     * @deprecated use {@link #PathChildrenCache(CuratorFramework, String, boolean, ThreadFactory)} instead
     */
    @SuppressWarnings("deprecation")
    public PathChildrenCache(CuratorFramework client, String path, PathChildrenCacheMode mode,
            ThreadFactory threadFactory) {
        this(client, path, mode != PathChildrenCacheMode.CACHE_PATHS_ONLY, false, threadFactory);
    }

    /**
     * @param client the client
     * @param path path to watch
     * @param cacheData if true, node contents are cached in addition to the stat
     */
    public PathChildrenCache(CuratorFramework client, String path, boolean cacheData) {
        this(client, path, cacheData, false, defaultThreadFactory);
    }

    /**
     * @param client the client
     * @param path path to watch
     * @param cacheData if true, node contents are cached in addition to the stat
     * @param threadFactory factory to use when creating internal threads
     */
    public PathChildrenCache(CuratorFramework client, String path, boolean cacheData, ThreadFactory threadFactory) {
        this(client, path, cacheData, false, threadFactory);
    }

    /**
     * @param client the client
     * @param path path to watch
     * @param cacheData if true, node contents are cached in addition to the stat
     * @param dataIsCompressed if true, data in the path is compressed
     * @param threadFactory factory to use when creating internal threads
     */
    public PathChildrenCache(CuratorFramework client, String path, boolean cacheData, boolean dataIsCompressed,
            ThreadFactory threadFactory) {
        this.client = client;
        this.path = path;
        this.cacheData = cacheData;
        this.dataIsCompressed = dataIsCompressed;
        executorService = Executors.newFixedThreadPool(1, threadFactory);
        ensurePath = client.newNamespaceAwareEnsurePath(path);
    }

    /**
     * Start the cache. The cache is not started automatically. You must call this method.
     *
     * @throws Exception errors
     */
    public void start() throws Exception {
        start(false);
    }

    /**
     * Same as {@link #start()} but gives the option of doing an initial build
     *
     * @param buildInitial if true, {@link #rebuild()} will be called before this method
     *                     returns in order to get an initial view of the node
     * @throws Exception errors
     */
    public void start(boolean buildInitial) throws Exception {
        Preconditions.checkState(!executorService.isShutdown(), "already started");

        client.getConnectionStateListenable().addListener(connectionStateListener);
        executorService.submit(new Callable<Object>() {
            @Override
            public Object call() throws Exception {
                listenerLoop();
                return null;
            }
        });

        if (buildInitial) {
            rebuild();
        } else {
            refresh(false);
        }
    }

    /**
     * NOTE: this is a BLOCKING method. Completely rebuild the internal cache by querying
     * for all needed data WITHOUT generating any events to send to listeners.
     *
     * @throws Exception errors
     */
    public void rebuild() throws Exception {
        Preconditions.checkState(!executorService.isShutdown(), "cache has been closed");

        ensurePath.ensure(client.getZookeeperClient());

        List<String> children = client.getChildren().forPath(path);
        for (String child : children) {
            String fullPath = ZKPaths.makePath(path, child);
            if (cacheData) {
                try {
                    Stat stat = new Stat();
                    byte[] bytes = dataIsCompressed
                            ? client.getData().decompressed().storingStatIn(stat).forPath(fullPath)
                            : client.getData().storingStatIn(stat).forPath(fullPath);
                    currentData.put(fullPath, new ChildData(fullPath, stat, bytes));
                } catch (KeeperException.NoNodeException ignore) {
                    // ignore
                }
            } else {
                Stat stat = client.checkExists().forPath(fullPath);
                if (stat != null) {
                    currentData.put(fullPath, new ChildData(fullPath, stat, null));
                }
            }

            if (rebuildTestExchanger != null) {
                rebuildTestExchanger.exchange(new Object());
            }
        }

        // this is necessary so that any updates that occurred while rebuilding are taken
        refresh(true);
    }

    /**
     * Close/end the cache
     *
     * @throws IOException errors
     */
    @Override
    public void close() throws IOException {
        Preconditions.checkState(!executorService.isShutdown(), "has not been started");

        client.getConnectionStateListenable().removeListener(connectionStateListener);
        executorService.shutdownNow();
    }

    /**
     * Return the cache listenable
     *
     * @return listenable
     */
    public ListenerContainer<PathChildrenCacheListener> getListenable() {
        return listeners;
    }

    /**
     * Return the current data. There are no guarantees of accuracy. This is
     * merely the most recent view of the data. The data is returned in sorted order.
     *
     * @return list of children and data
     */
    public List<ChildData> getCurrentData() {
        return ImmutableList.copyOf(Sets.<ChildData>newTreeSet(currentData.values()));
    }

    /**
     * Return the current data for the given path. There are no guarantees of accuracy. This is
     * merely the most recent view of the data. If there is no child with that path, <code>null</code>
     * is returned.
     *
     * @param fullPath full path to the node to check
     * @return data or null
     */
    public ChildData getCurrentData(String fullPath) {
        return currentData.get(fullPath);
    }

    /**
     * Clear out current data and begin a new query on the path
     *
     * @throws Exception errors
     */
    public void clearAndRefresh() throws Exception {
        currentData.clear();
        refresh(false);
    }

    /**
     * Clears the current data without beginning a new query and without generating any events
     * for listeners.
     */
    public void clear() {
        currentData.clear();
    }

    /**
     * Default behavior is just to log the exception
     *
     * @param e the exception
     */
    protected void handleException(Throwable e) {
        log.error("", e);
    }

    private void handleStateChange(ConnectionState newState) {
        switch (newState) {
        case SUSPENDED: {
            listenerEvents
                    .offer(new PathChildrenCacheEvent(PathChildrenCacheEvent.Type.CONNECTION_SUSPENDED, null));
            break;
        }

        case LOST: {
            listenerEvents.offer(new PathChildrenCacheEvent(PathChildrenCacheEvent.Type.CONNECTION_LOST, null));
            break;
        }

        case RECONNECTED: {
            try {
                refresh(true);
                listenerEvents.offer(
                        new PathChildrenCacheEvent(PathChildrenCacheEvent.Type.CONNECTION_RECONNECTED, null));
            } catch (Exception e) {
                handleException(e);
            }
            break;
        }
        }
    }

    private void refresh(final boolean forceGetDataAndStat) throws Exception {
        ensurePath.ensure(client.getZookeeperClient());

        final BackgroundCallback callback = new BackgroundCallback() {
            @Override
            public void processResult(CuratorFramework client, CuratorEvent event) throws Exception {
                processChildren(event.getChildren(), forceGetDataAndStat);
            }
        };

        client.getChildren().usingWatcher(childrenWatcher).inBackground(callback).forPath(path);
    }

    private void processChildren(List<String> children, boolean forceGetDataAndStat) throws Exception {
        List<String> fullPaths = Lists.transform(children, new Function<String, String>() {
            @Override
            public String apply(String child) {
                return ZKPaths.makePath(path, child);
            }
        });
        Set<String> removedNodes = Sets.newHashSet(currentData.keySet());
        removedNodes.removeAll(fullPaths);

        for (String fullPath : removedNodes) {
            remove(fullPath);
        }

        for (String name : children) {
            String fullPath = ZKPaths.makePath(path, name);
            if (forceGetDataAndStat || !currentData.containsKey(fullPath)) {
                getDataAndStat(fullPath);
            }
        }
    }

    private void remove(String fullPath) {
        ChildData data = currentData.remove(fullPath);
        if (data != null) {
            listenerEvents.offer(new PathChildrenCacheEvent(PathChildrenCacheEvent.Type.CHILD_REMOVED, data));
        }
    }

    private void applyNewData(String fullPath, int resultCode, Stat stat, byte[] bytes) {
        if (resultCode == KeeperException.Code.OK.intValue()) // otherwise - node must have dropped or something - we should be getting another event
        {
            ChildData data = new ChildData(fullPath, stat, bytes);
            ChildData previousData = currentData.put(fullPath, data);
            if (previousData == null) // i.e. new
            {
                listenerEvents.offer(new PathChildrenCacheEvent(PathChildrenCacheEvent.Type.CHILD_ADDED, data));
            } else if (previousData.getStat().getVersion() != stat.getVersion()) {
                listenerEvents.offer(new PathChildrenCacheEvent(PathChildrenCacheEvent.Type.CHILD_UPDATED, data));
            }
        }
    }

    private void getDataAndStat(final String fullPath) throws Exception {
        BackgroundCallback existsCallback = new BackgroundCallback() {
            @Override
            public void processResult(CuratorFramework client, CuratorEvent event) throws Exception {
                applyNewData(fullPath, event.getResultCode(), event.getStat(), null);
            }
        };

        BackgroundCallback getDataCallback = new BackgroundCallback() {
            @Override
            public void processResult(CuratorFramework client, CuratorEvent event) throws Exception {
                applyNewData(fullPath, event.getResultCode(), event.getStat(), event.getData());
            }
        };

        if (cacheData) {
            if (dataIsCompressed) {
                client.getData().decompressed().usingWatcher(dataWatcher).inBackground(getDataCallback)
                        .forPath(fullPath);
            } else {
                client.getData().usingWatcher(dataWatcher).inBackground(getDataCallback).forPath(fullPath);
            }
        } else {
            client.checkExists().usingWatcher(dataWatcher).inBackground(existsCallback).forPath(fullPath);
        }
    }

    private void listenerLoop() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                PathChildrenCacheEvent event = listenerEvents.take();
                callListeners(event);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }

    private void callListeners(final PathChildrenCacheEvent event) {
        listeners.forEach(new Function<PathChildrenCacheListener, Void>() {
            @Override
            public Void apply(PathChildrenCacheListener listener) {
                try {
                    listener.childEvent(client, event);
                } catch (Exception e) {
                    handleException(e);
                }
                return null;
            }
        });
    }
}