package org.togglz.zookeeper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.curator.framework.CuratorFramework;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.togglz.core.Feature;
import org.togglz.core.repository.FeatureState;
import org.togglz.core.repository.StateRepository;
import org.togglz.core.util.FeatureStateStorageWrapper;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.togglz.core.util.FeatureStateStorageWrapper.featureStateForWrapper;
public class ZookeeperStateRepository implements StateRepository, TreeCacheListener {
private static final Logger log = LoggerFactory.getLogger(ZookeeperStateRepository.class);
private static final ObjectMapper objectMapper = new ObjectMapper();
private TreeCache treeCache;
private ConcurrentMap<String, FeatureStateStorageWrapper> states;
protected final CuratorFramework curatorFramework;
protected final String featuresZnode;
// the initialization happens in another thread asynchronously.
// rather than wait for
protected CountDownLatch initializationLatch = new CountDownLatch(1);
private ZookeeperStateRepository(Builder builder) throws Exception {
this.curatorFramework = builder.curatorFramework;
this.featuresZnode = builder.featuresZnode;
initializeFeaturePath();
initializeStateCache();
}
private void initializeFeaturePath() {
try {
curatorFramework.createContainers(featuresZnode);
} catch (Exception e) {
throw new RuntimeException("couldn't initialize the zookeeper state repository", e);
}
}
private void initializeStateCache() throws Exception {
states = new ConcurrentHashMap<>();
// the treecache will keep a copy of the data in memory along with
// setting up watchers for addition / removal of nodes
treeCache = new TreeCache(curatorFramework, featuresZnode);
treeCache.getListenable().addListener(this);
treeCache.start();
long startTime = System.nanoTime();
log.info("Waiting for zookeeper state to be fully read");
initializationLatch.await();
long duration = System.nanoTime() - startTime;
log.debug("Initizlied the zookeeper state repository in {} ms", TimeUnit.NANOSECONDS.toMillis(duration));
}
@Override
public FeatureState getFeatureState(Feature feature) {
FeatureStateStorageWrapper wrapper = states.get(feature.name());
if (wrapper != null) {
return featureStateForWrapper(feature, wrapper);
}
return null;
}
@Override
public void setFeatureState(FeatureState featureState) {
FeatureStateStorageWrapper wrapper = FeatureStateStorageWrapper.wrapperForFeatureState(featureState);
try {
String json = objectMapper.writeValueAsString(wrapper);
String path = featuresZnode + "/" + featureState.getFeature().name();
curatorFramework.createContainers(path);
curatorFramework.setData().forPath(path, json.getBytes("UTF-8"));
states.put(featureState.getFeature().name(), wrapper);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent event) throws Exception {
String featureName;
ChildData eventData = event.getData();
switch (event.getType()) {
case NODE_ADDED:
String addedPath = eventData.getPath();
if (pathHasAFeatureInIt(addedPath)) {
featureName = getFeatureNameFromPath(addedPath);
if (featureName.contains("/")) {
break;
}
if (eventData.getData().length > 0) {
FeatureStateStorageWrapper featureState = objectMapper.readValue(eventData.getData(), FeatureStateStorageWrapper.class);
states.put(featureName, featureState);
}
}
break;
case NODE_UPDATED:
String updatedPath = eventData.getPath();
if (pathHasAFeatureInIt(updatedPath)) {
featureName = getFeatureNameFromPath(updatedPath);
FeatureStateStorageWrapper featureState = objectMapper.readValue(eventData.getData(), FeatureStateStorageWrapper.class);
states.put(featureName, featureState);
}
break;
case NODE_REMOVED:
String removedPath = eventData.getPath();
featureName = removedPath;
if (featureName.contains("/")) {
break;
}
states.remove(featureName);
break;
case INITIALIZED:
initializationLatch.countDown();
default:
break;
}
}
private String getFeatureNameFromPath(String updatedPath) {
String featureName;
featureName = updatedPath.substring(featuresZnode.length() + 1);
return featureName;
}
private boolean pathHasAFeatureInIt(String updatedPath) {
return updatedPath.length() > featuresZnode.length();
}
public static Builder newBuilder(CuratorFramework curatorFramework, String featuresZnode) {
return new Builder(curatorFramework, featuresZnode);
}
public static class Builder {
private final CuratorFramework curatorFramework;
private final String featuresZnode;
public Builder(CuratorFramework curatorFramework, String featuresZnode) {
this.curatorFramework = curatorFramework;
this.featuresZnode = featuresZnode;
}
public ZookeeperStateRepository build() throws Exception {
return new ZookeeperStateRepository(this);
}
}
}