aboutsummaryrefslogtreecommitdiffstats
path: root/zkfacade/src/main
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /zkfacade/src/main
Publish
Diffstat (limited to 'zkfacade/src/main')
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/CompletionTimeoutException.java14
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/Curator.java317
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/CuratorCompletionWaiter.java95
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/DNSResolvingFixerZooKeeperFactory.java45
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/NodeCacheWrapper.java53
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/PathChildrenCacheWrapper.java55
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/mock/MemoryFileSystem.java213
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/mock/MockCurator.java1135
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/package-info.java6
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/recipes/CuratorCounter.java67
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/recipes/CuratorLock.java75
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/recipes/CuratorLockException.java21
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/recipes/package-info.java5
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorCreateOperation.java49
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorDeleteOperation.java36
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorOperation.java35
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorOperations.java29
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorSetDataOperation.java40
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorTransaction.java74
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/package-info.java9
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/zookeeper/ZooKeeperServer.java124
-rw-r--r--zkfacade/src/main/java/com/yahoo/vespa/zookeeper/package-info.java5
-rw-r--r--zkfacade/src/main/java/org/apache/curator/Dummy.java12
-rw-r--r--zkfacade/src/main/java/org/apache/curator/framework/api/package-info.java5
-rw-r--r--zkfacade/src/main/java/org/apache/curator/framework/api/transaction/package-info.java5
-rw-r--r--zkfacade/src/main/java/org/apache/curator/framework/listen/package-info.java5
-rw-r--r--zkfacade/src/main/java/org/apache/curator/framework/package-info.java5
-rw-r--r--zkfacade/src/main/java/org/apache/curator/framework/recipes/atomic/package-info.java5
-rw-r--r--zkfacade/src/main/java/org/apache/curator/framework/recipes/barriers/package-info.java5
-rw-r--r--zkfacade/src/main/java/org/apache/curator/framework/recipes/cache/package-info.java5
-rw-r--r--zkfacade/src/main/java/org/apache/curator/framework/recipes/locks/package-info.java5
-rw-r--r--zkfacade/src/main/java/org/apache/curator/framework/state/package-info.java5
-rw-r--r--zkfacade/src/main/java/org/apache/curator/package-info.java5
-rw-r--r--zkfacade/src/main/java/org/apache/curator/retry/package-info.java5
-rw-r--r--zkfacade/src/main/java/org/apache/zookeeper/data/package-info.java6
-rw-r--r--zkfacade/src/main/java/org/apache/zookeeper/package-info.java6
-rwxr-xr-xzkfacade/src/main/sh/zkcat63
-rwxr-xr-xzkfacade/src/main/sh/zkctl63
-rwxr-xr-xzkfacade/src/main/sh/zkls63
39 files changed, 2770 insertions, 0 deletions
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/CompletionTimeoutException.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/CompletionTimeoutException.java
new file mode 100644
index 00000000000..fe2c8b3012b
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/CompletionTimeoutException.java
@@ -0,0 +1,14 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class CompletionTimeoutException extends RuntimeException {
+
+ public CompletionTimeoutException(String errorMessage) {
+ super(errorMessage);
+ }
+
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/Curator.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/Curator.java
new file mode 100644
index 00000000000..c037f7e8ae2
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/Curator.java
@@ -0,0 +1,317 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator;
+
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.path.Path;
+import com.yahoo.vespa.curator.recipes.CuratorCounter;
+import com.yahoo.vespa.zookeeper.ZooKeeperServer;
+import org.apache.curator.RetryPolicy;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.api.transaction.CuratorTransaction;
+import org.apache.curator.framework.api.transaction.CuratorTransactionFinal;
+import org.apache.curator.framework.recipes.atomic.DistributedAtomicLong;
+import org.apache.curator.framework.recipes.cache.ChildData;
+import org.apache.curator.framework.recipes.cache.NodeCacheListener;
+import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
+import org.apache.curator.framework.recipes.locks.InterProcessLock;
+import org.apache.curator.framework.recipes.locks.InterProcessMutex;
+import org.apache.curator.framework.state.ConnectionState;
+import org.apache.curator.framework.state.ConnectionStateListener;
+import org.apache.curator.retry.ExponentialBackoffRetry;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Curator interface for Vespa.
+ * This contains method for constructing common recipes and utilities as well as
+ * a small wrapper API for common operations which uses typed paths and avoids throwing checked exceptions.
+ * <p>
+ * There is a mock implementation in MockCurator.
+ *
+ * @author vegardh
+ * @author bratseth
+ */
+public class Curator {
+
+ private static final long UNKNOWN_HOST_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(30);
+ private static final int ZK_SESSION_TIMEOUT = 30000;
+ private static final int ZK_CONNECTION_TIMEOUT = 30000;
+
+ private static final int baseSleepTime = 1000; //ms
+ private static final int maxRetries = 10;
+
+ private final CuratorFramework curatorFramework;
+ protected final RetryPolicy retryPolicy;
+
+ private final String connectionSpec;
+ private final int serverCount;
+
+ /** Creates a curator instance from a comma-separated string of ZooKeeper host names */
+ public static Curator create(String connectionSpec) {
+ return new Curator(connectionSpec);
+ }
+
+ // Depend on ZooKeeperServer to make sure it is started first
+ @Inject
+ public Curator(ConfigserverConfig configserverConfig, ZooKeeperServer server) {
+ this(createConnectionSpec(configserverConfig));
+ }
+
+ private static String createConnectionSpec(ConfigserverConfig config) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < config.zookeeperserver().size(); i++) {
+ ConfigserverConfig.Zookeeperserver server = config.zookeeperserver(i);
+ sb.append(server.hostname());
+ sb.append(":");
+ sb.append(server.port());
+ if (i < config.zookeeperserver().size() - 1) {
+ sb.append(",");
+ }
+ }
+ return sb.toString();
+ }
+
+ private Curator(String connectionSpec) {
+ this.connectionSpec = connectionSpec;
+ this.serverCount = connectionSpec.split(",").length;
+ validateConnectionSpec(connectionSpec);
+ retryPolicy = new ExponentialBackoffRetry(baseSleepTime, maxRetries);
+ curatorFramework = CuratorFrameworkFactory.builder()
+ .retryPolicy(retryPolicy)
+ .sessionTimeoutMs(ZK_SESSION_TIMEOUT)
+ .connectionTimeoutMs(ZK_CONNECTION_TIMEOUT)
+ .connectString(connectionSpec)
+ .zookeeperFactory(new DNSResolvingFixerZooKeeperFactory(UNKNOWN_HOST_TIMEOUT_MILLIS))
+ .build();
+ addFakeListener();
+ curatorFramework.start();
+ }
+
+ protected Curator() {
+ this.connectionSpec = "";
+ this.serverCount = 0;
+ retryPolicy = new ExponentialBackoffRetry(baseSleepTime, maxRetries);
+ curatorFramework = null;
+ }
+
+ private static void validateConnectionSpec(String connectionSpec) {
+ if (connectionSpec == null || connectionSpec.isEmpty()) {
+ throw new IllegalArgumentException(String.format("Connections spec '%s' is not valid", connectionSpec));
+ }
+ }
+
+ /** Returns the number of zooKeeper servers in this cluster */
+ public int serverCount() { return serverCount; }
+
+ /** Returns a comma-separated list of the zookeeper servers in this cluster */
+ public String connectionSpec() { return connectionSpec; }
+
+ /** For internal use; prefer creating a {@link CuratorCounter} */
+ public DistributedAtomicLong createAtomicCounter(String path) {
+ return new DistributedAtomicLong(curatorFramework, path, new ExponentialBackoffRetry(baseSleepTime, maxRetries));
+ }
+
+ /** For internal use; prefer creating a {@link com.yahoo.vespa.curator.recipes.CuratorLock} */
+ public InterProcessLock createMutex(String lockPath) {
+ return new InterProcessMutex(curatorFramework, lockPath);
+ }
+
+ // To avoid getting warning in log, see ticket 6389740
+ private void addFakeListener() {
+ curatorFramework.getConnectionStateListenable().addListener(new ConnectionStateListener() {
+ @Override
+ public void stateChanged(CuratorFramework curatorFramework, ConnectionState connectionState) {
+ // empty, not needed now
+ }
+ });
+ }
+
+ public CompletionWaiter getCompletionWaiter(Path waiterPath, int numMembers, String id) {
+ return CuratorCompletionWaiter.create(curatorFramework, waiterPath, numMembers, id);
+ }
+
+ public CompletionWaiter createCompletionWaiter(Path parentPath, String waiterNode, int numMembers, String id) {
+ return CuratorCompletionWaiter.createAndInitialize(this, parentPath, waiterNode, numMembers, id);
+ }
+
+ /** Creates a listenable cache which keeps in sync with changes to all the immediate children of a path */
+ public DirectoryCache createDirectoryCache(String path, boolean cacheData, boolean dataIsCompressed, ExecutorService executorService) {
+ return new PathChildrenCacheWrapper(framework(), path, cacheData, dataIsCompressed, executorService);
+ }
+
+ /** Creates a listenable cache which keeps in sync with changes to a given node */
+ public FileCache createFileCache(String path, boolean dataIsCompressed) {
+ return new NodeCacheWrapper(framework(), path, dataIsCompressed);
+ }
+
+ /** A convenience method which returns whether the given path exists */
+ public boolean exists(Path path) {
+ try {
+ return framework().checkExists().forPath(path.getAbsolute()) != null;
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Could not check existence of " + path.getAbsolute(), e);
+ }
+ }
+
+ /**
+ * A convenience method which sets some content at a path.
+ * If the path and any of its parents does not exists they are created.
+ */
+ public void set(Path path, byte[] data) {
+ String absolutePath = path.getAbsolute();
+ try {
+ if ( ! exists(path))
+ framework().create().creatingParentsIfNeeded().forPath(absolutePath, data);
+ else
+ framework().setData().forPath(absolutePath, data);
+ } catch (Exception e) {
+ throw new RuntimeException("Could not set data at " + absolutePath, e);
+ }
+ }
+
+ /**
+ * Creates an empty node at a path, creating any parents as necessary.
+ * If the node already exists nothing is done.
+ */
+ public void create(Path path) {
+ if (exists(path)) return;
+
+ String absolutePath = path.getAbsolute();
+ try {
+ framework().create().creatingParentsIfNeeded().forPath(absolutePath);
+ } catch (org.apache.zookeeper.KeeperException.NodeExistsException e) {
+ // Path created between exists() and create() call, do nothing
+ } catch (Exception e) {
+ throw new RuntimeException("Could not create " + absolutePath, e);
+ }
+ }
+
+ /**
+ * Creates all the given paths in a single transaction. Any paths which already exists are ignored.
+ */
+ public void createAtomically(Path... paths) {
+ try {
+ CuratorTransaction transaction = framework().inTransaction();
+ for (Path path : paths) {
+ if ( ! exists(path)) {
+ transaction = transaction.create().forPath(path.getAbsolute()).and();
+ }
+ }
+ ((CuratorTransactionFinal)transaction).commit();
+ } catch (Exception e) {
+ throw new RuntimeException("Could not create " + Arrays.toString(paths), e);
+ }
+ }
+
+ /**
+ * Deletes the given path and any children it may have.
+ * If the path does not exists nothing is done.
+ */
+ public void delete(Path path) {
+ if ( ! exists(path)) return;
+
+ try {
+ framework().delete().guaranteed().deletingChildrenIfNeeded().forPath(path.getAbsolute());
+ } catch (Exception e) {
+ throw new RuntimeException("Could not delete " + path.getAbsolute(), e);
+ }
+ }
+
+ /**
+ * Returns the children at the given path.
+ * If the path does not exist or have no children an empty list (never null) is returned.
+ */
+ public List<String> getChildren(Path path) {
+ if ( ! exists(path)) return Collections.emptyList();
+
+ try {
+ return framework().getChildren().forPath(path.getAbsolute());
+ } catch (Exception e) {
+ throw new RuntimeException("Could not get children of " + path.getAbsolute(), e);
+ }
+ }
+
+ /**
+ * Returns the data at the given path, which may be a zero-length buffer if the node exists but have no data.
+ * Empty is returned if the path does not exist.
+ */
+ public Optional<byte[]> getData(Path path) {
+ if ( ! exists(path)) return Optional.empty();
+
+ try {
+ return Optional.of(framework().getData().forPath(path.getAbsolute()));
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Could not get data at " + path.getAbsolute(), e);
+ }
+ }
+
+ /** Returns the curator framework API */
+ public CuratorFramework framework() {
+ return curatorFramework;
+ }
+
+ /**
+ * Interface for waiting for completion of an operation
+ */
+ public interface CompletionWaiter {
+
+ /**
+ * Awaits completion of something. Blocks until an implementation defined
+ * condition has been met.
+ *
+ * @param timeout timeout for blocking await call.
+ * @throws CompletionTimeoutException if timeout is reached without completion.
+ */
+ void awaitCompletion(Duration timeout);
+
+ /**
+ * Notify completion of something. This method does not block and is called by clients
+ * that want to notify the completion waiter that something has completed.
+ */
+ void notifyCompletion();
+
+ }
+
+ /**
+ * A listenable cache of all the immediate children of a curator path.
+ * This wraps the Curator PathChildrenCache recipe to allow us to mock it.
+ */
+ public interface DirectoryCache {
+
+ void start();
+
+ void addListener(PathChildrenCacheListener listener);
+
+ List<ChildData> getCurrentData();
+
+ void close();
+
+ }
+
+ /**
+ * A listenable cache of the content of a single curator path.
+ * This wraps the Curator NodeCache recipe to allow us to mock it.
+ */
+ public interface FileCache {
+
+ void start();
+
+ void addListener(NodeCacheListener listener);
+
+ ChildData getCurrentData();
+
+ void close();
+
+ }
+
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/CuratorCompletionWaiter.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/CuratorCompletionWaiter.java
new file mode 100644
index 00000000000..69923c25998
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/CuratorCompletionWaiter.java
@@ -0,0 +1,95 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator;
+
+import com.yahoo.log.LogLevel;
+import com.yahoo.path.Path;
+import org.apache.curator.framework.CuratorFramework;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+
+/**
+ * Implementation of a Barrier that handles the case where more than number of members can call synchronize. If
+ * the number of members that synchronize exceed the expected number, the other members are immediately allowed
+ * to pass through the barrier.
+ *
+ * @author vegardh, lulf
+ * @since 5.1
+ */
+class CuratorCompletionWaiter implements Curator.CompletionWaiter {
+
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(CuratorCompletionWaiter.class.getName());
+ private final CuratorFramework curator;
+ private final String barrierPath;
+ private final String myId;
+ private final int memberQty;
+ private final Clock clock;
+
+ CuratorCompletionWaiter(int barrierMembers, CuratorFramework curator, String barrierPath, String myId, Clock clock) {
+ this.memberQty = barrierMembers;
+ this.myId = barrierPath + "/" + myId;
+ this.curator = curator;
+ this.barrierPath = barrierPath;
+ this.clock = clock;
+ }
+
+ @Override
+ public void awaitCompletion(Duration timeout) {
+ List<String> respondents;
+ try {
+ log.log(LogLevel.DEBUG, "Synchronizing on barrier " + barrierPath);
+ respondents = awaitInternal(timeout);
+ log.log(LogLevel.DEBUG, "Done synchronizing on barrier " + barrierPath);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ if (respondents.size() < memberQty) {
+ throw new CompletionTimeoutException("Timed out waiting for peer config servers to complete operation. Got response from: " + respondents + ". Timeout passed as argument was " + timeout.toMillis() + " ms");
+ }
+ }
+
+ private List<String> awaitInternal(Duration timeout) throws Exception {
+ Instant endTime = clock.instant().plus(timeout);
+ List<String> respondents;
+ do {
+ respondents = curator.getChildren().forPath(barrierPath);
+ if (respondents.size() >= memberQty) {
+ break;
+ }
+ Thread.sleep(100);
+ } while (clock.instant().isBefore(endTime));
+ return respondents;
+ }
+
+
+ @Override
+ public void notifyCompletion() {
+ try {
+ notifyInternal();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void notifyInternal() throws Exception {
+ curator.create().forPath(myId);
+ }
+
+ @Override
+ public String toString() {
+ return "'" + barrierPath + "', " + memberQty + " members";
+ }
+
+ public static Curator.CompletionWaiter create(CuratorFramework curator, Path barrierPath, int numMembers, String id) {
+ return new CuratorCompletionWaiter(numMembers, curator, barrierPath.getAbsolute(), id, Clock.systemUTC());
+ }
+
+ public static Curator.CompletionWaiter createAndInitialize(Curator curator, Path parentPath, String waiterNode, int numMembers, String id) {
+ Path waiterPath = parentPath.append(waiterNode);
+ curator.delete(waiterPath);
+ curator.createAtomically(parentPath, waiterPath);
+ return new CuratorCompletionWaiter(numMembers, curator.framework(), waiterPath.getAbsolute(), id, Clock.systemUTC());
+ }
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/DNSResolvingFixerZooKeeperFactory.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/DNSResolvingFixerZooKeeperFactory.java
new file mode 100644
index 00000000000..b0c966bc971
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/DNSResolvingFixerZooKeeperFactory.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator;
+
+import com.yahoo.log.LogLevel;
+import org.apache.curator.utils.DefaultZookeeperFactory;
+import org.apache.curator.utils.ZookeeperFactory;
+import org.apache.zookeeper.Watcher;
+import org.apache.zookeeper.ZooKeeper;
+
+import java.net.UnknownHostException;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+/**
+ * A ZooKeeper handle factory that handles unknown host exceptions.
+ *
+ * @author lulf
+ * @since 5.9
+ */
+class DNSResolvingFixerZooKeeperFactory implements ZookeeperFactory {
+
+ public static final long UNKNOWN_HOST_WAIT_TIME_MILLIS = TimeUnit.SECONDS.toMillis(10);
+
+ private static final Logger log = Logger.getLogger(DNSResolvingFixerZooKeeperFactory.class.getName());
+ private final DefaultZookeeperFactory zookeeperFactory;
+ private final long maxTimeout;
+ public DNSResolvingFixerZooKeeperFactory(long maxTimeout) {
+ this.maxTimeout = maxTimeout;
+ this.zookeeperFactory = new DefaultZookeeperFactory();
+ }
+ @Override
+ public ZooKeeper newZooKeeper(String connectString, int sessionTimeout, Watcher watcher, boolean canBeReadOnly) throws Exception {
+ long endTime = System.currentTimeMillis() + maxTimeout;
+ do {
+ try {
+ return zookeeperFactory.newZooKeeper(connectString, sessionTimeout, watcher, canBeReadOnly);
+ } catch (UnknownHostException e) {
+ log.log(LogLevel.WARNING, "Error creating ZooKeeper handle", e);
+ Thread.sleep(UNKNOWN_HOST_WAIT_TIME_MILLIS);
+ }
+ } while (System.currentTimeMillis() < endTime);
+ throw new RuntimeException("Error creating zookeeper handle within timeout");
+ }
+
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/NodeCacheWrapper.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/NodeCacheWrapper.java
new file mode 100644
index 00000000000..ffa551e2b1e
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/NodeCacheWrapper.java
@@ -0,0 +1,53 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator;
+
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.recipes.cache.ChildData;
+import org.apache.curator.framework.recipes.cache.NodeCache;
+import org.apache.curator.framework.recipes.cache.NodeCacheListener;
+
+import java.io.IOException;
+
+/**
+ * A file cache backed by a curator node cache.
+ *
+ * @author bratseth
+ */
+class NodeCacheWrapper implements Curator.FileCache {
+
+ private final NodeCache wrapped;
+
+ public NodeCacheWrapper(CuratorFramework curatorFramework, String path, boolean dataIsCompressed) {
+ wrapped = new NodeCache(curatorFramework, path, dataIsCompressed);
+ }
+
+ @Override
+ public void start() {
+ try {
+ wrapped.start(true);
+ } catch (Exception e) {
+ throw new IllegalStateException("Could not start the Curator cache", e);
+ }
+ }
+
+ @Override
+ public void addListener(NodeCacheListener listener) {
+ wrapped.getListenable().addListener(listener);
+
+ }
+
+ @Override
+ public ChildData getCurrentData() {
+ return wrapped.getCurrentData();
+ }
+
+ @Override
+ public void close() {
+ try {
+ wrapped.close();
+ } catch (IOException e) {
+ throw new RuntimeException("Exception closing curator cache", e);
+ }
+ }
+
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/PathChildrenCacheWrapper.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/PathChildrenCacheWrapper.java
new file mode 100644
index 00000000000..819cd235023
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/PathChildrenCacheWrapper.java
@@ -0,0 +1,55 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator;
+
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.recipes.cache.ChildData;
+import org.apache.curator.framework.recipes.cache.PathChildrenCache;
+import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * A directory cache backed by a curator path children cache.
+ *
+ * @author bratseth
+ */
+class PathChildrenCacheWrapper implements Curator.DirectoryCache {
+
+ private final PathChildrenCache wrapped;
+
+ public PathChildrenCacheWrapper(CuratorFramework curatorFramework, String path, boolean cacheData, boolean dataIsCompressed, ExecutorService executorService) {
+ wrapped = new PathChildrenCache(curatorFramework, path, cacheData, dataIsCompressed, executorService);
+ }
+
+ @Override
+ public void start() {
+ try {
+ wrapped.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);
+ } catch (Exception e) {
+ throw new IllegalStateException("Could not start the Curator cache", e);
+ }
+ }
+
+ @Override
+ public void addListener(PathChildrenCacheListener listener) {
+ wrapped.getListenable().addListener(listener);
+
+ }
+
+ @Override
+ public List<ChildData> getCurrentData() {
+ return wrapped.getCurrentData();
+ }
+
+ @Override
+ public void close() {
+ try {
+ wrapped.close();
+ } catch (IOException e) {
+ throw new RuntimeException("Exception closing curator cache", e);
+ }
+ }
+
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/mock/MemoryFileSystem.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/mock/MemoryFileSystem.java
new file mode 100644
index 00000000000..978827aebc2
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/mock/MemoryFileSystem.java
@@ -0,0 +1,213 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator.mock;
+
+import java.io.IOException;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.Paths;
+import java.nio.file.WatchService;
+import java.nio.file.attribute.UserPrincipalLookupService;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A simple in-memory "file system" useful for Curator caching/mocking.
+ *
+ * @author bratseth
+ */
+class MemoryFileSystem extends FileSystem {
+
+ private Node root = new Node(null, "");
+
+ @Override
+ public FileSystemProvider provider() {
+ throw new UnsupportedOperationException("Not implemented in MemoryFileSystem");
+ }
+
+ @Override
+ public void close() throws IOException {
+ }
+
+ @Override
+ public boolean isOpen() {
+ return true;
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return false;
+ }
+
+ @Override
+ public String getSeparator() {
+ return "/";
+ }
+
+ @Override
+ public Iterable<Path> getRootDirectories() {
+ return Collections.singleton(Paths.get("/"));
+ }
+
+ @Override
+ public Iterable<FileStore> getFileStores() {
+ throw new UnsupportedOperationException("Not implemented in MemoryFileSystem");
+ }
+
+ @Override
+ public Set<String> supportedFileAttributeViews() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Path getPath(String first, String... more) {
+ return Paths.get(first, more);
+ }
+
+ @Override
+ public PathMatcher getPathMatcher(String syntaxAndPattern) {
+ throw new UnsupportedOperationException("Not implemented in MemoryFileSystem");
+ }
+
+ @Override
+ public UserPrincipalLookupService getUserPrincipalLookupService() {
+ throw new UnsupportedOperationException("Not implemented in MemoryFileSystem");
+ }
+
+ @Override
+ public WatchService newWatchService() throws IOException {
+ throw new UnsupportedOperationException("Not implemented in MemoryFileSystem");
+ }
+
+ /** Returns the root of this file system */
+ public Node root() { return root; }
+
+ /** Replaces the directory root. This is used to implement transactional changes to the file system */
+ public void replaceRoot(Node newRoot) { this.root = newRoot; }
+
+ /**
+ * A node in this file system. Nodes may have children (a "directory"),
+ * content (a "file"), or both.
+ */
+ public static class Node implements Cloneable {
+
+ /** The parent of this node, or null if this is the root */
+ private final Node parent;
+
+ /** The local name of this node */
+ private final String name;
+
+ /** The content of this node. This buffer is effectively immutable. */
+ private byte[] content;
+
+ private Map<String, Node> children = new LinkedHashMap<>();
+
+ private Node(Node parent, String name) {
+ this(parent, name, new byte[0]);
+ }
+
+ private Node(Node parent, String name, byte[] content) {
+ this.parent = parent;
+ this.name = name;
+ this.content = Arrays.copyOf(content, content.length);
+ }
+
+ /** Returns a copy of the content of this node */
+ public byte[] getContent() { return Arrays.copyOf(content, content.length); }
+
+ /** Replaces the content of this file */
+ public void setContent(byte[] content) { this.content = Arrays.copyOf(content, content.length); }
+
+ /**
+ * Returns the node given by the path.
+ *
+ * @param create if true, any missing directories are created
+ * @return the node, or null if it does not exist
+ */
+ public Node getNode(Path path, boolean create) {
+ if (path.getNameCount() == 0 || path.toString().isEmpty()) return this;
+ String childName = path.getName(0).toString();
+ Node child = children.get(childName);
+ if (child == null) {
+ if (create)
+ child = add(childName);
+ else
+ return null;
+ }
+ // invariant: child exists
+
+ if (path.getNameCount() == 1)
+ return child;
+ else
+ return child.getNode(path.subpath(1, path.getNameCount()), create);
+ }
+
+ /** Returns the parent of this, or null if it is the root */
+ public Node parent() { return parent; }
+
+ public boolean isRoot() { return parent == null; }
+
+ /** Returns the local name of this node */
+ public String name() { return name; }
+
+ /** Adds an empty node to this and returns it. If it already exists this does nothing but return the node */
+ public Node add(String name) {
+ if (children.containsKey(name)) return children.get(name);
+
+ Node child = new Node(this, name);
+ children.put(name, child);
+ return child;
+ }
+
+ /**
+ * Adds a node to this. If it already exists it is replaced.
+ *
+ * @return the node which was replaced by this, or null if none
+ */
+ public Node add(Node node) {
+ return children.put(node.name(), node);
+ }
+
+ /**
+ * Removes the given child node of this, whether or not it has children.
+ * If it does not exists, this does nothing.
+ *
+ * @return the removed node, or null if none
+ */
+ public Node remove(String name) {
+ return children.remove(name);
+ }
+
+ /** Returns an unmodifiable map of the immediate children of this indexed by their local name */
+ public Map<String, Node> children() { return Collections.unmodifiableMap(children); }
+
+ @Override
+ public String toString() {
+ return "directory '" + name() + "'";
+ }
+
+ /** Returns a deep copy of this node and all nodes below it */
+ public Node clone() {
+ try {
+ Node clone = (Node)super.clone();
+ Map<String, Node> cloneChildren = new HashMap<>();
+ for (Map.Entry<String, Node> child : this.children.entrySet()) {
+ cloneChildren.put(child.getKey(), child.getValue().clone());
+ }
+ clone.children = cloneChildren;
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("Won't happen");
+ }
+ }
+
+ }
+
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/mock/MockCurator.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/mock/MockCurator.java
new file mode 100644
index 00000000000..cc5d9cc10fb
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/mock/MockCurator.java
@@ -0,0 +1,1135 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator.mock;
+
+import com.yahoo.collections.Pair;
+import com.yahoo.path.Path;
+import static com.yahoo.vespa.curator.mock.MemoryFileSystem.Node;
+
+import com.yahoo.vespa.curator.CompletionTimeoutException;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.curator.recipes.CuratorLockException;
+import org.apache.curator.CuratorZookeeperClient;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.api.ACLBackgroundPathAndBytesable;
+import org.apache.curator.framework.api.ACLCreateModeBackgroundPathAndBytesable;
+import org.apache.curator.framework.api.ACLPathAndBytesable;
+import org.apache.curator.framework.api.BackgroundCallback;
+import org.apache.curator.framework.api.BackgroundPathAndBytesable;
+import org.apache.curator.framework.api.BackgroundPathable;
+import org.apache.curator.framework.api.BackgroundVersionable;
+import org.apache.curator.framework.api.ChildrenDeletable;
+import org.apache.curator.framework.api.CreateBackgroundModeACLable;
+import org.apache.curator.framework.api.CreateBuilder;
+import org.apache.curator.framework.api.CuratorListener;
+import org.apache.curator.framework.api.CuratorWatcher;
+import org.apache.curator.framework.api.DeleteBuilder;
+import org.apache.curator.framework.api.ExistsBuilder;
+import org.apache.curator.framework.api.ExistsBuilderMain;
+import org.apache.curator.framework.api.GetACLBuilder;
+import org.apache.curator.framework.api.GetChildrenBuilder;
+import org.apache.curator.framework.api.GetDataBuilder;
+import org.apache.curator.framework.api.GetDataWatchBackgroundStatable;
+import org.apache.curator.framework.api.PathAndBytesable;
+import org.apache.curator.framework.api.Pathable;
+import org.apache.curator.framework.api.ProtectACLCreateModePathAndBytesable;
+import org.apache.curator.framework.api.SetACLBuilder;
+import org.apache.curator.framework.api.SetDataBackgroundVersionable;
+import org.apache.curator.framework.api.SetDataBuilder;
+import org.apache.curator.framework.api.SyncBuilder;
+import org.apache.curator.framework.api.UnhandledErrorListener;
+import org.apache.curator.framework.api.WatchPathable;
+import org.apache.curator.framework.api.Watchable;
+import org.apache.curator.framework.api.transaction.CuratorTransaction;
+import org.apache.curator.framework.api.transaction.CuratorTransactionBridge;
+import org.apache.curator.framework.api.transaction.CuratorTransactionFinal;
+import org.apache.curator.framework.api.transaction.CuratorTransactionResult;
+import org.apache.curator.framework.api.transaction.TransactionCheckBuilder;
+import org.apache.curator.framework.api.transaction.TransactionCreateBuilder;
+import org.apache.curator.framework.api.transaction.TransactionDeleteBuilder;
+import org.apache.curator.framework.api.transaction.TransactionSetDataBuilder;
+import org.apache.curator.framework.imps.CuratorFrameworkState;
+import org.apache.curator.framework.listen.Listenable;
+import org.apache.curator.framework.recipes.atomic.AtomicStats;
+import org.apache.curator.framework.recipes.atomic.AtomicValue;
+import org.apache.curator.framework.recipes.atomic.DistributedAtomicLong;
+import org.apache.curator.framework.recipes.cache.ChildData;
+import org.apache.curator.framework.recipes.cache.NodeCacheListener;
+import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
+import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
+import org.apache.curator.framework.recipes.locks.InterProcessLock;
+import org.apache.curator.framework.recipes.locks.InterProcessSemaphoreMutex;
+import org.apache.curator.framework.state.ConnectionStateListener;
+import org.apache.curator.utils.*;
+import org.apache.zookeeper.CreateMode;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.Watcher;
+import org.apache.zookeeper.data.ACL;
+import org.apache.zookeeper.data.Stat;
+
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * <p>A <b>non thread safe</b> mock of the curator API.
+ * The methods are implemented lazily, due to laziness.
+ * You may trigger an UnsupportedOperationException, and in some cases a NullPointerException in using
+ * this, which means additional functionality is needed.
+ * Due to the "fluent API" style of Curator managing to break JavaDoc at a fundamental level, there is no
+ * documentation on the contract of each method. The behavior here is deduced by observing what using code exists
+ * and peeking at the Curator code. It may be incorrect in some corner cases.</p>
+ *
+ * <p>Contains some code from PathUtils in ZooKeeper, licensed under the Apache 2.0 license.</p>
+ *
+ * @author bratseth
+ */
+public class MockCurator extends Curator {
+
+ public boolean timeoutOnLock = false;
+ public boolean throwExceptionOnLock = false;
+ private boolean shouldTimeoutOnEnter = false;
+ private int monotonicallyIncreasingNumber = 0;
+ private final boolean stableOrdering;
+
+ /** The file system used by this mock to store zookeeper files and directories */
+ private final MemoryFileSystem fileSystem = new MemoryFileSystem();
+
+ /** Atomic counters. A more accurate mock would store these as files in the file system */
+ private final Map<String, MockAtomicCounter> atomicCounters = new HashMap<>();
+
+ /** Listeners to changes to a particular path */
+ private final ListenerMap listeners = new ListenerMap();
+
+ private final CuratorFramework curatorFramework;
+
+ /** Creates a mock curator with stable ordering */
+ public MockCurator() {
+ this(true);
+ }
+
+ /**
+ * Creates a mock curator
+ *
+ * @param stableOrdering if true children of a node are returned in the same order each time they are queries.
+ * This is not what ZooKeeper does.
+ */
+ public MockCurator(boolean stableOrdering) {
+ this.stableOrdering = stableOrdering;
+ curatorFramework = new MockCuratorFramework();
+ curatorFramework.start();
+ }
+
+ /** Returns a started curator framework */
+ public CuratorFramework framework() { return curatorFramework; }
+
+ /** Returns an atomic counter in this, or empty if no such counter is created */
+ public Optional<DistributedAtomicLong> counter(String path) {
+ return Optional.ofNullable(atomicCounters.get(path));
+ }
+
+ // ----- Start of adaptor methods from Curator to the mock file system -----
+
+ /** Creates a node below the given directory root */
+ private String createNode(String pathString, byte[] content, boolean createParents, CreateMode createMode, Node root, Listeners listeners)
+ throws KeeperException.NodeExistsException, KeeperException.NoNodeException {
+ validatePath(pathString);
+ Path path = Path.fromString(pathString);
+ if (path.isRoot()) return "/"; // the root already exists
+ Node parent = root.getNode(Paths.get(path.getParentPath().toString()), createParents);
+ String name = nodeName(path.getName(), createMode);
+
+ if (parent == null)
+ throw new KeeperException.NoNodeException(path.getParentPath().toString());
+ if (parent.children().containsKey(path.getName()))
+ throw new KeeperException.NodeExistsException(path.toString());
+
+ parent.add(name).setContent(content);
+ String nodePath = "/" + path.getParentPath().toString() + "/" + name;
+ listeners.notify(Path.fromString(nodePath), content, PathChildrenCacheEvent.Type.CHILD_ADDED);
+ return nodePath;
+ }
+
+ /** Deletes a node below the given directory root */
+ private void deleteNode(String pathString, boolean deleteChildren, Node root, Listeners listeners)
+ throws KeeperException.NoNodeException, KeeperException.NotEmptyException {
+ validatePath(pathString);
+ Path path = Path.fromString(pathString);
+ Node parent = root.getNode(Paths.get(path.getParentPath().toString()), false);
+ if (parent == null) throw new KeeperException.NoNodeException(path.toString());
+ Node node = parent.children().get(path.getName());
+ if ( ! node.children().isEmpty() && ! deleteChildren)
+ throw new KeeperException.NotEmptyException(path.toString());
+ parent.remove(path.getName());
+ listeners.notify(path, new byte[0], PathChildrenCacheEvent.Type.CHILD_REMOVED);
+ }
+
+ /** Returns the data of a node */
+ private byte[] getData(String pathString, Node root) throws KeeperException.NoNodeException {
+ validatePath(pathString);
+ return getNode(pathString, root).getContent();
+ }
+
+ /** sets the data of an existing node */
+ private void setData(String pathString, byte[] content, Node root, Listeners listeners)
+ throws KeeperException.NoNodeException {
+ validatePath(pathString);
+ getNode(pathString, root).setContent(content);
+ listeners.notify(Path.fromString(pathString), content, PathChildrenCacheEvent.Type.CHILD_UPDATED);
+ }
+
+ private List<String> getChildren(String path, Node root) throws KeeperException.NoNodeException {
+ validatePath(path);
+ Node node = root.getNode(Paths.get(path), false);
+ if (node == null) throw new KeeperException.NoNodeException(path);
+ List<String> children = new ArrayList<>(node.children().keySet());
+ if (! stableOrdering)
+ Collections.shuffle(children);
+ return children;
+ }
+
+ private boolean exists(String path, Node root) {
+ validatePath(path);
+ Node parent = root.getNode(Paths.get(Path.fromString(path).getParentPath().toString()), false);
+ if (parent == null) return false;
+ Node node = parent.children().get(Path.fromString(path).getName());
+ return node != null;
+ }
+
+ /** Returns a node or throws the appropriate exception if it doesn't exist */
+ private Node getNode(String pathString, Node root) throws KeeperException.NoNodeException {
+ validatePath(pathString);
+ Path path = Path.fromString(pathString);
+ Node parent = root.getNode(Paths.get(path.getParentPath().toString()), false);
+ if (parent == null) throw new KeeperException.NoNodeException(path.toString());
+ Node node = parent.children().get(path.getName());
+ if (node == null) throw new KeeperException.NoNodeException(path.toString());
+ return node;
+ }
+
+ private String nodeName(String baseName, CreateMode createMode) {
+ switch (createMode) {
+ case PERSISTENT: case EPHEMERAL: return baseName;
+ case PERSISTENT_SEQUENTIAL: case EPHEMERAL_SEQUENTIAL: return baseName + monotonicallyIncreasingNumber++;
+ default: throw new UnsupportedOperationException(createMode + " support not implemented in MockCurator");
+ }
+ }
+
+ /** Validates a path using the same rules as ZooKeeper */
+ public static String validatePath(String path) throws IllegalArgumentException {
+ if (path == null) throw new IllegalArgumentException("Path cannot be null");
+ if (path.length() == 0) throw new IllegalArgumentException("Path length must be > 0");
+ if (path.charAt(0) != '/') throw new IllegalArgumentException("Path must start with / character");
+ if (path.length() == 1) return path; // done checking - it's the root
+ if (path.charAt(path.length() - 1) == '/')
+ throw new IllegalArgumentException("Path must not end with / character");
+
+ String reason = null;
+ char lastc = '/';
+ char chars[] = path.toCharArray();
+ char c;
+ for (int i = 1; i < chars.length; lastc = chars[i], i++) {
+ c = chars[i];
+
+ if (c == 0) {
+ reason = "null character not allowed @" + i;
+ break;
+ } else if (c == '/' && lastc == '/') {
+ reason = "empty node name specified @" + i;
+ break;
+ } else if (c == '.' && lastc == '.') {
+ if (chars[i-2] == '/' && ((i + 1 == chars.length) || chars[i+1] == '/')) {
+ reason = "relative paths not allowed @" + i;
+ break;
+ }
+ } else if (c == '.') {
+ if (chars[i-1] == '/' && ((i + 1 == chars.length) || chars[i+1] == '/')) {
+ reason = "relative paths not allowed @" + i;
+ break;
+ }
+ } else if (c > '\u0000' && c < '\u001f' || c > '\u007f' && c < '\u009F'
+ || c > '\ud800' && c < '\uf8ff' || c > '\ufff0' && c < '\uffff') {
+ reason = "invalid charater @" + i;
+ break;
+ }
+ }
+
+ if (reason != null)
+ throw new IllegalArgumentException("Invalid path string \"" + path + "\" caused by " + reason);
+ return path;
+ }
+
+ // ----- Mock of Curator recipes accessed through our Curator interface -----
+
+ @Override
+ public DistributedAtomicLong createAtomicCounter(String path) {
+ MockAtomicCounter counter = atomicCounters.get(path);
+ if (counter == null) {
+ counter = new MockAtomicCounter(path);
+ atomicCounters.put(path, counter);
+ }
+ return counter;
+ }
+
+ @Override
+ public InterProcessLock createMutex(String path) {
+ return new MockLock(path);
+ }
+
+ public MockCurator timeoutBarrierOnEnter(boolean shouldTimeout) {
+ shouldTimeoutOnEnter = shouldTimeout;
+ return this;
+ }
+
+ @Override
+ public CompletionWaiter getCompletionWaiter(Path parentPath, int numMembers, String id) {
+ return new MockCompletionWaiter();
+ }
+
+ @Override
+ public CompletionWaiter createCompletionWaiter(Path parentPath, String waiterNode, int numMembers, String id) {
+ return new MockCompletionWaiter();
+ }
+
+ @Override
+ public DirectoryCache createDirectoryCache(String path, boolean cacheData, boolean dataIsCompressed, ExecutorService executorService) {
+ return new MockDirectoryCache(Path.fromString(path));
+ }
+
+ @Override
+ public FileCache createFileCache(String path, boolean dataIsCompressed) {
+ return new MockFileCache(Path.fromString(path));
+ }
+
+ /**
+ * Invocation of changes to the file system state is abstracted through this to allow transactional
+ * changes to notify on commit
+ */
+ private abstract class Listeners {
+
+ /** Translating method */
+ public final void notify(Path path, byte[] data, PathChildrenCacheEvent.Type type) {
+ String pathString = "/" + path.toString(); // this silly path class strips the leading "/" :-/
+ PathChildrenCacheEvent event = new PathChildrenCacheEvent(type, new ChildData(pathString, null, data));
+ notify(path, event);
+ }
+
+ public abstract void notify(Path path, PathChildrenCacheEvent event);
+
+ }
+
+ /** The regular listener implementation which notifies registered file and directory listeners */
+ private class ListenerMap extends Listeners {
+
+ private final Map<Path, PathChildrenCacheListener> directoryListeners = new HashMap<>();
+ private final Map<Path, NodeCacheListener> fileListeners = new HashMap<>();
+
+ public void add(Path path, PathChildrenCacheListener listener) {
+ directoryListeners.put(path, listener);
+ }
+
+ public void add(Path path, NodeCacheListener listener) {
+ fileListeners.put(path, listener);
+ }
+
+ @Override
+ public void notify(Path path, PathChildrenCacheEvent event) {
+ try {
+ // Snapshot directoryListeners in case notification leads to new directoryListeners added
+ Set<Map.Entry<Path, PathChildrenCacheListener>>directoryLlistenerSnapshot = new HashSet<>(directoryListeners.entrySet());
+ for (Map.Entry<Path, PathChildrenCacheListener> listener : directoryLlistenerSnapshot) {
+ if (path.isChildOf(listener.getKey()))
+ listener.getValue().childEvent(curatorFramework, event);
+ }
+
+ // Snapshot directoryListeners in case notification leads to new directoryListeners added
+ Set<Map.Entry<Path, NodeCacheListener>> fileListenerSnapshot = new HashSet<>(fileListeners.entrySet());
+ for (Map.Entry<Path, NodeCacheListener> listener : fileListenerSnapshot) {
+ if (path.equals(listener.getKey()))
+ listener.getValue().nodeChanged();
+ }
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Exception notifying listeners", e);
+ }
+ }
+
+ }
+
+ private class MockCompletionWaiter implements CompletionWaiter {
+
+ @Override
+ public void awaitCompletion(Duration timeout) {
+ if (shouldTimeoutOnEnter) {
+ throw new CompletionTimeoutException("");
+ }
+ }
+
+ @Override
+ public void notifyCompletion() {
+ }
+
+ }
+
+ private class MockLock extends InterProcessSemaphoreMutex {
+
+ public MockLock(String path) {
+ super(curatorFramework, path);
+ }
+
+ @Override
+ public boolean acquire(long timeout, TimeUnit unit) {
+ if (throwExceptionOnLock) {
+ throw new CuratorLockException("Thrown by mock");
+ }
+ return !timeoutOnLock;
+ }
+
+ @Override
+ public void acquire() {
+ if (throwExceptionOnLock) {
+ throw new CuratorLockException("Thrown by mock");
+ }
+ }
+
+ @Override
+ public void release() { }
+
+ }
+
+ private class MockAtomicCounter extends DistributedAtomicLong {
+
+ private boolean initialized = false;
+ private MockLongValue value = new MockLongValue(0); // yes, uninitialized returns 0 :-/
+
+ public MockAtomicCounter(String path) {
+ super(curatorFramework, path, retryPolicy);
+ }
+
+ @Override
+ public boolean initialize(Long value) {
+ if (initialized) return false;
+ this.value = new MockLongValue(value);
+ initialized = true;
+ return true;
+ }
+
+ @Override
+ public AtomicValue<Long> get() {
+ if (value == null) return new MockLongValue(0);
+ return value;
+ }
+
+ public AtomicValue<Long> add(Long delta) throws Exception {
+ return trySet(value.postValue() + delta);
+ }
+
+ public AtomicValue<Long> subtract(Long delta) throws Exception {
+ return trySet(value.postValue() - delta);
+ }
+
+ @Override
+ public AtomicValue<Long> increment() {
+ return trySet(value.postValue() + 1);
+ }
+
+ public AtomicValue<Long> decrement() throws Exception {
+ return trySet(value.postValue() - 1);
+ }
+
+ @Override
+ public AtomicValue<Long> trySet(Long longval) {
+ value = new MockLongValue(longval);
+ return value;
+ }
+
+ public void forceSet(Long newValue) throws Exception {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ public AtomicValue<Long> compareAndSet(Long expectedValue, Long newValue) throws Exception {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ }
+
+ private class MockLongValue implements AtomicValue<Long> {
+
+ private long value;
+
+ public MockLongValue(long value) {
+ this.value = value;
+ }
+
+ @Override
+ public boolean succeeded() {
+ return true;
+ }
+
+ public void setValue(long value) {
+ this.value = value;
+ }
+
+ @Override
+ public Long preValue() {
+ return value;
+ }
+
+ @Override
+ public Long postValue() {
+ return value;
+ }
+
+ @Override
+ public AtomicStats getStats() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ }
+
+ private class MockDirectoryCache implements DirectoryCache {
+
+ /** The path this is caching and listening to */
+ private Path path;
+
+ public MockDirectoryCache(Path path) {
+ this.path = path;
+ }
+
+ @Override
+ public void start() {}
+
+ @Override
+ public void addListener(PathChildrenCacheListener listener) {
+ listeners.add(path, listener);
+ }
+
+ @Override
+ public List<ChildData> getCurrentData() {
+ Node parent = fileSystem.root().getNode(Paths.get(path.toString()), false);
+ if (parent == null) return Collections.emptyList(); // behavior in this case is unspecified
+
+ List<ChildData> data = new ArrayList<>();
+ collectData(parent, path, data);
+ Collections.sort(data);
+ return data;
+ }
+
+ private void collectData(Node parent, Path parentPath, List<ChildData> data) {
+ for (Node child : parent.children().values()) {
+ Path childPath = parentPath.append(child.name());
+ data.add(new ChildData("/" + childPath.toString(), null, child.getContent()));
+ }
+ }
+
+ @Override
+ public void close() {}
+
+ }
+
+ private class MockFileCache implements FileCache {
+
+ /** The path this is caching and listening to */
+ private Path path;
+
+ public MockFileCache(Path path) {
+ this.path = path;
+ }
+
+ @Override
+ public void start() {}
+
+ @Override
+ public void addListener(NodeCacheListener listener) {
+ listeners.add(path, listener);
+ }
+
+ @Override
+ public ChildData getCurrentData() {
+ Node node = fileSystem.root().getNode(Paths.get(path.toString()), false);
+ if (node == null) return null;
+ return new ChildData("/" + path.toString(), null, node.getContent());
+ }
+
+ @Override
+ public void close() {}
+
+ }
+
+ // ----- The rest of this file is adapting the Curator (non-recipe) API to the -----
+ // ----- file system methods above. -----
+ // ----- There's nothing to see unless you are interested in an illustration of -----
+ // ----- the folly of fluent API's or, more generally, mankind. -----
+
+ private abstract class MockBackgroundACLPathAndBytesableBuilder<T> implements PathAndBytesable<T>, ProtectACLCreateModePathAndBytesable<T> {
+
+ public BackgroundPathAndBytesable<T> withACL(List<ACL> list) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ public PathAndBytesable<T> inBackground() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ public PathAndBytesable<T> inBackground(Object o) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ public PathAndBytesable<T> inBackground(BackgroundCallback backgroundCallback) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ public PathAndBytesable<T> inBackground(BackgroundCallback backgroundCallback, Object o) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ public PathAndBytesable<T> inBackground(BackgroundCallback backgroundCallback, Executor executor) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ public PathAndBytesable<T> inBackground(BackgroundCallback backgroundCallback, Object o, Executor executor) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ public ACLBackgroundPathAndBytesable<T> withMode(CreateMode createMode) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public ACLCreateModeBackgroundPathAndBytesable<String> withProtection() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ public T forPath(String s, byte[] bytes) throws Exception {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ public T forPath(String s) throws Exception {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ }
+
+ private class MockCreateBuilder extends MockBackgroundACLPathAndBytesableBuilder<String> implements CreateBuilder {
+
+ private boolean createParents = false;
+ private CreateMode createMode = CreateMode.PERSISTENT;
+
+ @Override
+ public ProtectACLCreateModePathAndBytesable<String> creatingParentsIfNeeded() {
+ createParents = true;
+ return this;
+ }
+
+ @Override
+ public ACLCreateModeBackgroundPathAndBytesable<String> withProtection() {
+ // Protection against the server crashing after creating the file but before returning to the client.
+ // Not relevant for an in-memory mock, obviously
+ return this;
+ }
+
+ public ACLBackgroundPathAndBytesable<String> withMode(CreateMode createMode) {
+ this.createMode = createMode;
+ return this;
+ }
+
+ @Override
+ public CreateBackgroundModeACLable compressed() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public ProtectACLCreateModePathAndBytesable<String> creatingParentContainersIfNeeded() {
+ // TODO: Add proper support for container nodes, see https://issues.apache.org/jira/browse/ZOOKEEPER-2163.
+ return creatingParentsIfNeeded();
+ }
+
+ @Override
+ @Deprecated
+ public ACLPathAndBytesable<String> withProtectedEphemeralSequential() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ public String forPath(String s) throws Exception {
+ return createNode(s, new byte[0], createParents, createMode, fileSystem.root(), listeners);
+ }
+
+ public String forPath(String s, byte[] bytes) throws Exception {
+ return createNode(s, bytes, createParents, createMode, fileSystem.root(), listeners);
+ }
+
+ }
+
+ private class MockBackgroundPathableBuilder<T> implements BackgroundPathable<T>, Watchable<BackgroundPathable<T>> {
+
+ @Override
+ public Pathable<T> inBackground() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public Pathable<T> inBackground(Object o) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public Pathable<T> inBackground(BackgroundCallback backgroundCallback) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public Pathable<T> inBackground(BackgroundCallback backgroundCallback, Object o) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public Pathable<T> inBackground(BackgroundCallback backgroundCallback, Executor executor) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public Pathable<T> inBackground(BackgroundCallback backgroundCallback, Object o, Executor executor) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public BackgroundPathable<T> watched() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public BackgroundPathable<T> usingWatcher(Watcher watcher) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public BackgroundPathable<T> usingWatcher(CuratorWatcher curatorWatcher) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ public T forPath(String path) throws Exception {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ }
+
+ private class MockGetChildrenBuilder extends MockBackgroundPathableBuilder<List<String>> implements GetChildrenBuilder {
+
+ @Override
+ public WatchPathable<List<String>> storingStatIn(Stat stat) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public List<String> forPath(String path) throws Exception {
+ return getChildren(path, fileSystem.root());
+ }
+
+ }
+
+ private class MockExistsBuilder extends MockBackgroundPathableBuilder<Stat> implements ExistsBuilder {
+
+ @Override
+ public Stat forPath(String path) throws Exception {
+ if (exists(path, fileSystem.root()))
+ return new Stat(); // A more accurate mock should set the stat fields
+ else
+ return null;
+ }
+
+ @Override
+ public ExistsBuilderMain creatingParentContainersIfNeeded() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+ }
+
+ private class MockDeleteBuilder extends MockBackgroundPathableBuilder<Void> implements DeleteBuilder {
+
+ private boolean deleteChildren = false;
+
+ @Override
+ public BackgroundVersionable deletingChildrenIfNeeded() {
+ deleteChildren = true;
+ return this;
+ }
+
+ @Override
+ public ChildrenDeletable guaranteed() {
+ return this;
+ }
+
+ @Override
+ public BackgroundPathable<Void> withVersion(int i) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ public Void forPath(String pathString) throws Exception {
+ deleteNode(pathString, deleteChildren, fileSystem.root(), listeners);
+ return null;
+ }
+
+ }
+
+ private class MockGetDataBuilder extends MockBackgroundPathableBuilder<byte[]> implements GetDataBuilder {
+
+ @Override
+ public GetDataWatchBackgroundStatable decompressed() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public WatchPathable<byte[]> storingStatIn(Stat stat) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ public byte[] forPath(String path) throws Exception {
+ return getData(path, fileSystem.root());
+ }
+
+ }
+
+ private class MockSetDataBuilder extends MockBackgroundACLPathAndBytesableBuilder<Stat> implements SetDataBuilder {
+
+ @Override
+ public SetDataBackgroundVersionable compressed() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public BackgroundPathAndBytesable<Stat> withVersion(int i) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public Stat forPath(String path, byte[] bytes) throws Exception {
+ setData(path, bytes, fileSystem.root(), listeners);
+ return null;
+ }
+
+ }
+
+ /** Allows addition of directoryListeners which are never called */
+ private class MockListenable<T> implements Listenable<T> {
+
+ @Override
+ public void addListener(T t) {
+ }
+
+ @Override
+ public void addListener(T t, Executor executor) {
+ }
+
+ @Override
+ public void removeListener(T t) {
+ }
+
+ }
+
+ private class MockCuratorTransactionFinal implements CuratorTransactionFinal {
+
+ /** The new directory root in which the transactional changes are made */
+ private Node newRoot;
+
+ private boolean committed = false;
+
+ private final DelayedListener delayedListener = new DelayedListener();
+
+ public MockCuratorTransactionFinal() {
+ newRoot = fileSystem.root().clone();
+ }
+
+ @Override
+ public Collection<CuratorTransactionResult> commit() throws Exception {
+ fileSystem.replaceRoot(newRoot);
+ committed = true;
+ delayedListener.commit();
+ return null; // TODO
+ }
+
+ @Override
+ public TransactionCreateBuilder create() {
+ ensureNotCommitted();
+ return new MockTransactionCreateBuilder();
+ }
+
+ @Override
+ public TransactionDeleteBuilder delete() {
+ ensureNotCommitted();
+ return new MockTransactionDeleteBuilder();
+ }
+
+ @Override
+ public TransactionSetDataBuilder setData() {
+ ensureNotCommitted();
+ return new MockTransactionSetDataBuilder();
+ }
+
+ @Override
+ public TransactionCheckBuilder check() {
+ ensureNotCommitted();
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ private void ensureNotCommitted() {
+ if (committed) throw new IllegalStateException("transaction already committed");
+ }
+
+ private class MockTransactionCreateBuilder implements TransactionCreateBuilder {
+
+ private CreateMode createMode = CreateMode.PERSISTENT;
+
+ @Override
+ public PathAndBytesable<CuratorTransactionBridge> withACL(List<ACL> list) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public ACLPathAndBytesable<CuratorTransactionBridge> compressed() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public ACLPathAndBytesable<CuratorTransactionBridge> withMode(CreateMode createMode) {
+ this.createMode = createMode;
+ return this;
+ }
+
+ @Override
+ public CuratorTransactionBridge forPath(String s, byte[] bytes) throws Exception {
+ createNode(s, bytes, false, createMode, newRoot, delayedListener);
+ return new MockCuratorTransactionBridge();
+ }
+
+ @Override
+ public CuratorTransactionBridge forPath(String s) throws Exception {
+ createNode(s, new byte[0], false, createMode, newRoot, delayedListener);
+ return new MockCuratorTransactionBridge();
+ }
+
+ }
+
+ private class MockTransactionDeleteBuilder implements TransactionDeleteBuilder {
+
+ @Override
+ public Pathable<CuratorTransactionBridge> withVersion(int i) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public CuratorTransactionBridge forPath(String path) throws Exception {
+ deleteNode(path, false, newRoot, delayedListener);
+ return new MockCuratorTransactionBridge();
+ }
+
+ }
+
+ private class MockTransactionSetDataBuilder implements TransactionSetDataBuilder {
+
+ @Override
+ public PathAndBytesable<CuratorTransactionBridge> compressed() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public PathAndBytesable<CuratorTransactionBridge> withVersion(int i) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public CuratorTransactionBridge forPath(String s, byte[] bytes) throws Exception {
+ MockCurator.this.setData(s, bytes, newRoot, delayedListener);
+ return new MockCuratorTransactionBridge();
+ }
+
+ @Override
+ public CuratorTransactionBridge forPath(String s) throws Exception {
+ MockCurator.this.setData(s, new byte[0], newRoot, delayedListener);
+ return new MockCuratorTransactionBridge();
+ }
+
+ }
+
+ private class MockCuratorTransactionBridge implements CuratorTransactionBridge {
+
+ @Override
+ public CuratorTransactionFinal and() {
+ return MockCuratorTransactionFinal.this;
+ }
+
+ }
+
+ /** A class which collects listen events and forwards them to the regular directoryListeners on commit */
+ private class DelayedListener extends Listeners {
+
+ private final List<Pair<Path, PathChildrenCacheEvent>> events = new ArrayList<>();
+
+ @Override
+ public void notify(Path path, PathChildrenCacheEvent event) {
+ events.add(new Pair<>(path, event));
+ }
+
+ public void commit() {
+ for (Pair<Path, PathChildrenCacheEvent> event : events)
+ listeners.notify(event.getFirst(), event.getSecond());
+ }
+
+ }
+
+ }
+
+ private class MockCuratorFramework implements CuratorFramework {
+
+ private CuratorFrameworkState curatorState = CuratorFrameworkState.LATENT;
+
+ @Override
+ public void start() {
+ curatorState = CuratorFrameworkState.STARTED;
+ }
+
+ @Override
+ public void close() {
+ curatorState = CuratorFrameworkState.STOPPED;
+ }
+
+ @Override
+ public CuratorFrameworkState getState() {
+ return curatorState;
+ }
+
+ @Override
+ @Deprecated
+ public boolean isStarted() {
+ return curatorState == CuratorFrameworkState.STARTED;
+ }
+
+ @Override
+ public CreateBuilder create() {
+ return new MockCreateBuilder();
+ }
+
+ @Override
+ public DeleteBuilder delete() {
+ return new MockDeleteBuilder();
+ }
+
+ @Override
+ public ExistsBuilder checkExists() {
+ return new MockExistsBuilder();
+ }
+
+ @Override
+ public GetDataBuilder getData() {
+ return new MockGetDataBuilder();
+ }
+
+ @Override
+ public SetDataBuilder setData() {
+ return new MockSetDataBuilder();
+ }
+
+ @Override
+ public GetChildrenBuilder getChildren() {
+ return new MockGetChildrenBuilder();
+ }
+
+ @Override
+ public GetACLBuilder getACL() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public SetACLBuilder setACL() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public CuratorTransaction inTransaction() {
+ return new MockCuratorTransactionFinal();
+ }
+
+ @Override
+ @Deprecated
+ public void sync(String path, Object backgroundContextObject) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public void createContainers(String s) throws Exception {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public Listenable<ConnectionStateListener> getConnectionStateListenable() {
+ return new MockListenable<>();
+ }
+
+ @Override
+ public Listenable<CuratorListener> getCuratorListenable() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public Listenable<UnhandledErrorListener> getUnhandledErrorListenable() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ @Deprecated
+ public CuratorFramework nonNamespaceView() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public CuratorFramework usingNamespace(String newNamespace) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public String getNamespace() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public CuratorZookeeperClient getZookeeperClient() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Deprecated
+ @Override
+ public EnsurePath newNamespaceAwareEnsurePath(String path) {
+ return new EnsurePath(path);
+ }
+
+ @Override
+ public void clearWatcherReferences(Watcher watcher) {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ @Override
+ public boolean blockUntilConnected(int i, TimeUnit timeUnit) throws InterruptedException {
+ return true;
+ }
+
+ @Override
+ public void blockUntilConnected() throws InterruptedException {
+
+ }
+
+ @Override
+ public SyncBuilder sync() {
+ throw new UnsupportedOperationException("Not implemented in MockCurator");
+ }
+
+ }
+
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/package-info.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/package-info.java
new file mode 100644
index 00000000000..62235689f3b
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/package-info.java
@@ -0,0 +1,6 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+@PublicApi
+package com.yahoo.vespa.curator;
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/recipes/CuratorCounter.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/recipes/CuratorCounter.java
new file mode 100644
index 00000000000..8a5c2d35851
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/recipes/CuratorCounter.java
@@ -0,0 +1,67 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator.recipes;
+
+import com.yahoo.vespa.curator.Curator;
+import org.apache.curator.framework.recipes.atomic.AtomicValue;
+import org.apache.curator.framework.recipes.atomic.DistributedAtomicLong;
+
+/**
+ * A distributed atomic counter.
+ *
+ * @author lulf
+ * @since 5.1
+ */
+public class CuratorCounter {
+
+ private final DistributedAtomicLong counter;
+
+ public CuratorCounter(Curator curator, String counterPath) {
+ this.counter = curator.createAtomicCounter(counterPath);
+ }
+
+ /**
+ * Atomically increment and return next value.
+ *
+ * @return the new value.
+ */
+ public synchronized long next() {
+ try {
+ AtomicValue<Long> value = counter.increment();
+ if (!value.succeeded()) {
+ throw new RuntimeException("Increment did not succeed");
+ }
+ return value.postValue();
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to get next value", e);
+ }
+ }
+
+ public synchronized void set(long current) {
+ try {
+ counter.trySet(current);
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to set value", e);
+ }
+ }
+
+ public long get() {
+ try {
+ AtomicValue<Long> value = counter.get();
+ if (!value.succeeded()) {
+ throw new RuntimeException("Get did not succeed");
+ }
+ return value.postValue();
+ } catch (Exception e) {
+ throw new RuntimeException("Unable to get value", e);
+ }
+ }
+
+ public void initialize(long value) {
+ try {
+ counter.initialize(value);
+ } catch (Exception e) {
+ throw new RuntimeException("Error initializing atomic counter", e);
+ }
+ }
+
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/recipes/CuratorLock.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/recipes/CuratorLock.java
new file mode 100644
index 00000000000..3ac748061ca
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/recipes/CuratorLock.java
@@ -0,0 +1,75 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator.recipes;
+
+import com.yahoo.vespa.curator.Curator;
+import org.apache.curator.framework.recipes.locks.InterProcessLock;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class CuratorLock implements Lock {
+
+ private final InterProcessLock mutex;
+
+ public CuratorLock(Curator curator, String lockPath) {
+ this.mutex = curator.createMutex(lockPath);
+ }
+
+ public boolean hasLock() {
+ return mutex.isAcquiredInThisProcess();
+ }
+
+ @Override
+ public void lock() {
+ try {
+ mutex.acquire();
+ } catch (Exception e) {
+ throw new CuratorLockException(e);
+ }
+ }
+
+ @Override
+ public void lockInterruptibly() throws InterruptedException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean tryLock() {
+ try {
+ return tryLock(0, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ throw new CuratorLockException(e);
+ }
+ }
+
+ @Override
+ public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
+ try {
+ return mutex.acquire(time, unit);
+ } catch (InterruptedException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new CuratorLockException(e);
+ }
+ }
+
+ @Override
+ public void unlock() {
+ try {
+ mutex.release();
+ } catch (Exception e) {
+ throw new CuratorLockException(e);
+ }
+ }
+
+ @Override
+ public Condition newCondition() {
+ throw new UnsupportedOperationException();
+ }
+
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/recipes/CuratorLockException.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/recipes/CuratorLockException.java
new file mode 100644
index 00000000000..7c637a6b8be
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/recipes/CuratorLockException.java
@@ -0,0 +1,21 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator.recipes;
+
+/**
+ * @author lulf
+ * @since 5.1
+ */
+public class CuratorLockException extends RuntimeException {
+
+ public CuratorLockException(String message, Exception e) {
+ super(message, e);
+ }
+
+ public CuratorLockException(String message) {
+ super(message);
+ }
+
+ public CuratorLockException(Exception e) {
+ super(e);
+ }
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/recipes/package-info.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/recipes/package-info.java
new file mode 100644
index 00000000000..95a1a013f3c
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/recipes/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.curator.recipes;
+
+import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorCreateOperation.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorCreateOperation.java
new file mode 100644
index 00000000000..340815d60a1
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorCreateOperation.java
@@ -0,0 +1,49 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator.transaction;
+
+import com.yahoo.path.Path;
+import com.yahoo.vespa.curator.Curator;
+import org.apache.curator.framework.api.transaction.CuratorTransaction;
+
+import java.util.Optional;
+
+/**
+ * ZooKeeper create operation.
+ *
+ * @author lulf
+ * @author bratseth
+ */
+class CuratorCreateOperation implements CuratorOperation {
+
+ private final String path;
+ private final Optional<byte[]> data;
+
+ CuratorCreateOperation(String path, Optional<byte[]> data) {
+ this.path = path;
+ this.data = data;
+ }
+
+ @Override
+ public void check(Curator curator) {
+ int lastSlash = path.lastIndexOf("/");
+ if (lastSlash < 0) return; // root; ok
+ String parent = path.substring(0, lastSlash);
+ if (!parent.isEmpty() && ! curator.exists(Path.fromString(parent)) )
+ throw new IllegalStateException("Cannot perform " + this + ": Parent '" + parent + "' does not exist");
+ }
+
+ @Override
+ public CuratorTransaction and(CuratorTransaction transaction) throws Exception {
+ if (data.isPresent()) {
+ return transaction.create().forPath(path, data.get()).and();
+ } else {
+ return transaction.create().forPath(path).and();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "CREATE " + path;
+ }
+
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorDeleteOperation.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorDeleteOperation.java
new file mode 100644
index 00000000000..c9e180b4827
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorDeleteOperation.java
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator.transaction;
+
+import com.yahoo.path.Path;
+import com.yahoo.vespa.curator.Curator;
+import org.apache.curator.framework.api.transaction.CuratorTransaction;
+
+/**
+ * @author lulf
+ * @author bratseth
+ */
+class CuratorDeleteOperation implements CuratorOperation {
+
+ private final String path;
+
+ CuratorDeleteOperation(String path) {
+ this.path = path;
+ }
+
+ @Override
+ public void check(Curator curator) {
+ if ( ! curator.exists(Path.fromString(path)) )
+ throw new IllegalStateException("Cannot perform " + this + ": Path does not exist");
+ }
+
+ @Override
+ public CuratorTransaction and(CuratorTransaction transaction) throws Exception {
+ return transaction.delete().forPath(path).and();
+ }
+
+ @Override
+ public String toString() {
+ return "DELETE " + path;
+ }
+
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorOperation.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorOperation.java
new file mode 100644
index 00000000000..f0a4f5f1d66
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorOperation.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator.transaction;
+
+import com.yahoo.transaction.Transaction;
+import com.yahoo.vespa.curator.Curator;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.api.transaction.CuratorTransaction;
+
+import java.util.Optional;
+
+/**
+ * The ZooKeeper operations that we support doing transactional.
+ *
+ * @author lulf
+ * @author bratseth
+ */
+public interface CuratorOperation extends Transaction.Operation {
+
+ /**
+ * Implementations must support adding this operation to a curator transaction.
+ *
+ * @param transaction {@link CuratorTransaction} to append this operation to.
+ * @return the transaction, for chaining.
+ * @throws Exception if unable to create transaction for this operation.
+ */
+ CuratorTransaction and(CuratorTransaction transaction) throws Exception;
+
+ /**
+ * Check if this operation can be performed without making any changes.
+ *
+ * @throws IllegalStateException if it cannot
+ */
+ void check(Curator curator);
+
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorOperations.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorOperations.java
new file mode 100644
index 00000000000..6dc009b4cfd
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorOperations.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator.transaction;
+
+import java.util.Optional;
+
+/**
+ * Factory for transactional ZooKeeper operations
+ *
+ * @author lulf
+ */
+public class CuratorOperations {
+
+ public static CuratorOperation setData(String path, byte[] bytes) {
+ return new CuratorSetDataOperation(path, bytes);
+ }
+
+ public static CuratorOperation create(String path, byte[] bytes) {
+ return new CuratorCreateOperation(path, Optional.of(bytes));
+ }
+
+ public static CuratorOperation create(String path) {
+ return new CuratorCreateOperation(path, Optional.empty());
+ }
+
+ public static CuratorOperation delete(String path) {
+ return new CuratorDeleteOperation(path);
+ }
+
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorSetDataOperation.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorSetDataOperation.java
new file mode 100644
index 00000000000..eddca50a69e
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorSetDataOperation.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator.transaction;
+
+import com.yahoo.path.Path;
+import com.yahoo.vespa.curator.Curator;
+import org.apache.curator.framework.api.transaction.CuratorTransaction;
+
+/**
+ * ZooKeeper setData operation.
+ *
+ * @author lulf
+ * @author bratseth
+ */
+class CuratorSetDataOperation implements CuratorOperation {
+
+ private final String path;
+ private final byte[] data;
+
+ public CuratorSetDataOperation(String path, byte[] data) {
+ this.path = path;
+ this.data = data;
+ }
+
+ @Override
+ public void check(Curator curator) {
+ if ( ! curator.exists(Path.fromString(path)) )
+ throw new IllegalStateException("Cannot perform " + this + ": Path does not exist");
+ }
+
+ @Override
+ public CuratorTransaction and(CuratorTransaction transaction) throws Exception {
+ return transaction.setData().forPath(path, data).and();
+ }
+
+ @Override
+ public String toString() {
+ return "SET " + path;
+ }
+
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorTransaction.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorTransaction.java
new file mode 100644
index 00000000000..886a08a3ca0
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/CuratorTransaction.java
@@ -0,0 +1,74 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.curator.transaction;
+
+import com.yahoo.transaction.Transaction;
+import com.yahoo.vespa.curator.Curator;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.api.transaction.CuratorTransactionFinal;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+
+/**
+ * Transaction implementation against ZooKeeper.
+ *
+ * @author lulf
+ */
+public class CuratorTransaction implements Transaction {
+
+ private static final Logger log = Logger.getLogger(CuratorTransaction.class.getName());
+ private final List<Operation> operations = new ArrayList<>();
+ private final Curator curator;
+
+ public CuratorTransaction(Curator curator) {
+ this.curator = curator;
+ }
+
+ @Override
+ public Transaction add(Operation operation) {
+ this.operations.add(operation);
+ return this;
+ }
+
+ @Override
+ public Transaction add(List<Operation> operations) {
+ this.operations.addAll(operations);
+ return this;
+ }
+
+ @Override
+ public List<Operation> operations() { return operations; }
+
+ @Override
+ public void prepare() {
+ for (Operation operation : operations)
+ ((CuratorOperation)operation).check(curator);
+ }
+
+ @Override
+ public void commit() {
+ try {
+ org.apache.curator.framework.api.transaction.CuratorTransaction transaction = curator.framework().inTransaction();
+ for (Operation operation : operations) {
+ CuratorOperation zkOperation = (CuratorOperation) operation;
+ transaction = zkOperation.and(transaction);
+ }
+ ((CuratorTransactionFinal) transaction).commit();
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @Override
+ public void rollbackOrLog() {
+ log.severe("The following ZooKeeper operations were incorrectly committed and probably require " +
+ "manual correction: " + operations);
+ }
+
+ @Override
+ public void close() {
+ operations.clear();
+ }
+
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/package-info.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/package-info.java
new file mode 100644
index 00000000000..9977c6aa062
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/transaction/package-info.java
@@ -0,0 +1,9 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.curator.transaction;
+
+import com.yahoo.osgi.annotation.ExportPackage;
+
+/**
+ * Classes which allows curator operations to participate in two-phase transactions over multiple systems.
+ */ \ No newline at end of file
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/zookeeper/ZooKeeperServer.java b/zkfacade/src/main/java/com/yahoo/vespa/zookeeper/ZooKeeperServer.java
new file mode 100644
index 00000000000..ffae797561c
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/zookeeper/ZooKeeperServer.java
@@ -0,0 +1,124 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.zookeeper;
+
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ZookeeperServerConfig;
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.defaults.Defaults;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Writes zookeeper config and starts zookeeper server.
+ *
+ * @author lulf
+ * @since 5.3
+ */
+public class ZooKeeperServer extends AbstractComponent implements Runnable {
+
+ private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(ZooKeeperServer.class.getName());
+ private static final String ZOOKEEPER_JMX_LOG4J_DISABLE = "zookeeper.jmx.log4j.disable";
+ static final String ZOOKEEPER_JUTE_MAX_BUFFER = "jute.maxbuffer";
+ private final Thread zkServerThread;
+ private final ZookeeperServerConfig config;
+
+ ZooKeeperServer(ZookeeperServerConfig config, boolean startServer) {
+ this.config = config;
+ System.setProperty("zookeeper.jmx.log4j.disable", "true");
+ System.setProperty(ZOOKEEPER_JUTE_MAX_BUFFER, "" + config.juteMaxBuffer());
+ writeConfigToDisk(config);
+ zkServerThread = new Thread(this, "zookeeper server");
+ if (startServer) {
+ zkServerThread.start();
+ }
+ }
+
+ @Inject
+ public ZooKeeperServer(ZookeeperServerConfig config) {
+ this(config, true);
+ }
+
+ private void writeConfigToDisk(ZookeeperServerConfig config) {
+ String cfg = transformConfigToString(config);
+ try (FileWriter writer = new FileWriter(Defaults.getDefaults().underVespaHome(config.zooKeeperConfigFile()))) {
+ writer.write(cfg);
+ writeMyIdFile(config);
+ } catch (IOException e) {
+ throw new RuntimeException("Error writing zookeeper config", e);
+ }
+ }
+
+ private String transformConfigToString(ZookeeperServerConfig config) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("tickTime=").append(config.tickTime()).append("\n");
+ sb.append("initLimit=").append(config.initLimit()).append("\n");
+ sb.append("syncLimit=").append(config.syncLimit()).append("\n");
+ sb.append("maxClientCnxns=").append(config.maxClientConnections()).append("\n");
+ sb.append("snapCount=").append(config.snapshotCount()).append("\n");
+ sb.append("dataDir=").append(Defaults.getDefaults().underVespaHome(config.dataDir())).append("\n");
+ sb.append("clientPort=").append(config.clientPort()).append("\n");
+ sb.append("autopurge.purgeInterval=").append(config.autopurge().purgeInterval()).append("\n");
+ sb.append("autopurge.snapRetainCount=").append(config.autopurge().snapRetainCount()).append("\n");
+ if (config.server().size() > 1) {
+ ensureThisServerIsRepresented(config.myid(), config.server());
+ for (ZookeeperServerConfig.Server server : config.server()) {
+ addServerToCfg(sb, server);
+ }
+ }
+ return sb.toString();
+ }
+
+ private void writeMyIdFile(ZookeeperServerConfig config) throws IOException {
+ if (config.server().size() > 1) {
+ try (FileWriter writer = new FileWriter(Defaults.getDefaults().underVespaHome(config.myidFile()))) {
+ writer.write(config.myid() + "\n");
+ }
+ }
+ }
+
+ private void ensureThisServerIsRepresented(int myid, List<ZookeeperServerConfig.Server> servers) {
+ boolean found = false;
+ for (ZookeeperServerConfig.Server server : servers) {
+ if (myid == server.id()) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ throw new RuntimeException("No id in zookeeper server list that corresponds to my id(" + myid + ")");
+ }
+ }
+
+ private void addServerToCfg(StringBuilder sb, ZookeeperServerConfig.Server server) {
+ sb.append("server.").append(server.id()).append("=").append(server.hostname()).append(":").append(server.quorumPort()).append(":").append(server.electionPort()).append("\n");
+ }
+
+ private void shutdown() {
+ zkServerThread.interrupt();
+ try {
+ zkServerThread.join();
+ } catch (InterruptedException e) {
+ log.log(LogLevel.WARNING, "Error joining server thread on shutdown", e);
+ }
+ }
+
+ @Override
+ public void run() {
+ System.setProperty(ZOOKEEPER_JMX_LOG4J_DISABLE, "true");
+ String[] args = new String[]{Defaults.getDefaults().underVespaHome(config.zooKeeperConfigFile())};
+ log.log(LogLevel.DEBUG, "Starting ZooKeeper server with config: " + args[0]);
+ org.apache.zookeeper.server.quorum.QuorumPeerMain.main(args);
+ }
+
+ @Override
+ public void deconstruct() {
+ shutdown();
+ super.deconstruct();
+ }
+
+ public ZookeeperServerConfig getConfig() { return config; }
+
+}
diff --git a/zkfacade/src/main/java/com/yahoo/vespa/zookeeper/package-info.java b/zkfacade/src/main/java/com/yahoo/vespa/zookeeper/package-info.java
new file mode 100644
index 00000000000..19f55999a06
--- /dev/null
+++ b/zkfacade/src/main/java/com/yahoo/vespa/zookeeper/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.zookeeper;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/zkfacade/src/main/java/org/apache/curator/Dummy.java b/zkfacade/src/main/java/org/apache/curator/Dummy.java
new file mode 100644
index 00000000000..c4d3a43a63e
--- /dev/null
+++ b/zkfacade/src/main/java/org/apache/curator/Dummy.java
@@ -0,0 +1,12 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package org.apache.curator;
+
+/**
+ * A dummy to make the attach-jars Maven profile work for this module (it needs to find a class to javadoc and attach source).
+ * Having this beats having duplicated stuff in pom.xml.
+ * @author vegardh
+ *
+ */
+public class Dummy {
+
+}
diff --git a/zkfacade/src/main/java/org/apache/curator/framework/api/package-info.java b/zkfacade/src/main/java/org/apache/curator/framework/api/package-info.java
new file mode 100644
index 00000000000..2d93d37a59f
--- /dev/null
+++ b/zkfacade/src/main/java/org/apache/curator/framework/api/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage(version = @Version(major = 2, minor = 9, micro = 1))
+package org.apache.curator.framework.api;
+import com.yahoo.osgi.annotation.ExportPackage;
+import com.yahoo.osgi.annotation.Version;
diff --git a/zkfacade/src/main/java/org/apache/curator/framework/api/transaction/package-info.java b/zkfacade/src/main/java/org/apache/curator/framework/api/transaction/package-info.java
new file mode 100644
index 00000000000..15d0d1cfae4
--- /dev/null
+++ b/zkfacade/src/main/java/org/apache/curator/framework/api/transaction/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage(version = @Version(major = 2, minor = 9, micro = 1))
+package org.apache.curator.framework.api.transaction;
+import com.yahoo.osgi.annotation.ExportPackage;
+import com.yahoo.osgi.annotation.Version;
diff --git a/zkfacade/src/main/java/org/apache/curator/framework/listen/package-info.java b/zkfacade/src/main/java/org/apache/curator/framework/listen/package-info.java
new file mode 100644
index 00000000000..82f46d6e86b
--- /dev/null
+++ b/zkfacade/src/main/java/org/apache/curator/framework/listen/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage(version = @Version(major = 2, minor = 9, micro = 1))
+package org.apache.curator.framework.listen;
+import com.yahoo.osgi.annotation.ExportPackage;
+import com.yahoo.osgi.annotation.Version;
diff --git a/zkfacade/src/main/java/org/apache/curator/framework/package-info.java b/zkfacade/src/main/java/org/apache/curator/framework/package-info.java
new file mode 100644
index 00000000000..b0583be23e7
--- /dev/null
+++ b/zkfacade/src/main/java/org/apache/curator/framework/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage(version = @Version(major = 2, minor = 9, micro = 1))
+package org.apache.curator.framework;
+import com.yahoo.osgi.annotation.ExportPackage;
+import com.yahoo.osgi.annotation.Version;
diff --git a/zkfacade/src/main/java/org/apache/curator/framework/recipes/atomic/package-info.java b/zkfacade/src/main/java/org/apache/curator/framework/recipes/atomic/package-info.java
new file mode 100644
index 00000000000..3efdb7ec033
--- /dev/null
+++ b/zkfacade/src/main/java/org/apache/curator/framework/recipes/atomic/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage(version = @Version(major = 2, minor = 9, micro = 1))
+package org.apache.curator.framework.recipes.atomic;
+import com.yahoo.osgi.annotation.ExportPackage;
+import com.yahoo.osgi.annotation.Version;
diff --git a/zkfacade/src/main/java/org/apache/curator/framework/recipes/barriers/package-info.java b/zkfacade/src/main/java/org/apache/curator/framework/recipes/barriers/package-info.java
new file mode 100644
index 00000000000..e58541545b9
--- /dev/null
+++ b/zkfacade/src/main/java/org/apache/curator/framework/recipes/barriers/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage(version = @Version(major = 2, minor = 9, micro = 1))
+package org.apache.curator.framework.recipes.barriers;
+import com.yahoo.osgi.annotation.ExportPackage;
+import com.yahoo.osgi.annotation.Version;
diff --git a/zkfacade/src/main/java/org/apache/curator/framework/recipes/cache/package-info.java b/zkfacade/src/main/java/org/apache/curator/framework/recipes/cache/package-info.java
new file mode 100644
index 00000000000..4c3dbc7d0bd
--- /dev/null
+++ b/zkfacade/src/main/java/org/apache/curator/framework/recipes/cache/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage(version = @Version(major = 2, minor = 9, micro = 1))
+package org.apache.curator.framework.recipes.cache;
+import com.yahoo.osgi.annotation.ExportPackage;
+import com.yahoo.osgi.annotation.Version;
diff --git a/zkfacade/src/main/java/org/apache/curator/framework/recipes/locks/package-info.java b/zkfacade/src/main/java/org/apache/curator/framework/recipes/locks/package-info.java
new file mode 100644
index 00000000000..c6029aaa365
--- /dev/null
+++ b/zkfacade/src/main/java/org/apache/curator/framework/recipes/locks/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage(version = @Version(major = 2, minor = 9, micro = 1))
+package org.apache.curator.framework.recipes.locks;
+import com.yahoo.osgi.annotation.ExportPackage;
+import com.yahoo.osgi.annotation.Version;
diff --git a/zkfacade/src/main/java/org/apache/curator/framework/state/package-info.java b/zkfacade/src/main/java/org/apache/curator/framework/state/package-info.java
new file mode 100644
index 00000000000..f58f5fecaea
--- /dev/null
+++ b/zkfacade/src/main/java/org/apache/curator/framework/state/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage(version = @Version(major = 2, minor = 9, micro = 1))
+package org.apache.curator.framework.state;
+import com.yahoo.osgi.annotation.ExportPackage;
+import com.yahoo.osgi.annotation.Version;
diff --git a/zkfacade/src/main/java/org/apache/curator/package-info.java b/zkfacade/src/main/java/org/apache/curator/package-info.java
new file mode 100644
index 00000000000..64bd6834ded
--- /dev/null
+++ b/zkfacade/src/main/java/org/apache/curator/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage(version = @Version(major = 2, minor = 9, micro = 1))
+package org.apache.curator;
+import com.yahoo.osgi.annotation.ExportPackage;
+import com.yahoo.osgi.annotation.Version;
diff --git a/zkfacade/src/main/java/org/apache/curator/retry/package-info.java b/zkfacade/src/main/java/org/apache/curator/retry/package-info.java
new file mode 100644
index 00000000000..580d815ab7a
--- /dev/null
+++ b/zkfacade/src/main/java/org/apache/curator/retry/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage(version = @Version(major = 2, minor = 9, micro = 1))
+package org.apache.curator.retry;
+import com.yahoo.osgi.annotation.ExportPackage;
+import com.yahoo.osgi.annotation.Version;
diff --git a/zkfacade/src/main/java/org/apache/zookeeper/data/package-info.java b/zkfacade/src/main/java/org/apache/zookeeper/data/package-info.java
new file mode 100644
index 00000000000..2667ea60c2d
--- /dev/null
+++ b/zkfacade/src/main/java/org/apache/zookeeper/data/package-info.java
@@ -0,0 +1,6 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage(version = @Version(major = 3, minor = 4, micro = 6))
+package org.apache.zookeeper.data;
+
+import com.yahoo.osgi.annotation.ExportPackage;
+import com.yahoo.osgi.annotation.Version;
diff --git a/zkfacade/src/main/java/org/apache/zookeeper/package-info.java b/zkfacade/src/main/java/org/apache/zookeeper/package-info.java
new file mode 100644
index 00000000000..80158a42a65
--- /dev/null
+++ b/zkfacade/src/main/java/org/apache/zookeeper/package-info.java
@@ -0,0 +1,6 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage(version = @Version(major = 3, minor = 4, micro = 6))
+package org.apache.zookeeper;
+
+import com.yahoo.osgi.annotation.ExportPackage;
+import com.yahoo.osgi.annotation.Version;
diff --git a/zkfacade/src/main/sh/zkcat b/zkfacade/src/main/sh/zkcat
new file mode 100755
index 00000000000..0f031444400
--- /dev/null
+++ b/zkfacade/src/main/sh/zkcat
@@ -0,0 +1,63 @@
+#!/bin/sh
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+# BEGIN environment bootstrap section
+# Do not edit between here and END as this section should stay identical in all scripts
+
+findpath () {
+ myname=${0}
+ mypath=${myname%/*}
+ myname=${myname##*/}
+ if [ "$mypath" ] && [ -d "$mypath" ]; then
+ return
+ fi
+ mypath=$(pwd)
+ if [ -f "${mypath}/${myname}" ]; then
+ return
+ fi
+ echo "FATAL: Could not figure out the path where $myname lives from $0"
+ exit 1
+}
+
+COMMON_ENV=libexec/vespa/common-env.sh
+
+source_common_env () {
+ if [ "$VESPA_HOME" ] && [ -d "$VESPA_HOME" ]; then
+ # ensure it ends with "/" :
+ VESPA_HOME=${VESPA_HOME%/}/
+ export VESPA_HOME
+ common_env=$VESPA_HOME/$COMMON_ENV
+ if [ -f "$common_env" ]; then
+ . $common_env
+ return
+ fi
+ fi
+ return 1
+}
+
+findroot () {
+ source_common_env && return
+ if [ "$VESPA_HOME" ]; then
+ echo "FATAL: bad VESPA_HOME value '$VESPA_HOME'"
+ exit 1
+ fi
+ if [ "$ROOT" ] && [ -d "$ROOT" ]; then
+ VESPA_HOME="$ROOT"
+ source_common_env && return
+ fi
+ findpath
+ while [ "$mypath" ]; do
+ VESPA_HOME=${mypath}
+ source_common_env && return
+ mypath=${mypath%/*}
+ done
+ echo "FATAL: missing VESPA_HOME environment variable"
+ echo "Could not locate $COMMON_ENV anywhere"
+ exit 1
+}
+
+findroot
+
+# END environment bootstrap section
+
+$VESPA_HOME/bin/zkctl get $@
diff --git a/zkfacade/src/main/sh/zkctl b/zkfacade/src/main/sh/zkctl
new file mode 100755
index 00000000000..d93e2933986
--- /dev/null
+++ b/zkfacade/src/main/sh/zkctl
@@ -0,0 +1,63 @@
+#!/bin/sh
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+# BEGIN environment bootstrap section
+# Do not edit between here and END as this section should stay identical in all scripts
+
+findpath () {
+ myname=${0}
+ mypath=${myname%/*}
+ myname=${myname##*/}
+ if [ "$mypath" ] && [ -d "$mypath" ]; then
+ return
+ fi
+ mypath=$(pwd)
+ if [ -f "${mypath}/${myname}" ]; then
+ return
+ fi
+ echo "FATAL: Could not figure out the path where $myname lives from $0"
+ exit 1
+}
+
+COMMON_ENV=libexec/vespa/common-env.sh
+
+source_common_env () {
+ if [ "$VESPA_HOME" ] && [ -d "$VESPA_HOME" ]; then
+ # ensure it ends with "/" :
+ VESPA_HOME=${VESPA_HOME%/}/
+ export VESPA_HOME
+ common_env=$VESPA_HOME/$COMMON_ENV
+ if [ -f "$common_env" ]; then
+ . $common_env
+ return
+ fi
+ fi
+ return 1
+}
+
+findroot () {
+ source_common_env && return
+ if [ "$VESPA_HOME" ]; then
+ echo "FATAL: bad VESPA_HOME value '$VESPA_HOME'"
+ exit 1
+ fi
+ if [ "$ROOT" ] && [ -d "$ROOT" ]; then
+ VESPA_HOME="$ROOT"
+ source_common_env && return
+ fi
+ findpath
+ while [ "$mypath" ]; do
+ VESPA_HOME=${mypath}
+ source_common_env && return
+ mypath=${mypath%/*}
+ done
+ echo "FATAL: missing VESPA_HOME environment variable"
+ echo "Could not locate $COMMON_ENV anywhere"
+ exit 1
+}
+
+findroot
+
+# END environment bootstrap section
+
+(echo "$@" | sudo -u yahoo java -cp $VESPA_HOME/lib/jars/zkctl-jar-with-dependencies.jar -Dlog4j.configuration=file:$VESPA_HOME/etc/log4j-vespa.properties org.apache.zookeeper.ZooKeeperMain -server 127.0.0.1:2181) 2>&1
diff --git a/zkfacade/src/main/sh/zkls b/zkfacade/src/main/sh/zkls
new file mode 100755
index 00000000000..ccc52b0a4af
--- /dev/null
+++ b/zkfacade/src/main/sh/zkls
@@ -0,0 +1,63 @@
+#!/bin/sh
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+# BEGIN environment bootstrap section
+# Do not edit between here and END as this section should stay identical in all scripts
+
+findpath () {
+ myname=${0}
+ mypath=${myname%/*}
+ myname=${myname##*/}
+ if [ "$mypath" ] && [ -d "$mypath" ]; then
+ return
+ fi
+ mypath=$(pwd)
+ if [ -f "${mypath}/${myname}" ]; then
+ return
+ fi
+ echo "FATAL: Could not figure out the path where $myname lives from $0"
+ exit 1
+}
+
+COMMON_ENV=libexec/vespa/common-env.sh
+
+source_common_env () {
+ if [ "$VESPA_HOME" ] && [ -d "$VESPA_HOME" ]; then
+ # ensure it ends with "/" :
+ VESPA_HOME=${VESPA_HOME%/}/
+ export VESPA_HOME
+ common_env=$VESPA_HOME/$COMMON_ENV
+ if [ -f "$common_env" ]; then
+ . $common_env
+ return
+ fi
+ fi
+ return 1
+}
+
+findroot () {
+ source_common_env && return
+ if [ "$VESPA_HOME" ]; then
+ echo "FATAL: bad VESPA_HOME value '$VESPA_HOME'"
+ exit 1
+ fi
+ if [ "$ROOT" ] && [ -d "$ROOT" ]; then
+ VESPA_HOME="$ROOT"
+ source_common_env && return
+ fi
+ findpath
+ while [ "$mypath" ]; do
+ VESPA_HOME=${mypath}
+ source_common_env && return
+ mypath=${mypath%/*}
+ done
+ echo "FATAL: missing VESPA_HOME environment variable"
+ echo "Could not locate $COMMON_ENV anywhere"
+ exit 1
+}
+
+findroot
+
+# END environment bootstrap section
+
+$VESPA_HOME/bin/zkctl ls $@