org.lable.oss.dynamicconfig.provider.zookeeper.NodeWatcher.java Source code

Java tutorial

Introduction

Here is the source code for org.lable.oss.dynamicconfig.provider.zookeeper.NodeWatcher.java

Source

/*
 * Copyright (C) 2015 Lable (info@lable.nl)
 *
 * 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 org.lable.oss.dynamicconfig.provider.zookeeper;

import org.apache.zookeeper.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
 * Zookeeper node watcher.
 */
public class NodeWatcher implements Watcher, Runnable, Closeable {
    private final static Logger logger = LoggerFactory.getLogger(NodeWatcher.class);

    /*
     * If the Zookeeper quorum cannot be reached, the watcher thread will attempt to reconnect, with incrementally
     * longer waits between attempts. It should wait at most this long (in minutes) between attempts.
     */
    static final int MAX_RETRY_WAIT_MINUTES = 5;

    private final AsyncCallback.DataCallback callback;
    private final String quorum;
    private final String path;

    private ZooKeeper zk;

    private int retryCounter;
    private int retryWait;

    State state = State.STARTING;

    /**
     * Construct a new NodeWatcher.
     *
     * @param quorum Comma-separated list of addresses for the Zookeeper quorum.
     * @param callback Callback method called upon changes in the node.
     * @param path Path of the znode monitored.
     */
    public NodeWatcher(String quorum, AsyncCallback.DataCallback callback, String path) {
        this.quorum = quorum;
        this.callback = callback;
        this.path = path;
        resetRetryCounters();
        state = State.LIVE;
        connect();
    }

    @Override
    public void run() {
        try {
            synchronized (this) {
                wait();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    @Override
    public void close() throws IOException {
        if (state == State.CLOSED)
            return;

        synchronized (this) {
            state = State.CLOSED;
            try {
                zk.close();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            notifyAll();
        }
    }

    @Override
    public void process(WatchedEvent watchedEvent) {
        if (state != State.LIVE)
            return;

        Event.KeeperState state = watchedEvent.getState();
        Event.EventType type = watchedEvent.getType();

        // This switch handles all relevant states, and tries to reset the watch on the znode after it is
        // triggered.
        switch (state) {
        case SyncConnected:
        case ConnectedReadOnly:
            resetRetryCounters();
            switch (type) {
            case NodeCreated:
            case NodeDataChanged:
                // Configuration znode changed, let the callback know.
                zk.getData(path, this, callback, null);
                break;
            case None:
                registerWatcher(path);
                break;
            case NodeDeleted:
                logger.error("Our configuration znode was deleted. Waiting for it to be recreated");
                registerWatcher(path);
                break;
            }
            break;
        case Disconnected:
            logger.warn("Disconnected from Zookeeper quorum, reconnecting");
            // The Zookeeper instance will automatically attempt reconnection.
            waitBeforeRetrying();
            break;
        case Expired:
            logger.warn("Connection to Zookeeper quorum expired. Attempting to reconnect");
            // The Zookeeper instance is no longer valid. We have to reconnect ourselves.
            connect();
            break;
        case SaslAuthenticated:
        case AuthFailed:
            // Probably not relevant to us.
            break;
        }
    }

    /**
     * Connect to the Zookeeper quorum and create a new Zookeeper instance.
     */
    void connect() {
        if (state != State.LIVE)
            return;

        logger.debug("Connecting to ZooKeeper Quorum.");
        if (zk != null) {
            try {
                zk.close();
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
            }
        }
        if (retryCounter > 0) {
            logger.warn("Failed to connect to Zookeeper quorum, retrying (" + retryCounter + ").");
        }
        try {
            zk = new ZooKeeper(quorum, 3000, this);
        } catch (IOException e) {
            waitBeforeRetrying();
            connect();
        }
    }

    /**
     * Register the ZooKeeper watcher for a node. If registering it fails {@link #waitBeforeRetrying()} is called
     * once to prevent hammering.
     *
     * @param path Node to watch.
     */
    void registerWatcher(String path) {
        try {
            // Register the watcher.
            zk.exists(path, this);
        } catch (KeeperException.SessionExpiredException e) {
            connect();
        } catch (KeeperException e) {
            logger.error("KeeperException caught, retrying", e);
            waitBeforeRetrying();
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
        }
    }

    /**
     * Sleep a while before continuing. This increments a waiting counter and sleeps longer each time it is
     * called, until {@link #MAX_RETRY_WAIT_MINUTES} is reached.
     */
    void waitBeforeRetrying() {
        if (retryWait < MAX_RETRY_WAIT_MINUTES * 60) {
            retryWait *= 2;
            if (retryWait > MAX_RETRY_WAIT_MINUTES * 60) {
                retryWait = MAX_RETRY_WAIT_MINUTES * 60;
            }
        }
        retryCounter++;
        try {
            logger.info("Failed to connect to ZooKeeper quorum, waiting " + retryWait + "s before retrying.");
            TimeUnit.SECONDS.sleep(retryWait);
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
        }
    }

    /**
     * Reset retry waiting time and counter.
     */
    void resetRetryCounters() {
        retryCounter = 0;
        retryWait = 10;
    }

    public enum State {
        STARTING, LIVE, CLOSED
    }
}