dmg.cells.zookeeper.PathChildrenCache.java Source code

Java tutorial

Introduction

Here is the source code for dmg.cells.zookeeper.PathChildrenCache.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.
 *
 * CHANGES:
 *
 * - Modified by Gerd Behrmann, May 2016.
 */

package dmg.cells.zookeeper;

import com.google.common.base.Preconditions;
import com.google.common.collect.Ordering;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.api.BackgroundCallback;
import org.apache.curator.framework.listen.ListenerContainer;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.curator.framework.state.ConnectionState;
import org.apache.curator.framework.state.ConnectionStateListener;
import org.apache.curator.utils.PathUtils;
import org.apache.curator.utils.ThreadUtils;
import org.apache.curator.utils.ZKPaths;
import org.apache.zookeeper.KeeperException;
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.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;

/**
 * <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></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>
 * <p>Similar to the Curator recipe of the same name, but simpler and with fewer features. Works around
 * issues in Curator 2.10 (see CURATOR-326 and CURATOR-328 in Curator JIRA). We should probably move
 * back to the upstream version once a fix is released and maybe work with upstream to improve the
 * original recipe further (e.g. non-blocking path creation). </p>
 */
public class PathChildrenCache implements Closeable {
    private final Logger log = LoggerFactory.getLogger(getClass());

    private final CuratorFramework client;
    private final String path;
    private final boolean cacheData;
    private final boolean dataIsCompressed;
    private final ListenerContainer<PathChildrenCacheListener> listeners = new ListenerContainer<>();
    private final ConcurrentMap<String, ChildData> currentData = new ConcurrentHashMap<>();
    private final AtomicReference<State> state = new AtomicReference<>(State.LATENT);

    private final Watcher childrenWatcher = event -> {
        try {
            refresh(RefreshMode.NORMAL);
        } catch (Exception e) {
            handleException(e);
        }
    };

    private final Watcher dataWatcher = event -> {
        try {
            if (event.getType() == Watcher.Event.EventType.NodeDeleted) {
                remove(event.getPath());
            } else if (event.getType() == Watcher.Event.EventType.NodeDataChanged) {
                getDataAndStat(event.getPath());
            } else {
                log.debug("Data watcher ignored {}", event);
            }
        } catch (Exception e) {
            handleException(e);
        }
    };

    private final ConnectionStateListener connectionStateListener = (client,
            newState) -> handleStateChange(newState);

    private enum State {
        LATENT, STARTED, CLOSED
    }

    private enum RefreshMode {
        NORMAL, REBUILD
    }

    /**
     * @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);
    }

    /**
     * @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
     */
    public PathChildrenCache(CuratorFramework client, String path, boolean cacheData, boolean dataIsCompressed) {
        this.client = client;
        this.path = PathUtils.validatePath(path);
        this.cacheData = cacheData;
        this.dataIsCompressed = dataIsCompressed;
    }

    /**
     * Start the cache. The cache is not started automatically. You must call this method.
     *
     * @throws Exception errors
     */
    public void start() throws Exception {
        Preconditions.checkState(state.compareAndSet(State.LATENT, State.STARTED), "already started");

        client.getConnectionStateListenable().addListener(connectionStateListener);

        refresh(RefreshMode.NORMAL);
    }

    /**
     * Close/end the cache
     *
     * @throws IOException errors
     */
    @Override
    public void close() throws IOException {
        if (state.compareAndSet(State.STARTED, State.CLOSED)) {
            client.getConnectionStateListenable().removeListener(connectionStateListener);
            listeners.clear();
            client.clearWatcherReferences(childrenWatcher);
            client.clearWatcherReferences(dataWatcher);
        }
    }

    /**
     * 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 Ordering.natural().immutableSortedCopy(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);
    }

    void refresh(RefreshMode mode) throws Exception {
        final BackgroundCallback callback = (client, event) -> {
            if (PathChildrenCache.this.state.get() != State.CLOSED) {
                if (event.getResultCode() == KeeperException.Code.OK.intValue()) {
                    processChildren(event.getChildren(), mode);
                } else if (event.getResultCode() == KeeperException.Code.NONODE.intValue()) {
                    ensurePathAndThenRefresh(mode);
                } else if (event.getResultCode() == KeeperException.Code.CONNECTIONLOSS.intValue()
                        || event.getResultCode() == KeeperException.Code.SESSIONEXPIRED.intValue()) {
                    log.debug("Refresh callback ignored {}", event);
                } else {
                    handleException(KeeperException.create(event.getResultCode()));
                }
            }
        };
        client.getChildren().usingWatcher(childrenWatcher).inBackground(callback).forPath(path);
    }

    void ensurePathAndThenRefresh(RefreshMode mode) throws Exception {
        BackgroundCallback callback = (client, event) -> {
            if (event.getResultCode() == KeeperException.Code.OK.intValue()
                    || event.getResultCode() == KeeperException.Code.NONODE.intValue()) {
                refresh(mode);
            }
        };
        client.checkExists().creatingParentContainersIfNeeded().inBackground(callback)
                .forPath(ZKPaths.makePath(path, "ignored"));
    }

    void callListeners(final PathChildrenCacheEvent event) {
        listeners.forEach(listener -> {
            try {
                listener.childEvent(client, event);
            } catch (Exception e) {
                handleException(e);
            }
            return null;
        });
    }

    void getDataAndStat(String fullPath) throws Exception {
        BackgroundCallback callback = (client, event) -> {
            if (event.getResultCode() == KeeperException.Code.OK.intValue()) {
                updateCache(fullPath, event.getStat(), cacheData ? event.getData() : null);
            }
        };

        if (dataIsCompressed && cacheData) {
            client.getData().decompressed().usingWatcher(dataWatcher).inBackground(callback).forPath(fullPath);
        } else {
            client.getData().usingWatcher(dataWatcher).inBackground(callback).forPath(fullPath);
        }
    }

    /**
     * Default behavior is just to log the exception
     *
     * @param e the exception
     */
    protected void handleException(Throwable e) {
        if (e instanceof RuntimeException) {
            log.error("", e);
        } else {
            log.error(e.getMessage());
        }
        ThreadUtils.checkInterrupted(e);
    }

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

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

        case LOST:
            callListeners(new PathChildrenCacheEvent(PathChildrenCacheEvent.Type.CONNECTION_LOST, null));
            break;

        case CONNECTED:
        case RECONNECTED:
            callListeners(new PathChildrenCacheEvent(PathChildrenCacheEvent.Type.CONNECTION_RECONNECTED, null));
            try {
                refresh(RefreshMode.REBUILD);
            } catch (Exception e) {
                handleException(e);
            }
            break;
        }
    }

    private void processChildren(List<String> children, RefreshMode mode) throws Exception {
        /* Although we got watchers on the nodes and these watchers will remove the cached data
         * when the node is removed, the watchers can be lost when the ZooKeeper session expires.
         */
        Set<String> removedNodes = new HashSet<>(currentData.keySet());
        for (String child : children) {
            removedNodes.remove(ZKPaths.makePath(path, child));
        }
        for (String fullPath : removedNodes) {
            remove(fullPath);
        }

        for (String child : children) {
            String fullPath = ZKPaths.makePath(path, child);
            if (mode == RefreshMode.REBUILD || !currentData.containsKey(fullPath)) {
                getDataAndStat(fullPath);
            }
        }
    }

    private void updateCache(String fullPath, Stat stat, byte[] bytes) {
        ChildData data = new ChildData(fullPath, stat, bytes);
        ChildData previousData = currentData.put(fullPath, data);
        if (previousData == null) {
            callListeners(new PathChildrenCacheEvent(PathChildrenCacheEvent.Type.CHILD_ADDED, data));
        } else if (previousData.getStat().getVersion() != stat.getVersion()) {
            callListeners(new PathChildrenCacheEvent(PathChildrenCacheEvent.Type.CHILD_UPDATED, data));
        }
    }
}