/*
* Copyright 2014 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 org.springframework.xd.dirt.zookeeper;
import java.util.concurrent.CopyOnWriteArraySet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.state.ConnectionState;
import org.apache.curator.framework.state.ConnectionStateListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.context.SmartLifecycle;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A wrapper for a {@link CuratorFramework} instance whose lifecycle is managed as a Spring bean. Accepts
* {@link ZooKeeperConnectionListener}s to be notified when connection or disconnection events are received.
*
* @author Mark Fisher
* @author David Turanski
* @author Patrick Peralta
*/
public class ZooKeeperConnection implements SmartLifecycle {
/**
* Logger.
*/
private static final Logger logger = LoggerFactory.getLogger(ZooKeeperConnection.class);
/**
* The default client connect string. Port 2181 on localhost.
*/
public static final String DEFAULT_CLIENT_CONNECT_STRING = "localhost:2181";
/**
* The default ZooKeeper session timeout in milliseconds.
*/
public static final int DEFAULT_SESSION_TIMEOUT = 60000;
/**
* The default ZooKeeper connection timeout in milliseconds.
*/
public static final int DEFAULT_CONNECTION_TIMEOUT = 30000;
/**
* The default initial number of milliseconds between connection retries.
*/
public static final int DEFAULT_INITIAL_RETRY_WAIT = 1000;
/**
* The default number of connection attempts that will be made after
* a failed connection attempt.
*/
public static final int DEFAULT_MAX_RETRY_ATTEMPTS = 3;
/**
* The underlying {@link CuratorFramework} instance.
*/
private volatile CuratorFramework curatorFramework;
/**
* Curator client retry policy.
*/
private volatile RetryPolicy retryPolicy;
/**
* Connection listener for Curator {@link ConnectionState} events.
*/
private final DelegatingConnectionStateListener connectionListener = new DelegatingConnectionStateListener();
/**
* The set of {@link ZooKeeperConnectionListener}s that should be notified for connection and disconnection events.
*/
private final CopyOnWriteArraySet<ZooKeeperConnectionListener> listeners = new CopyOnWriteArraySet<ZooKeeperConnectionListener>();
/**
* Flag that indicates whether this connection is currently active within a context.
*/
private volatile boolean running;
/**
* Flag that indicates whether this connection should be started automatically.
*/
private volatile boolean autoStartup = true;
/**
* The current ZooKeeper ConnectionState.
*/
private volatile ConnectionState currentState;
/**
* The ZooKeeper connect string.
*/
private final String clientConnectString;
/**
* Namespace path within ZooKeeper. Default is {@link Paths#XD_NAMESPACE}.
*/
private final String namespace;
/**
* ZooKeeper session timeout in milliseconds.
*/
private final int sessionTimeout;
/**
* ZooKeeper connection timeout in milliseconds.
*/
private final int connectionTimeout;
/**
* Establish a ZooKeeper connection with the default client connect string: {@value #DEFAULT_CLIENT_CONNECT_STRING}
*/
public ZooKeeperConnection() {
this(DEFAULT_CLIENT_CONNECT_STRING, null);
}
/**
* Establish a ZooKeeper connection with the provided client connect string.
*
* @param clientConnectString one or more {@code host:port} strings, comma-delimited if more than one
*/
public ZooKeeperConnection(String clientConnectString) {
this(clientConnectString, null);
}
/**
* Establish a ZooKeeper connection with the provided client connect string and namespace.
*
* @param clientConnectString one or more {@code host:port} strings, comma-delimited if more than one
* @param namespace the root path namespace in ZooKeeper (or default namespace if null)
*/
public ZooKeeperConnection(String clientConnectString, String namespace) {
this(clientConnectString, namespace, DEFAULT_SESSION_TIMEOUT, DEFAULT_CONNECTION_TIMEOUT,
DEFAULT_INITIAL_RETRY_WAIT, DEFAULT_MAX_RETRY_ATTEMPTS);
}
/**
* Establish a ZooKeeper connection with the provided client connect string, namespace,
* and timing values.
*
* <p>This class uses {@link ExponentialBackoffRetry} internally.</p>
*
* @param clientConnectString one or more {@code host:port} strings, comma-delimited if more than one
* @param namespace the root path namespace in ZooKeeper (or default namespace if null)
* @param sessionTimeout ZooKeeper session timeout in milliseconds
* @param connectionTimeout ZooKeeper connection timeout in milliseconds
* @param initialRetryWait milliseconds between connection retries (base value)
* @param retryMaxAttempts number of connection attempts that will be made after a failed connection attempt
*/
public ZooKeeperConnection(String clientConnectString, String namespace, int sessionTimeout,
int connectionTimeout, int initialRetryWait, int retryMaxAttempts) {
Assert.hasText(clientConnectString, "clientConnectString is required");
this.clientConnectString = clientConnectString;
this.namespace = StringUtils.hasText(namespace) ? namespace : Paths.XD_NAMESPACE;
this.sessionTimeout = sessionTimeout;
this.connectionTimeout = connectionTimeout;
this.retryPolicy = new ExponentialBackoffRetry(initialRetryWait, retryMaxAttempts);
}
/**
* Checks whether the underlying connection is established.
*
* @return true if connected
*/
public boolean isConnected() {
return (this.currentState == ConnectionState.CONNECTED || this.currentState == ConnectionState.RECONNECTED);
}
/**
* Provides access to the underlying {@link CuratorFramework} instance.
*
* @return the {@link CuratorFramework} instance
*/
public CuratorFramework getClient() {
return this.curatorFramework;
}
/**
* Add a {@link ZooKeeperConnectionListener}.
*
* @param listener the listener to add
* @return true if the listener was added or false if it was already registered
*/
public boolean addListener(ZooKeeperConnectionListener listener) {
return this.listeners.add(listener);
}
/**
* Remove a {@link ZooKeeperConnectionListener}.
*
* @param listener the listener to remove
* @return true if the listener was removed or false if it was never registered
*/
public boolean removeListener(ZooKeeperConnectionListener listener) {
return this.listeners.remove(listener);
}
// Lifecycle Implementation
/**
* {@inheritDoc}
*/
@Override
public boolean isAutoStartup() {
return this.autoStartup;
}
/**
* Set the flag that indicates whether this connection
* should be started automatically.
*
* @param autoStartup if true, indicates this connection should
* be started automatically
*/
public void setAutoStartup(boolean autoStartup) {
this.autoStartup = autoStartup;
}
/**
* Set the Curator retry policy.
*
* @param retryPolicy Curator client {@link RetryPolicy}
*/
public void setRetryPolicy(RetryPolicy retryPolicy) {
Assert.notNull(retryPolicy, "retryPolicy cannot be null");
this.retryPolicy = retryPolicy;
}
/**
* Return the Curator retry policy.
*
* @return the Curator retry policy
*/
public RetryPolicy getRetryPolicy() {
return this.retryPolicy;
}
/**
* {@inheritDoc}
*/
@Override
public int getPhase() {
// start in the last possible phase
return Integer.MAX_VALUE;
}
/**
* Check whether this client is running.
*/
@Override
public boolean isRunning() {
return this.running;
}
/**
* Starts the underlying {@link CuratorFramework} instance.
*/
@Override
public synchronized void start() {
if (!this.running) {
this.curatorFramework = CuratorFrameworkFactory.builder()
.defaultData(new byte[0])
.namespace(namespace)
.retryPolicy(this.retryPolicy)
.connectString(this.clientConnectString)
.sessionTimeoutMs(sessionTimeout)
.connectionTimeoutMs(connectionTimeout)
.build();
this.curatorFramework.getConnectionStateListenable().addListener(connectionListener);
curatorFramework.start();
this.running = true;
}
}
/**
* Closes the underlying {@link CuratorFramework} instance.
*/
@Override
public synchronized void stop() {
if (this.running) {
if (this.currentState != null) {
curatorFramework.close();
}
this.running = false;
}
}
/**
* Closes the underlying {@link CuratorFramework} instance and then invokes the callback.
*/
@Override
public void stop(Runnable callback) {
this.stop();
callback.run();
}
/**
* Listener for Curator {@link ConnectionState} events that delegates to any registered ZooKeeperListeners.
*/
private class DelegatingConnectionStateListener implements ConnectionStateListener {
/**
* {@inheritDoc}
*/
@Override
public void stateChanged(CuratorFramework client, ConnectionState newState) {
currentState = newState;
switch (newState) {
case CONNECTED:
logger.info(">>> Curator connected event: " + newState);
for (ZooKeeperConnectionListener listener : listeners) {
listener.onConnect(client);
}
break;
case RECONNECTED:
logger.info(">>> Curator reconnected event: " + newState);
for (ZooKeeperConnectionListener listener : listeners) {
listener.onResume(client);
}
break;
case LOST:
logger.info(">>> Curator disconnected event: " + newState);
for (ZooKeeperConnectionListener listener : listeners) {
listener.onDisconnect(client);
}
break;
case SUSPENDED:
logger.info(">>> Curator suspended event: " + newState);
for (ZooKeeperConnectionListener listener : listeners) {
listener.onSuspend(client);
}
break;
case READ_ONLY:
// todo: ?
logger.info(">>> Curator read-only event: " + newState);
break;
}
}
}
}