/**
* Copyright 2014 Comcast Cable Communications Management, LLC
*
* 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.comcast.viper.flume2storm.location;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.comcast.viper.flume2storm.utility.test.TestCondition;
import com.comcast.viper.flume2storm.utility.test.TestUtils;
import com.comcast.viper.flume2storm.zookeeper.ZkClient;
import com.comcast.viper.flume2storm.zookeeper.ZkClientListener;
import com.comcast.viper.flume2storm.zookeeper.ZkNodeCreationArg;
import com.comcast.viper.flume2storm.zookeeper.ZkOperation;
import com.comcast.viper.flume2storm.zookeeper.ZkUtilies;
import com.google.common.base.Preconditions;
/**
* An implementation of LocationService that uses ZooKeeper for coordination:
* the ServiceProviders advertise their presence by maintaining an ephemeral
* znode.
*
* @param <SP>
* The Service Provider class
*/
public class DynamicLocationService<SP extends ServiceProvider<?>> extends AbstractLocationService<SP> {
// TODO It would be nice to have a utility that connects using the dynamic
// location service and expose the service providers available
protected static final Logger LOG = LoggerFactory.getLogger(DynamicLocationService.class);
private static final String SNODE_BASE_NAME = "server";
protected final ZkClient zkClient;
// ZK's path for that service: $basePath/$serviceName
protected final String servicePath;
// Local registration
private final Map<SP, String> registrations;
private final ServiceProviderSerialization<SP> serviceProviderSerialization;
/**
* Constructor for {@link DynamicLocationService}
*
* @param config
* The {@link DynamicLocationService} configuration
* @param ser
* The {@link ServiceProvider} serialization
*/
public DynamicLocationService(final DynamicLocationServiceConfiguration config,
final ServiceProviderSerialization<SP> ser) {
this.serviceProviderSerialization = ser;
zkClient = new ZkClient(new ZkListener());
zkClient.configure(config);
servicePath = ZkUtilies.buildZkPath(config.getBasePath(), config.getServiceName());
registrations = new ConcurrentHashMap<SP, String>();
LOG.debug("Created DynamicLocationService with: {}", config);
}
/**
* Connects ZooKeeper and initializes the {@link DynamicLocationService}
*
* @see com.comcast.viper.flume2storm.location.LocationService#start()
*/
@Override
public boolean start() {
LOG.debug("Starting...");
if (!zkClient.start())
return false;
// TODO use configuration to set a max timeout
return waitReady();
}
/**
* Disconnects ZooKeeper and terminates the {@link DynamicLocationService}
*
* @see com.comcast.viper.flume2storm.location.LocationService#stop()
*/
@Override
public boolean stop() {
LOG.debug("Stopping...");
return zkClient.stop();
}
/**
* @return True if connected to Zookeeper
*/
public boolean isConnected() {
return zkClient.getState().isConnected();
}
private class ZkClientReadyCondition implements TestCondition {
protected ZkClientReadyCondition() {
super();
}
/**
* @see com.comcast.viper.flume2storm.utility.test.TestCondition#evaluate()
*/
@Override
public boolean evaluate() {
return zkClient.getState().isSetup();
}
}
/**
* Sleeps until the library is ready to proceed (i.e. connection established,
* setup completed, ...)
*
* @param timeout
* The maximum amount of time to wait (in milliseconds)
* @return True if the {@link DynamicLocationService} is ready, false if the
* operation timed out
*/
protected boolean waitReady(final int timeout) {
try {
return TestUtils.waitFor(new ZkClientReadyCondition(), timeout, 100);
} catch (Exception e) {
return false;
}
}
/**
* Sleeps until the library is ready to proceed (i.e. connection established,
* setup completed, ...)
*
* @throws InterruptedException
* If the thread is interrupted while waiting
*/
protected boolean waitReady() {
return waitReady(Integer.MAX_VALUE);
}
/**
* @see com.comcast.viper.flume2storm.location.LocationService#register(com.comcast.viper.flume2storm.location.ServiceProvider)
*/
public void register(SP serviceProvider) {
Preconditions.checkNotNull(serviceProvider);
// TODO Perhaps put a request on the queue to make sure this happens
// when connected
if (registrations.containsKey(serviceProvider)) {
LOG.warn("ServiceProvider {} already registered", serviceProvider);
return;
}
if (!isConnected()) {
LOG.warn("Not connected to ZooKeeper");
return;
}
LOG.debug("Registering service...");
try {
// TODO improve disconnection performances by using a std
// ephemeral node and using UUID of the ServiceInstance
final byte[] bytes = serviceProviderSerialization.serialize(serviceProvider);
final String p = ZkUtilies.buildZkPath(servicePath, SNODE_BASE_NAME);
String lastNodeName = new ZkOperation(zkClient, p).createNode(new ZkNodeCreationArg().setCreateMode(
CreateMode.EPHEMERAL_SEQUENTIAL).setData(bytes));
LOG.debug("Created ephemeral node: {}", lastNodeName);
registrations.put(serviceProvider, lastNodeName);
} catch (final Exception e) {
LOG.error("Failed to register server instance: " + serviceProvider, e);
}
}
/**
* @see com.comcast.viper.flume2storm.location.LocationService#unregister(com.comcast.viper.flume2storm.location.ServiceProvider)
*/
public void unregister(SP serviceProvider) {
Preconditions.checkNotNull(serviceProvider);
if (!isConnected()) {
LOG.warn("Not connected to ZooKeeper");
return;
}
if (!registrations.containsKey(serviceProvider)) {
LOG.warn("ServiceProvider {} not registered locally", serviceProvider);
return;
}
try {
new ZkOperation(zkClient, registrations.remove(serviceProvider)).deleteNode();
} catch (Exception e) {
LOG.error("Failed to unregister server instance: " + serviceProvider, e);
}
}
/**
* @see com.comcast.viper.flume2storm.location.LocationService#getSerialization()
*/
public ServiceProviderSerialization<SP> getSerialization() {
return serviceProviderSerialization;
}
private class ZkListener implements ZkClientListener, Serializable {
private static final long serialVersionUID = 4468828482344119321L;
protected ZkListener() {
super();
}
/**
* @see com.comcast.viper.flume2storm.zookeeper.ZkClientListener#initialize()
*/
@Override
public void initialize() {
// Making directory structure
try {
LOG.debug("Enforcing base path ({}) existence...", servicePath);
new ZkOperation(zkClient, servicePath).createNode(new ZkNodeCreationArg().setCreateHierarchy(true)
.setCreateMode(CreateMode.PERSISTENT));
getServiceInstances();
} catch (final Exception e) {
LOG.error("Failed to setup ZooKeeper connection", e);
}
// Registering
// TODO if any?
}
/**
* @see com.comcast.viper.flume2storm.zookeeper.ZkClientListener#terminate()
*/
@Override
public void terminate() {
// Nothing to do
}
/**
* @see com.comcast.viper.flume2storm.zookeeper.ZkClientListener#onConnection()
*/
@Override
public void onConnection() {
// Nothing to do
}
/**
* @see com.comcast.viper.flume2storm.zookeeper.ZkClientListener#onDisconnection()
*/
@Override
public void onDisconnection() {
// Nothing to do
}
}
/**
* Updates the list of active {@link ServiceProvider} servers, and sets a
* watch so that we get notified if the list changes
*/
protected synchronized void getServiceInstances() {
if (!zkClient.getState().isConnected()) {
return;
}
final Watcher watcher = new Watcher() {
@Override
public void process(final WatchedEvent event) {
LOG.debug("Service node watch triggered with event: {}", event);
getServiceInstances();
}
};
try {
final List<String> res = new ZkOperation(zkClient, servicePath).getChildren(watcher);
final Collection<SP> newList = new ArrayList<SP>();
for (final String child : res) {
final String childPath = ZkUtilies.buildZkPath(servicePath, child);
try {
final byte[] bytes = new ZkOperation(zkClient, childPath).getData();
newList.add(serviceProviderSerialization.deserialize(bytes));
} catch (final KeeperException.NoNodeException nne) {
// Programming note: The node was removed been the time we
// got the
// list and the parsing time... no big deal
LOG.debug("znode {} disappear: {}", childPath, nne.getLocalizedMessage());
} catch (final Exception e) {
LOG.warn("Failed to deserialize znode " + childPath, e);
}
}
serviceProviderManager.set(newList);
} catch (final Exception e) {
if (zkClient.getState().isConnected()) {
LOG.error("Failed to get service instances", e);
}
// Otherwise, we might have a clue why this is failing! :)
}
}
}