com.metamx.druid.curator.announcement.Announcer.java Source code

Java tutorial

Introduction

Here is the source code for com.metamx.druid.curator.announcement.Announcer.java

Source

/*
 * Druid - a distributed column store.
 * Copyright (C) 2012  Metamarkets Group Inc.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package com.metamx.druid.curator.announcement;

import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Sets;
import com.google.common.io.Closeables;
import com.metamx.common.IAE;
import com.metamx.common.ISE;
import com.metamx.common.Pair;
import com.metamx.common.lifecycle.LifecycleStart;
import com.metamx.common.lifecycle.LifecycleStop;
import com.metamx.common.logger.Logger;
import com.metamx.druid.curator.ShutdownNowIgnoringExecutorService;
import com.metamx.druid.curator.cache.PathChildrenCacheFactory;
import com.metamx.druid.curator.cache.SimplePathChildrenCacheFactory;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.curator.utils.ZKPaths;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.data.Stat;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Announces things on Zookeeper.
 */
public class Announcer {
    private static final Logger log = new Logger(Announcer.class);

    private final CuratorFramework curator;
    private final PathChildrenCacheFactory factory;

    private final List<Pair<String, byte[]>> toAnnounce = Lists.newArrayList();
    private final ConcurrentMap<String, PathChildrenCache> listeners = new MapMaker().makeMap();
    private final ConcurrentMap<String, ConcurrentMap<String, byte[]>> announcements = new MapMaker().makeMap();
    private final List<String> parentsIBuilt = new CopyOnWriteArrayList<String>();

    private boolean started = false;

    public Announcer(CuratorFramework curator, ExecutorService exec) {
        this.curator = curator;
        this.factory = new SimplePathChildrenCacheFactory(false, true,
                new ShutdownNowIgnoringExecutorService(exec));
    }

    @LifecycleStart
    public void start() {
        synchronized (toAnnounce) {
            if (started) {
                return;
            }

            started = true;

            for (Pair<String, byte[]> pair : toAnnounce) {
                announce(pair.lhs, pair.rhs);
            }
            toAnnounce.clear();
        }
    }

    @LifecycleStop
    public void stop() {
        synchronized (toAnnounce) {
            if (!started) {
                return;
            }

            started = false;

            for (Map.Entry<String, PathChildrenCache> entry : listeners.entrySet()) {
                Closeables.closeQuietly(entry.getValue());
            }

            for (Map.Entry<String, ConcurrentMap<String, byte[]>> entry : announcements.entrySet()) {
                String basePath = entry.getKey();

                for (String announcementPath : entry.getValue().keySet()) {
                    unannounce(ZKPaths.makePath(basePath, announcementPath));
                }
            }

            for (String parent : parentsIBuilt) {
                try {
                    curator.delete().forPath(parent);
                } catch (Exception e) {
                    log.info(e, "Unable to delete parent[%s], boooo.", parent);
                }
            }
        }
    }

    /**
     * Announces the provided bytes at the given path.  Announcement means that it will create an ephemeral node
     * and monitor it to make sure that it always exists until it is unannounced or this object is closed.
     *
     * @param path  The path to announce at
     * @param bytes The payload to announce
     */
    public void announce(String path, byte[] bytes) {
        synchronized (toAnnounce) {
            if (!started) {
                toAnnounce.add(Pair.of(path, bytes));
                return;
            }
        }

        final ZKPaths.PathAndNode pathAndNode = ZKPaths.getPathAndNode(path);

        final String parentPath = pathAndNode.getPath();
        boolean buildParentPath = false;

        ConcurrentMap<String, byte[]> subPaths = announcements.get(parentPath);

        if (subPaths == null) {
            try {
                if (curator.checkExists().forPath(parentPath) == null) {
                    buildParentPath = true;
                }
            } catch (Exception e) {
                log.debug(e, "Problem checking if the parent existed, ignoring.");
            }

            // I don't have a watcher on this path yet, create a Map and start watching.
            announcements.putIfAbsent(parentPath, new MapMaker().<String, byte[]>makeMap());

            // Guaranteed to be non-null, but might be a map put in there by another thread.
            final ConcurrentMap<String, byte[]> finalSubPaths = announcements.get(parentPath);

            // Synchronize to make sure that I only create a listener once.
            synchronized (finalSubPaths) {
                if (!listeners.containsKey(parentPath)) {
                    final PathChildrenCache cache = factory.make(curator, parentPath);
                    cache.getListenable().addListener(new PathChildrenCacheListener() {
                        private final AtomicReference<Set<String>> pathsLost = new AtomicReference<Set<String>>(
                                null);

                        @Override
                        public void childEvent(CuratorFramework client, PathChildrenCacheEvent event)
                                throws Exception {
                            log.debug("Path[%s] got event[%s]", parentPath, event);
                            switch (event.getType()) {
                            case CHILD_REMOVED:
                                final ChildData child = event.getData();
                                final ZKPaths.PathAndNode childPath = ZKPaths.getPathAndNode(child.getPath());
                                final byte[] value = finalSubPaths.get(childPath.getNode());
                                if (value != null) {
                                    log.info("Node[%s] dropped, reinstating.", child.getPath());
                                    createAnnouncement(child.getPath(), value);
                                }
                                break;
                            case CONNECTION_LOST:
                                // Lost connection, which means session is broken, take inventory of what has been seen.
                                // This is to protect from a race condition in which the ephemeral node could have been
                                // created but not actually seen by the PathChildrenCache, which means that it won't know
                                // that it disappeared and thus will not generate a CHILD_REMOVED event for us.  Under normal
                                // circumstances, this can only happen upon connection loss; but technically if you have
                                // an adversary in the system, they could also delete the ephemeral node before the cache sees
                                // it.  This does not protect from that case, so don't have adversaries.

                                Set<String> pathsToReinstate = Sets.newHashSet();
                                for (String node : finalSubPaths.keySet()) {
                                    pathsToReinstate.add(ZKPaths.makePath(parentPath, node));
                                }

                                for (ChildData data : cache.getCurrentData()) {
                                    pathsToReinstate.remove(data.getPath());
                                }

                                if (!pathsToReinstate.isEmpty()
                                        && !pathsLost.compareAndSet(null, pathsToReinstate)) {
                                    log.info("Already had a pathsLost set!?[%s]", parentPath);
                                }
                                break;
                            case CONNECTION_RECONNECTED:
                                final Set<String> thePathsLost = pathsLost.getAndSet(null);

                                if (thePathsLost != null) {
                                    for (String path : thePathsLost) {
                                        log.info("Reinstating [%s]", path);
                                        final ZKPaths.PathAndNode split = ZKPaths.getPathAndNode(path);
                                        createAnnouncement(path,
                                                announcements.get(split.getPath()).get(split.getNode()));
                                    }
                                }
                                break;
                            }
                        }
                    });

                    synchronized (toAnnounce) {
                        if (started) {
                            if (buildParentPath) {
                                createPath(parentPath);
                            }
                            startCache(cache);
                            listeners.put(parentPath, cache);
                        }
                    }
                }
            }

            subPaths = finalSubPaths;
        }

        boolean created = false;
        synchronized (toAnnounce) {
            if (started) {
                byte[] oldBytes = subPaths.putIfAbsent(pathAndNode.getNode(), bytes);

                if (oldBytes == null) {
                    created = true;
                } else if (!Arrays.equals(oldBytes, bytes)) {
                    throw new IAE("Cannot reannounce different values under the same path");
                }
            }
        }

        if (created) {
            try {
                createAnnouncement(path, bytes);
            } catch (Exception e) {
                throw Throwables.propagate(e);
            }
        }
    }

    public void update(final String path, final byte[] bytes) {
        final ZKPaths.PathAndNode pathAndNode = ZKPaths.getPathAndNode(path);

        final String parentPath = pathAndNode.getPath();
        final String nodePath = pathAndNode.getNode();

        ConcurrentMap<String, byte[]> subPaths = announcements.get(parentPath);

        if (subPaths == null || subPaths.get(nodePath) == null) {
            throw new ISE("Cannot update a path[%s] that hasn't been announced!", path);
        }

        synchronized (toAnnounce) {
            try {
                byte[] oldBytes = subPaths.get(nodePath);

                if (!Arrays.equals(oldBytes, bytes)) {
                    subPaths.put(nodePath, bytes);
                    updateAnnouncement(path, bytes);
                }
            } catch (Exception e) {
                throw Throwables.propagate(e);
            }
        }
    }

    private String createAnnouncement(final String path, byte[] value) throws Exception {
        return curator.create().compressed().withMode(CreateMode.EPHEMERAL).inBackground().forPath(path, value);
    }

    private Stat updateAnnouncement(final String path, final byte[] value) throws Exception {
        return curator.setData().compressed().inBackground().forPath(path, value);
    }

    /**
     * Unannounces an announcement created at path.  Note that if all announcements get removed, the Announcer
     * will continue to have ZK watches on paths because clearing them out is a source of ugly race conditions.
     * <p/>
     * If you need to completely clear all the state of what is being watched and announced, stop() the Announcer.
     *
     * @param path
     */
    public void unannounce(String path) {
        log.info("unannouncing [%s]", path);
        final ZKPaths.PathAndNode pathAndNode = ZKPaths.getPathAndNode(path);
        final String parentPath = pathAndNode.getPath();

        final ConcurrentMap<String, byte[]> subPaths = announcements.get(parentPath);

        if (subPaths == null || subPaths.remove(pathAndNode.getNode()) == null) {
            log.error("Path[%s] not announced, cannot unannounce.", path);
            return;
        }

        try {
            curator.delete().guaranteed().forPath(path);
        } catch (KeeperException.NoNodeException e) {
            log.info("node[%s] didn't exist anyway...", path);
        } catch (Exception e) {
            throw Throwables.propagate(e);
        }
    }

    private void startCache(PathChildrenCache cache) {
        try {
            cache.start();
        } catch (Exception e) {
            Closeables.closeQuietly(cache);
            throw Throwables.propagate(e);
        }
    }

    private void createPath(String parentPath) {
        try {
            curator.create().creatingParentsIfNeeded().forPath(parentPath);
            parentsIBuilt.add(parentPath);
        } catch (Exception e) {
            log.info(e, "Problem creating parentPath[%s], someone else created it first?", parentPath);
        }
    }
}