io.tilt.minka.spectator.Spectator.java Source code

Java tutorial

Introduction

Here is the source code for io.tilt.minka.spectator.Spectator.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 io.tilt.minka.spectator;

import static org.apache.curator.framework.state.ConnectionState.LOST;
import static org.apache.curator.framework.state.ConnectionState.SUSPENDED;

import java.io.Closeable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.lang.Validate;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.UnhandledErrorListener;
import org.apache.curator.framework.imps.CuratorFrameworkState;
import org.apache.curator.framework.recipes.leader.LeaderLatch.State;
import org.apache.curator.framework.state.ConnectionState;
import org.apache.curator.framework.state.ConnectionStateListener;
import org.apache.curator.retry.RetryNTimes;
import org.apache.zookeeper.ZooKeeper.States;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Coordination utilities for distributed servers, extending Apache Curator recipes.
 * 
 * A publish/subscribe distributed message queue 
 * A leader election (thru Apache Curator)
 * A named locking facility to synchronize distributed processors. (thru Apache Curator)
 * 
 * @author Cristian Gonzalez
 * @since Oct 7, 2015
 */
public class Spectator implements Closeable {

    private static final Logger logger = LoggerFactory.getLogger(Spectator.class);

    private final String SHUTDOWN_THREAD_NAME = "Spectator-Shutdown";
    public final String ZOOKEEPER_CONNECT_STRING = "zookeeper.connect";
    private final String SPECTATOR_ZK_SUBDOMAIN_PATH = "spectator-domain";

    // retry during 30 mins, each 5 seconds
    public final static int CONNECTION_RETRY_ATTEMPTS_DEFAULT = 3;
    public final static int CONNECTION_RETRY_DELAY_MS_DEFAULT = 1000;
    public final static int QUEUE_RETENTION_DAYS_DEFAULT = 5;

    private static CuratorFramework sharedClient;
    private Map<String, Set<UserInstanceObject>> mapObjects;
    private String zookeeperHostPort;
    private static int connectionRetryAttempts = CONNECTION_RETRY_ATTEMPTS_DEFAULT;
    private static int connectionRetryDelayMs = CONNECTION_RETRY_DELAY_MS_DEFAULT;
    private static int queueRetentionDays = QUEUE_RETENTION_DAYS_DEFAULT;

    private ReentrantLock shutdownLock;
    private boolean shutdownDone;
    private final String logId;

    public Spectator(Spectator spec) {
        this(spec.getZookeeperHostPort(), spec.getLogId());
    }

    public Spectator() {
        this("", null);
    }

    protected String getLogId() {
        return this.logId;
    }

    public Spectator(Spectator spec, String logId) {
        this(spec.getZookeeperHostPort(), logId);
        boolean started = isStarted();
        sharedClient = spec.getClient();
        if (started) {
            hookListeners();
        }

    }

    public Spectator(final String hostPortArg, String logId) {
        this.logId = logId == null ? String.valueOf(hashCode()) : logId;
        this.shutdownLock = new ReentrantLock(true);
        // initialize user objects map
        this.mapObjects = new ConcurrentHashMap<>();
        setHostPort(hostPortArg);

        // ensure finalization
        Runtime.getRuntime().addShutdownHook(new Thread(() -> leaveAllMeetings(true), SHUTDOWN_THREAD_NAME));
    }

    private void setHostPort(final String hostPortArg) {
        String hostPort = null;
        if (hostPortArg == null || hostPortArg.isEmpty()) {
            // obtain ZK connection params
            hostPort = System.getProperty(ZOOKEEPER_CONNECT_STRING);
            if (hostPort == null || hostPort.isEmpty()) {
                hostPort = System.getenv(ZOOKEEPER_CONNECT_STRING);
            }
        } else {
            hostPort = hostPortArg;
        }
        if (hostPort != null) {
            zookeeperHostPort = hostPort;
            // initialize connection
            getClient();
        } else {
            logger.warn("{}: ({}) Spectator will wait for zookeeper connect string be statically set !",
                    getClass().getSimpleName(), logId);
        }
    }

    protected Map<String, Set<UserInstanceObject>> getUserMap() {
        return mapObjects;
    }

    /**
     * Manually close all spectator objects.
     * Dont use this for JVM destruction method. 
     * instead use {@linkplain destroy()}
     * 
     * @param closeUserListeners    Use true for user listeners to be called back   
     *   
     */
    public void close(boolean closeUserListeners) {
        // avoid calling user's listeners
        leaveAllMeetings(closeUserListeners);
    }

    public void close() {
        close(true);
    }

    /**
     * Close all spectator objects.
     * All user listeners will be called back
     */
    public void destroy() {
        leaveAllMeetings(true);
    }

    protected boolean checkExistance(final String name, final Object listener) {
        Validate.notNull(name, "the duty Name is required");
        Validate.notNull(listener, "the duty Listener is required");
        if (mapObjects.containsKey(name)) {
            for (UserInstanceObject uio : mapObjects.get(name)) {
                if (uio != null) {
                    if ((uio.getServerCandidate() != null && uio.getServerCandidate().equals(listener))
                            || (uio.getConsumer() != null && uio.getConsumer().equals(listener))) {
                        logger.warn("{}: ({}) Attempting to overwrite a Name ({}) with the same listener",
                                getClass().getSimpleName(), logId, name);
                        return true;
                    } else {
                        /*throw new RuntimeException("Name (" + name + 
                            ") already exists with a different listener instance");
                            */
                        // TODO quizas debiera tirarla
                        return false;
                    }
                }
            }
        }
        return false;
    }

    /** 
     * stop participating leader election, locks and queueing nodes 
     * called at:  
     * 1) jvm shutdown, 
     * 2) zookeeper's connection lost/suspended
     * @param closeUserObjects  when call by the user he doesnt need to be called back 
     */
    private void leaveAllMeetings(final boolean closeUserObjects) {
        if (!shutdownDone && !shutdownLock.isHeldByCurrentThread() && shutdownLock.tryLock()) {
            if (mapObjects.isEmpty()) {
                logger.warn("{}: ({}) No user listeners to stop/close !?", getClass().getSimpleName(), logId);
            } else {
                logger.info("{}: ({}) Closing all spectator objects and stopping user listeners...",
                        getClass().getSimpleName(), logId);
                for (Entry<String, Set<UserInstanceObject>> entry : mapObjects.entrySet()) {
                    String name = entry.getKey();
                    logger.warn("{}: ({}) Leaving {} ", getClass().getSimpleName(), logId, name);
                    for (UserInstanceObject uio : entry.getValue()) {
                        discard(name, uio, closeUserObjects);
                    }
                }
            }
            /*if (isStarted()) {
            sharedClient.close();
            }*/
            shutdownDone = true;
        } else {
            logger.warn("{}: ({}) Unable to obtain shutdown locks to discard spectator objects",
                    getClass().getSimpleName(), logId);
        }
    }

    private void hookListeners() {
        sharedClient.getConnectionStateListenable().addListener(new ConnectionStateListener() {
            @Override
            public void stateChanged(CuratorFramework client, ConnectionState newState) {
                logger.info("{}: ({}) Connection state perceived as {} ", Spectator.class.getSimpleName(), logId,
                        newState);
                if (newState == SUSPENDED) {
                    // only leave at lost
                    leaveAllMeetings(true);
                } else if (newState == LOST) {
                    leaveAllMeetings(true);
                } else if (newState == ConnectionState.RECONNECTED) {
                    // the latch recreates the znode and re-adds my previous listener ??
                } else if (newState == ConnectionState.READ_ONLY) {
                    // i'm not allowing entering read only mode
                }
            }
        });
        sharedClient.getUnhandledErrorListenable().addListener(new UnhandledErrorListener() {
            @Override
            public void unhandledError(String message, Throwable e) {
                logger.error("{}: ({}) Curator drops an Unhandled Error: {} ", getClass().getSimpleName(), logId,
                        message, e);
            }
        });
    }

    private void discard(String name, UserInstanceObject uio, final boolean closeUserObjects) {
        try {
            // play friendly and avoid recursiveness
            if (uio.getServerCandidate() != null) {
                uio.setCallbackListeners(closeUserObjects);
            }
            // in case of leader latch only
            if (uio.getLatch() != null && uio.getLatch().getState() == State.STARTED) {
                uio.getLatch().close();
            }

            // in case of Locks: distributed mutex mostly
            if (uio.getMutex() != null && uio.getMutex().isAcquiredInThisProcess()) {
                uio.getMutex().release();
            }
            // in case of Queues: subscription mostly
            if (uio.getNode() != null) {
                uio.getNode().close();
            }

        } catch (Exception e) {
            if (isStarted() && isConnected()) {
                logger.error("{}: ({}) Unexpected while discarding object: {} during leaving all meetings",
                        getClass().getSimpleName(), logId, name, e);
            } else {
                logger.error(
                        "{}: ({}) Zookeeper Disconection: while discarding object: {} during leaving all meetings",
                        getClass().getSimpleName(), logId, name, e.getMessage());
            }
        } finally {
            // call client's emergency callback to warn about dead ZK !! 
            if (uio.getConnectionLostCallback() != null) {
                logger.info("{}: ({}) Calling connection lost callback: {}", getClass().getSimpleName(), logId,
                        uio.getConnectionLostCallback().getClass().getName());
                uio.getConnectionLostCallback().run();
            } else {
                logger.warn("{}: ({}) Connection lost callback is NULL ", getClass().getSimpleName(), logId);
            }
        }

    }

    protected long elapsed(final long now) {
        return System.currentTimeMillis() - now;
    }

    protected void remove(final String name, final UserInstanceObject uio) {
        Set<UserInstanceObject> set = mapObjects.get(name);
        if (set != null) {
            set.remove(uio);
        }
    }

    protected void add(final String name, final UserInstanceObject uo) {
        Set<UserInstanceObject> set = mapObjects.get(name);
        if (set == null) {
            mapObjects.put(name, set = new HashSet<UserInstanceObject>());
        }
        set.add(uo);
    }

    public boolean isConnected() {
        try {
            return sharedClient.getZookeeperClient().isConnected()
                    && sharedClient.getZookeeperClient().getZooKeeper().getState() == States.CONNECTED;
        } catch (Exception e) {
        }
        return false;
    }

    protected boolean isStarted() {
        try {
            return sharedClient != null && sharedClient.getState() == CuratorFrameworkState.STARTED;
        } catch (Exception e) {
            logger.error("{}: ({}) Unexpected while checking is connected", getClass().getSimpleName(), logId, e);
        }
        return false;
    }

    protected CuratorFramework getClient() {
        if (!isStarted()) {
            if (zookeeperHostPort == null) {
                logger.error(
                        "{}: ({}) Zookeeper's connect string ({}) unresolved. Must set system property, "
                                + "environment variable or use static class setter",
                        getClass().getSimpleName(), logId, ZOOKEEPER_CONNECT_STRING);
            } else {
                synchronized (Spectator.class) {
                    if (!isStarted()) {
                        logger.info("{}: ({}) Spectator connecting to zookeeper...", getClass().getSimpleName(),
                                logId);
                        RetryPolicy retryPolicy = new RetryNTimes(connectionRetryAttempts, connectionRetryDelayMs);
                        sharedClient = CuratorFrameworkFactory.builder().namespace(SPECTATOR_ZK_SUBDOMAIN_PATH)
                                .connectString(zookeeperHostPort).retryPolicy(retryPolicy).build();
                        hookListeners();
                        try {
                            sharedClient.blockUntilConnected(10, TimeUnit.SECONDS);
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                        sharedClient.start();
                    }
                }
            }
        }
        return sharedClient;
    }

    public void setConnectionLostCallback(final String ensembleName, final Runnable callback) {
        final Set<UserInstanceObject> uios = this.mapObjects.get(ensembleName);
        for (UserInstanceObject uio : uios) {
            uio.setConnectionLostCallback(callback);
        }
    }

    /**
     * Global to all ensembles generated within this instance
     * @param callback
     */
    public void setConnectionLostCallback(final Runnable callback) {
        for (Collection<UserInstanceObject> coll : this.mapObjects.values()) {
            for (UserInstanceObject uio : coll) {
                uio.setConnectionLostCallback(callback);
            }
        }
    }

    /**
     * @return  If not manually set defaults to @see {@link Spectator.CONNECTION_RETRY_ATTEMPTS_DEFAULT}
     */
    public int getConnectionRetryAttempts() {
        return connectionRetryAttempts;
    }

    /**
     * In case ZK connection fails, set your own number of retries
     * @param connectionRetryAttempts
     */
    public void setConnectionRetryAttempts(int connectionRetryAttempts) {
        Spectator.connectionRetryAttempts = connectionRetryAttempts;
        if (isStarted()) {
            sharedClient.getZookeeperClient()
                    .setRetryPolicy(new RetryNTimes(connectionRetryAttempts, connectionRetryDelayMs));
        }
    }

    /**
     * @return  If not manually set defaults to @see {@link Spectator.CONNECTION_RETRY_DELAY_MS_DEFAULT}
     */
    public int getConnectionRetryDelayMs() {
        return connectionRetryDelayMs;
    }

    /**
     * In case ZK connection fails, set your own delay between retries
     * @param connectionRetryDelayMs
     */
    public void setConnectionRetryDelayMs(int connectionRetryDelayMs) {
        Spectator.connectionRetryDelayMs = connectionRetryDelayMs;
        if (isStarted()) {
            sharedClient.getZookeeperClient()
                    .setRetryPolicy(new RetryNTimes(connectionRetryAttempts, connectionRetryDelayMs));
        }
    }

    /**
     * @return  If not manually set defaults to @see {@link Spectator.QUEUE_RETENTION_DAYS_DEFAULT}
     */
    public static int getQueueRetentionDays() {
        return queueRetentionDays;
    }

    /**
     * Messages on queues deletion occurs by default to @see {@link Spectator.QUEUE_RETENTION_DAYS_DEFAULT}
     * @param queueRetentionDays
     */
    public void setQueueRetentionDays(int queueRetentionDays) {
        Spectator.queueRetentionDays = queueRetentionDays;
    }

    /**
     * @return  current zookeeper's connect string manually set or get as system property/environment var.
     */
    public String getZookeeperHostPort() {
        return zookeeperHostPort;
    }

    /**
     * When zookeeper's connect string not provided as System property or environment variable
     * You must manually set the value, or nothing will work.
     * 
     * @param zookeeperHostPortArg  in the format "hostname1:port,hostname2:port"
     */
    public void setZookeeperHostPort(String zookeeperHostPortArg) {
        zookeeperHostPort = zookeeperHostPortArg;
    }

}