/*
* Copyright © 2014 Cask Data, Inc.
*
* 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 co.cask.cdap.common.zookeeper.store;
import co.cask.cdap.api.common.Bytes;
import co.cask.cdap.common.conf.AbstractPropertyStore;
import co.cask.cdap.common.conf.PropertyUpdater;
import co.cask.cdap.common.io.Codec;
import co.cask.cdap.common.zookeeper.ZKExtOperations;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.apache.twill.common.Threads;
import org.apache.twill.zookeeper.NodeData;
import org.apache.twill.zookeeper.ZKClient;
import org.apache.twill.zookeeper.ZKClients;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Set;
/**
* This class uses ZK for storing properties/configures. It provides update methods for updating properties,
* and listener methods for watching for changes in properties.
*
* TODO: Unify this and SharedResourceCache in security module.
*
* @param <T> Type of property object
*/
public final class ZKPropertyStore<T> extends AbstractPropertyStore<T> {
private static final Logger LOG = LoggerFactory.getLogger(ZKPropertyStore.class);
private static final int MAX_ZK_FAILURE_RETRIES = 10;
private final ZKClient zkClient;
private final Codec<T> codec;
private final Set<String> watchedSet;
/**
* Creates an instance of {@link ZKPropertyStore}.
*
* @param zkClient client for interacting with ZooKeeper. Nodes will be created at root represented by this ZKClient.
* @param codec The codec for encode/decode property
*/
public static <T> ZKPropertyStore<T> create(ZKClient zkClient, Codec<T> codec) {
return new ZKPropertyStore<>(zkClient, codec);
}
/**
* Creates an instance of {@link ZKPropertyStore} with nodes created under the given namespace.
*
* @param zkClient client for interacting with ZooKeeper
* @param namespace Namespace for zk nodes to reside in
* @param codec The codec for encode/decode property
*/
public static <T> ZKPropertyStore<T> create(ZKClient zkClient, String namespace, Codec<T> codec) {
return new ZKPropertyStore<>(ZKClients.namespace(zkClient, namespace), codec);
}
/**
* Constructor.
*/
private ZKPropertyStore(ZKClient zkClient, Codec<T> codec) {
this.zkClient = zkClient;
this.codec = codec;
this.watchedSet = Sets.newHashSet();
}
@Override
public ListenableFuture<T> update(String name, PropertyUpdater<T> updater) {
return ZKExtOperations.updateOrCreate(zkClient, getPath(name), updater, codec);
}
@Override
public ListenableFuture<T> set(String name, T property) {
try {
return ZKExtOperations.setOrCreate(zkClient, getPath(name),
codec.encode(property), property, MAX_ZK_FAILURE_RETRIES);
} catch (IOException e) {
return Futures.immediateFailedFuture(e);
}
}
@Override
protected synchronized boolean listenerAdded(String name) {
if (watchedSet.add(name)) {
// Start watching for node change and maintain cached value.
// Invocation of listener would be triggered inside ZK callback.
existsAndWatch(name);
return false;
}
// Invoke it with the cached property if available when first added.
// If no cache value exists, meaning either property was removed or still pending for update for the first time
// For first case, no need to invoke listener. For second case, when the cache get updated, the newly
// added listener would get triggered
return true;
}
private String getPath(String name) {
return "/" + name;
}
private void getDataAndWatch(final String name) {
Futures.addCallback(zkClient.getData(getPath(name), new Watcher() {
@Override
public void process(WatchedEvent event) {
if (isClosed()) {
return;
}
if (event.getType() == Event.EventType.NodeDeleted) {
existsAndWatch(name);
} else {
getDataAndWatch(name);
}
}
}), new FutureCallback<NodeData>() {
@Override
public void onSuccess(NodeData result) {
byte[] data = result.getData();
if (data == null) {
updateAndNotify(name, null);
} else {
try {
updateAndNotify(name, codec.decode(data));
} catch (IOException e) {
LOG.error("Failed to decode property data for {}: {}", name, Bytes.toStringBinary(data), e);
notifyError(name, e);
}
}
}
@Override
public void onFailure(Throwable t) {
if (t instanceof KeeperException.NoNodeException) {
// If node not exists, watch for exists.
existsAndWatch(name);
} else {
LOG.error("Failed to get property data for {}", name, t);
notifyError(name, t);
}
}
}, Threads.SAME_THREAD_EXECUTOR);
}
private void existsAndWatch(final String name) {
Futures.addCallback(zkClient.exists(getPath(name), new Watcher() {
@Override
public void process(WatchedEvent event) {
if (isClosed()) {
return;
}
// If the event is not node created, meaning the node was existed.
// Hence getDataAndWatch should be handling that case already
if (event.getType() == Event.EventType.NodeCreated) {
getDataAndWatch(name);
}
}
}), new FutureCallback<Stat>() {
@Override
public void onSuccess(Stat result) {
// If the node exists, call getData. Otherwise, the watcher should handle the case when the node is created
if (result != null) {
getDataAndWatch(name);
}
}
@Override
public void onFailure(Throwable t) {
LOG.error("Failed to check exists for property data for {}", name, t);
notifyError(name, t);
}
}, Threads.SAME_THREAD_EXECUTOR);
}
}