diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /zkfacade/src/main |
Publish
Diffstat (limited to 'zkfacade/src/main')
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 $@ |