com.heliosapm.streams.common.zoo.AdminFinder.java Source code

Java tutorial

Introduction

Here is the source code for com.heliosapm.streams.common.zoo.AdminFinder.java

Source

/*
 * Copyright 2015 the original author or authors.
 *
 * 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.heliosapm.streams.common.zoo;

import java.nio.charset.Charset;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.curator.CuratorZookeeperClient;
import org.apache.curator.RetryPolicy;
import org.apache.curator.RetrySleeper;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.imps.CuratorFrameworkState;
import org.apache.curator.framework.listen.Listenable;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.TreeCache;
import org.apache.curator.framework.recipes.cache.TreeCacheEvent;
import org.apache.curator.framework.recipes.cache.TreeCacheListener;
import org.apache.curator.framework.state.ConnectionState;
import org.apache.curator.framework.state.ConnectionStateListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.zookeeper.ClientCnxn;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

import com.heliosapm.utils.config.ConfigurationHelper;
import com.heliosapm.utils.io.StdInCommandHandler;
import com.heliosapm.utils.jmx.JMXHelper;
import com.heliosapm.utils.jmx.JMXManagedThreadFactory;
import com.heliosapm.utils.jmx.JMXManagedThreadPool;

/**
 * <p>Title: AdminFinder</p>
 * <p>Description: Zookeeper client to find and listen on the StreamHubAdmin server.</p> 
 * <p>Company: Helios Development Group LLC</p>
 * @author Whitehead (nwhitehead AT heliosdev DOT org)
 * <p><code>com.heliosapm.streams.common.zoo.AdminFinder</code></p>
 */

public class AdminFinder implements Watcher, RetryPolicy, ConnectionStateListener {
    /** The singleton instance */
    private static volatile AdminFinder instance = null;
    /** The singleton instance ctor lock */
    private static final Object lock = new Object();
    /** The zookeep parent node name to retrieve the streamhub admin url */
    public static final String ZOOKEEP_URL_ROOT = "/streamhub/admin";
    /** The zookeep node name to retrieve the streamhub admin url */
    public static final String ZOOKEEP_URL = ZOOKEEP_URL_ROOT + "/url";
    /** The command line arg prefix for the zookeep connect */
    public static final String ZOOKEEP_CONNECT_ARG = "--zookeep=";
    /** The default zookeep connect */
    public static final String DEFAULT_ZOOKEEP_CONNECT = "localhost:2181";
    /** The command line arg prefix for the zookeep session timeout in ms. */
    public static final String ZOOKEEP_TIMEOUT_ARG = "--ztimeout=";
    /** The default zookeep session timeout in ms. */
    public static final int DEFAULT_ZOOKEEP_TIMEOUT = 15000;
    /** The command line arg prefix for the retry pause period in ms. */
    public static final String RETRY_ARG = "--retry=";
    /** The default retry pause period in ms. */
    public static final int DEFAULT_RETRY = 15000;
    /** The command line arg prefix for the connect retry timeout in ms. */
    public static final String CONNECT_TIMEOUT_ARG = "--ctimeout=";
    /** The default zookeep connect timeout in ms. */
    public static final int DEFAULT_CONNECT_TIMEOUT = 5000;
    /** The UTF character set */
    public static final Charset UTF8 = Charset.forName("UTF8");

    /**
     * Initializes and acquires the AdminFinder singleton instance
     * @param args The app command line arguments
     * @return the AdminFinder singleton instance
     */
    public static AdminFinder getInstance(final String[] args) {
        if (instance == null) {
            synchronized (lock) {
                if (instance == null) {
                    instance = new AdminFinder(args);
                }
            }
        }
        instance.loff();
        instance.start();
        return instance;
    }

    /**
     * Acquires the initialized AdminFinder singleton instance
     * @return the AdminFinder singleton instance
     */
    public static AdminFinder getInstance() {
        if (instance == null) {
            synchronized (lock) {
                if (instance == null) {
                    throw new IllegalStateException("The AdminFinder has not been initialized");
                }
            }
        }
        return instance;
    }

    /** Instance logger */
    protected final Logger log = LogManager.getLogger(AdminFinder.class);
    /** The zookeep connect string */
    protected String zookeepConnect = null;

    /** The zookeep session timeout in ms. */
    protected int zookeepTimeout = -1;
    /** The curator zookeep client */
    protected CuratorZookeeperClient czk = null;
    /** The zookeep client */
    protected ZooKeeper zk = null;

    /** The tree cache instance */
    protected TreeCache treeCache = null;

    /** The curator framework */
    protected CuratorFramework cf = null;

    /** The admin URL */
    protected final AtomicReference<String> adminURL = new AtomicReference<String>(null);
    /** The admin URL latch to wait on if not set */
    protected final AtomicReference<CountDownLatch> adminURLLatch = new AtomicReference<CountDownLatch>(
            new CountDownLatch(1));

    /** The curator framework connection state */
    protected final AtomicReference<ConnectionState> cfState = new AtomicReference<ConnectionState>(
            ConnectionState.LOST);

    /** The zookeep session id */
    protected Long sessionId = null;
    /** The initial connect retry timeout in ms. */
    protected int connectTimeout = -1;
    /** The reconnect payse time in ms. */
    protected int retryPauseTime = -1;
    /** The thread factroy for the curator client */
    protected final ThreadFactory threadFactory = JMXManagedThreadFactory.newThreadFactory("ZooKeeperAdminFinder",
            true);
    /** The thread pool to run async tasks in */
    protected final JMXManagedThreadPool threadPool = JMXManagedThreadPool.builder().corePoolSize(2).maxPoolSize(12)
            .keepAliveTimeMs(60000)
            .objectName(
                    JMXHelper.objectName("com.heliosapm.streams.common.zoo:service=ThreadPool,name=AdminFinder"))
            .poolName("AdminFinder").prestart(2).publishJMX(true).queueSize(32).threadFactory(threadFactory)
            .build();

    /** A set of registered admin URL listeners */
    protected final Set<AdminURLListener> listeners = new CopyOnWriteArraySet<AdminURLListener>();

    /**
     * Creates a new AdminFinder
     * @param args the command line args
     */
    public AdminFinder(final String[] args) {
        zookeepConnect = findArg(ZOOKEEP_CONNECT_ARG,
                ConfigurationHelper.getSystemThenEnvProperty("zookeep.connect", DEFAULT_ZOOKEEP_CONNECT), args);
        zookeepTimeout = findArg(ZOOKEEP_TIMEOUT_ARG, DEFAULT_ZOOKEEP_TIMEOUT, args);
        connectTimeout = findArg(CONNECT_TIMEOUT_ARG, DEFAULT_CONNECT_TIMEOUT, args);
        retryPauseTime = findArg(RETRY_ARG, DEFAULT_RETRY, args);
        cf = CuratorFrameworkFactory.builder().canBeReadOnly(false).connectionTimeoutMs(connectTimeout)
                .sessionTimeoutMs(zookeepTimeout).connectString(zookeepConnect)
                .retryPolicy(new ExponentialBackoffRetry(5000, 200))
                //.retryPolicy(this)
                .threadFactory(threadFactory).build();
        cf.getConnectionStateListenable().addListener(this);
    }

    /*
     * States:
     * =======
     * cf not started
     * cf started / not connected
     * cf started / connected
     * 
     */

    /**
     * Starts the connection attempt loop
     * @return this AdminFinder 
     */
    protected AdminFinder start() {
        if (!isConnected()) {
            threadFactory.newThread(new Runnable() {
                @Override
                public void run() {
                    log.info("Starting Connection Loop");
                    if (cf.getState() != CuratorFrameworkState.STARTED) {
                        loff();
                        try {
                            cf.start();
                            final AtomicInteger attempt = new AtomicInteger(0);
                            while (true) {
                                final int att = attempt.incrementAndGet();
                                final long start = System.currentTimeMillis();
                                try {
                                    log.info("Starting connection attempt #{}", att);
                                    if (cf.blockUntilConnected(retryPauseTime, TimeUnit.MILLISECONDS)) {
                                        log.info("Connected to [{}] on attempt #{}",
                                                cf.getZookeeperClient().getCurrentConnectionString(), att);
                                        czk = cf.getZookeeperClient();
                                        try {
                                            zk = czk.getZooKeeper();
                                        } catch (Exception ex) {
                                            log.warn(
                                                    "Failed to get ZooKeeper instance from connected CuratorZookeeperClient",
                                                    ex);
                                        }
                                        acquireAdminURL();
                                        break;
                                    }
                                    final long elapsedSecs = TimeUnit.MILLISECONDS
                                            .toSeconds(System.currentTimeMillis() - start);
                                    log.info(
                                            "No connection acquired in last [{}] seconds for attempt #{}. Retrying....",
                                            elapsedSecs, att);
                                } catch (InterruptedException iex) {
                                    if (Thread.interrupted())
                                        Thread.interrupted();
                                    log.info(
                                            "Connect attempt #{} Interrupted while waiting on connect. Starting new attempt.",
                                            att);
                                }
                            }
                        } finally {
                            lon();
                        }
                    }

                }
            }).start();
        }
        return this;
    }

    /**
     * Acquires the admin url
     * @param block if true, blocks if the admin url is not available yet
     * @return the admin url or null if it has not been acquired yet and block was false
     */
    public String getAdminURL(final boolean block) {
        String s = adminURL.get();
        if (s == null && block) {
            try {
                adminURLLatch.get().await();
                s = adminURL.get();
            } catch (InterruptedException iex) {
                log.error("Interrupted while waiting on AdminURL", iex);
                throw new RuntimeException("Interrupted while waiting on AdminURL", iex);
            }
        }
        return s;
    }

    /**
     * Adds an adminURL listener
     * @param listener the listener to add
     */
    public void registerAdminURLListener(final AdminURLListener listener) {
        if (listener != null) {
            listeners.add(listener);
            final String s = adminURL.get();
            if (s != null)
                listener.onAdminURL(s);
        }
    }

    /**
     * Removes an adminURL listener
     * @param listener the listener to remove
     */
    public void removeAdminURLListener(final AdminURLListener listener) {
        if (listener != null) {
            listeners.remove(listener);
        }
    }

    /**
     * {@inheritDoc}
     * @see org.apache.curator.framework.state.ConnectionStateListener#stateChanged(org.apache.curator.framework.CuratorFramework, org.apache.curator.framework.state.ConnectionState)
     */
    @Override
    public void stateChanged(final CuratorFramework client, final ConnectionState newState) {
        final ConnectionState cs = cfState.getAndSet(newState);
        log.info("cfState transition: [{}] --> [{}]", cs, newState);

        switch (newState) {
        case CONNECTED:
            break;
        case LOST:
            break;
        case READ_ONLY:
            break;
        case RECONNECTED:
            break;
        case SUSPENDED:
            break;
        default:
            break;

        }
    }

    /**
     * Indicates if the underlying curator framework is started
     * @return true if the underlying curator framework is started, false otherwise
     */
    public boolean isStarted() {
        return cf.getState() != CuratorFrameworkState.STOPPED;
    }

    /**
     * Indicates if the underlying curator framework is connected
     * @return true if the underlying curator framework is connected, false otherwise
     */
    public boolean isConnected() {
        return cfState.get().isConnected();
    }

    /**
     * Attempts to acquire the AdminURL. If not present, starts an AdminURL waiting loop.
     */
    protected void acquireAdminURL() {
        try {
            log.info("Starting AdminURL Acquisition Loop");
            Stat stat = zk.exists(ZOOKEEP_URL, false);
            if (stat != null) {
                updateAdminURL(zk.getData(ZOOKEEP_URL, false, stat));
            } else {
                log.error(
                        "Connected ZooKeeper server does not contain the StreamHubAdmin URL. Will wait for it on [{}]",
                        czk.getCurrentConnectionString());
                waitForAdminURLBind();
            }
        } catch (Exception ex) {
            throw new RuntimeException("Failed to acquire AdminURL from [" + zookeepConnect + "]", ex);
        }
    }

    /**
     * Handles tasks required when the AdminURL is acquired
     * @param data The data acquired from thezookeep bound URL
     */
    protected void updateAdminURL(final byte[] data) {
        final String aUrl = new String(data, UTF8);
        log.info("AdminURL acquired: [{}]", aUrl);
        final String prior = adminURL.getAndSet(aUrl);
        adminURLLatch.get().countDown();
        fireAdminURLAcquired(prior, aUrl);
    }

    /**
     * Starts an AdminURL bind event loop
     */
    protected void waitForAdminURLBind() {
        final TreeCache tc = TreeCache.newBuilder(cf, ZOOKEEP_URL)
                //.setExecutor(threadFactory)
                //.setExecutor((ExecutorService)threadPool)
                //.setCacheData(true)
                .build();
        final AtomicBoolean waiting = new AtomicBoolean(true);
        final Thread waitForAdminURLThread = threadFactory.newThread(new Runnable() {
            @Override
            public void run() {
                final Thread waitThread = Thread.currentThread();
                try {
                    final Listenable<TreeCacheListener> listen = tc.getListenable();
                    listen.addListener(new TreeCacheListener() {
                        @Override
                        public void childEvent(final CuratorFramework client, final TreeCacheEvent event)
                                throws Exception {
                            ChildData cd = event.getData();
                            if (cd != null) {
                                log.info("TreeCache Bound [{}]", cd.getPath());
                                final String boundPath = cd.getPath();
                                if (ZOOKEEP_URL.equals(boundPath)) {
                                    updateAdminURL(cd.getData());
                                    tc.close();
                                    waiting.set(false);
                                    waitThread.interrupt();
                                }
                            }
                        }
                    });
                    tc.start();
                    log.debug("AdminURL TreeCache Started");
                    // Check for the data one more time in case we missed 
                    // the bind event while setting up the listener
                    final ZooKeeper z = cf.getZookeeperClient().getZooKeeper();
                    final Stat st = z.exists(ZOOKEEP_URL, false);
                    if (st != null) {
                        updateAdminURL(z.getData(ZOOKEEP_URL, false, st));
                        tc.close();
                    }
                    while (true) {
                        try {
                            Thread.currentThread().join(retryPauseTime);
                            log.info("Still waiting for AdminURL....");
                        } catch (InterruptedException iex) {
                            if (Thread.interrupted())
                                Thread.interrupted();
                        }
                        if (!waiting.get())
                            break;
                    }
                    log.info("Ended wait for AdminURL");
                } catch (Exception ex) {
                    log.error("Failed to wait for AdminURL bind", ex);
                    // FIXME:
                } finally {
                    try {
                        tc.close();
                    } catch (Exception x) {
                        /* No Op */}
                }
            }
        });
        waitForAdminURLThread.start();
    }

    /**
     * Notifies all registered listeners of an AdminURL event
     * @param priorUrl The prior URL, null if this is the first
     * @param newUrl The new URL, null if a removed
     */
    protected void fireAdminURLAcquired(final String priorUrl, final String newUrl) {
        if (!listeners.isEmpty()) {
            for (final AdminURLListener listener : listeners) {
                threadPool.execute(new Runnable() {
                    @Override
                    public void run() {
                        if (newUrl != null) {
                            if (priorUrl != null) {
                                listener.onAdminURLChanged(priorUrl, newUrl);
                            } else {
                                listener.onAdminURL(newUrl);
                            }
                        } else {
                            listener.onAdminURLRemoved(priorUrl);
                        }
                    }
                });
            }
        }
    }

    /**
     * Get Admn URL test
     * @param args none
     */
    public static void main(final String[] args) {
        final AdminFinder af = AdminFinder.getInstance(args);
        af.registerAdminURLListener(new EmptyAdminURLListener() {
            @Override
            public void onAdminURL(final String adminURL) {
                System.err.println("GOT ADMIN URL: " + adminURL);
            }
        });
        af.threadPool.execute(new Runnable() {
            public void run() {
                System.err.println("[" + Thread.currentThread().getName() + "] Starting wait for AdminURL...");
                final String s = af.getAdminURL(true);
                System.err.println("[" + Thread.currentThread().getName() + "] Got it: [" + s + "]");
            }
        });
        StdInCommandHandler.getInstance().run();
    }

    protected volatile Level cxnLevel = Level.INFO;

    protected void loff() {
        LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
        Configuration config = ctx.getConfiguration();
        LoggerConfig loggerConfig = config.getLoggerConfig(ClientCnxn.class.getName());
        cxnLevel = loggerConfig.getLevel();
        loggerConfig.setLevel(Level.ERROR);
        ctx.updateLoggers();
    }

    protected void lon() {
        LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
        Configuration config = ctx.getConfiguration();
        LoggerConfig loggerConfig = config.getLoggerConfig(ClientCnxn.class.getName());
        loggerConfig.setLevel(cxnLevel);
        ctx.updateLoggers();
    }

    /**
     * Finds a command line arg value
     * @param prefix The prefix
     * @param defaultValue The default value if not found
     * @param args The command line args to search
     * @return the value
     */
    private static int findArg(final String prefix, final int defaultValue, final String[] args) {
        final String s = findArg(prefix, (String) null, args);
        if (s == null)
            return defaultValue;
        try {
            return Integer.parseInt(s);
        } catch (Exception ex) {
            return defaultValue;
        }
    }

    /**
     * Finds a command line arg value
     * @param prefix The prefix
     * @param defaultValue The default value if not found
     * @param args The command line args to search
     * @return the value
     */
    @SuppressWarnings("unused")
    private static long findArg(final String prefix, final long defaultValue, final String[] args) {
        final String s = findArg(prefix, (String) null, args);
        if (s == null)
            return defaultValue;
        try {
            return Long.parseLong(s);
        } catch (Exception ex) {
            return defaultValue;
        }
    }

    /**
     * Finds a command line arg value
     * @param prefix The prefix
     * @param defaultValue The default value if not found
     * @param args The command line args to search
     * @return the value
     */
    private static String findArg(final String prefix, final String defaultValue, final String[] args) {
        for (String s : args) {
            if (s.startsWith(prefix)) {
                s = s.replace(prefix, "").trim();
                return s;
            }
        }
        return defaultValue;
    }

    /**
     * {@inheritDoc}
     * @see org.apache.zookeeper.Watcher#process(org.apache.zookeeper.WatchedEvent)
     */
    @Override
    public void process(final WatchedEvent event) {
        switch (event.getState()) {
        case Disconnected:
            log.info("ZooKeep Session Disconnected");
            sessionId = null;
            break;
        case Expired:
            log.info("ZooKeep Session Expired");
            sessionId = null;
            break;
        case SyncConnected:
            sessionId = getSessionId();
            log.info("ZooKeep Connected. SessionID: [{}]", sessionId);
            break;
        default:
            log.info("ZooKeep Event: [{}]", event);
            break;
        }
    }

    /**
     * Acquires the curator client's zookeeper client's session id
     * @return the curator client's zookeeper client's session id
     */
    protected Long getSessionId() {
        if (czk == null)
            return null;
        try {
            return czk.getZooKeeper().getSessionId();
        } catch (Exception ex) {
            log.error("Disaster. Curator client does not have a zookeeper. Developer Error", ex);
            System.exit(-1);
            return null;
        }
    }

    private final AtomicInteger retries = new AtomicInteger(0);

    /**
     * {@inheritDoc}
     * @see org.apache.curator.RetryPolicy#allowRetry(int, long, org.apache.curator.RetrySleeper)
     */
    @Override
    public boolean allowRetry(final int retryCount, final long elapsedTimeMs, final RetrySleeper sleeper) {
        final int r = retries.incrementAndGet();
        if (elapsedTimeMs < retryPauseTime) {
            try {
                sleeper.sleepFor(retryPauseTime - elapsedTimeMs, TimeUnit.MILLISECONDS);
            } catch (Exception ex) {
                log.warn("RetrySleeper Interrupted", ex);
            }
        }
        log.info("Attempting connect retry #{} to [{}]", r, zookeepConnect);
        return true;
    }

    /**
     * Returns the configured zookeeper connect string
     * @return the configured zookeeper connect string
     */
    public String getZookeepConnect() {
        return zookeepConnect;
    }

}