Merge branch 'stable-3.0'

* stable-3.0:
  Remove unused constants from tests
  Add zookeeper plugin documentation
  Move zookeeper related code from multi-site plugin to zookeeper plugin

Change-Id: I1c492455147b4bde204a0e38466fc4c387aae393
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..548e384
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,53 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+load(
+    "//tools/bzl:plugin.bzl",
+    "PLUGIN_DEPS",
+    "PLUGIN_TEST_DEPS",
+    "gerrit_plugin",
+)
+
+gerrit_plugin(
+    name = "zookeeper",
+    srcs = glob(["src/main/java/**/*.java"]),
+    manifest_entries = [
+        "Gerrit-PluginName: zookeeper",
+        "Gerrit-Module: com.googlesource.gerrit.plugins.validation.dfsrefdb.zookeeper.ZkValidationModule",
+        "Implementation-Title: zookeeper plugin",
+        "Implementation-URL: https://review.gerrithub.io/admin/repos/GerritForge/plugins_zookeeper",
+    ],
+    resources = glob(["src/main/resources/**/*"]),
+    deps = [
+        "@curator-client//jar",
+        "@curator-framework//jar",
+        "@curator-recipes//jar",
+        "@global-refdb//jar",
+        "@zookeeper//jar",
+    ],
+)
+
+junit_tests(
+    name = "zookeeper_tests",
+    srcs = glob(["src/test/java/**/*.java"]),
+    resources = glob(["src/test/resources/**/*"]),
+    tags = [
+        "local",
+        "zookeeper",
+    ],
+    deps = [
+        ":zookeeper__plugin_test_deps",
+    ],
+)
+
+java_library(
+    name = "zookeeper__plugin_test_deps",
+    testonly = 1,
+    visibility = ["//visibility:public"],
+    exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+        ":zookeeper__plugin",
+        "@curator-framework//jar",
+        "@curator-recipes//jar",
+        "@curator-test//jar",
+        "@curator-client//jar",
+        "//lib/testcontainers",
+    ],
+)
diff --git a/README.md b/README.md
index fee6669..f92fc18 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,3 @@
 # plugins_zookeeper
+
 Zookeeper plugin for Gerrit Code Review
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
new file mode 100644
index 0000000..2f83eec
--- /dev/null
+++ b/external_plugin_deps.bzl
@@ -0,0 +1,40 @@
+load("//tools/bzl:maven_jar.bzl", "maven_jar")
+
+def external_plugin_deps():
+    CURATOR_VER = "4.2.0"
+
+    maven_jar(
+        name = "curator-test",
+        artifact = "org.apache.curator:curator-test:" + CURATOR_VER,
+        sha1 = "98ac2dd69b8c07dcaab5e5473f93fdb9e320cd73",
+    )
+
+    maven_jar(
+        name = "curator-framework",
+        artifact = "org.apache.curator:curator-framework:" + CURATOR_VER,
+        sha1 = "5b1cc87e17b8fe4219b057f6025662a693538861",
+    )
+
+    maven_jar(
+        name = "curator-recipes",
+        artifact = "org.apache.curator:curator-recipes:" + CURATOR_VER,
+        sha1 = "7f775be5a7062c2477c51533b9d008f70411ba8e",
+    )
+
+    maven_jar(
+        name = "curator-client",
+        artifact = "org.apache.curator:curator-client:" + CURATOR_VER,
+        sha1 = "d5d50930b8dd189f92c40258a6ba97675fea3e15",
+    )
+
+    maven_jar(
+        name = "zookeeper",
+        artifact = "org.apache.zookeeper:zookeeper:3.4.14",
+        sha1 = "c114c1e1c8172a7cd3f6ae39209a635f7a06c1a1",
+    )
+
+    maven_jar(
+        name = "global-refdb",
+        artifact = "com.gerritforge:global-refdb:0.1.1",
+        sha1 = "d6ab59906db7b20a52c8994502780b2a6ab23872",
+    )
diff --git a/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZkConnectionConfig.java b/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZkConnectionConfig.java
new file mode 100644
index 0000000..28a2527
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZkConnectionConfig.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.validation.dfsrefdb.zookeeper;
+
+import org.apache.curator.RetryPolicy;
+
+public class ZkConnectionConfig {
+
+  public final RetryPolicy curatorRetryPolicy;
+  public final Long transactionLockTimeout;
+
+  public ZkConnectionConfig(RetryPolicy curatorRetryPolicy, Long transactionLockTimeout) {
+    this.curatorRetryPolicy = curatorRetryPolicy;
+    this.transactionLockTimeout = transactionLockTimeout;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZkSharedRefDatabase.java b/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZkSharedRefDatabase.java
new file mode 100644
index 0000000..5378879
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZkSharedRefDatabase.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.validation.dfsrefdb.zookeeper;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDatabase;
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDbLockException;
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+import java.nio.charset.StandardCharsets;
+import org.apache.curator.RetryPolicy;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.recipes.atomic.AtomicValue;
+import org.apache.curator.framework.recipes.atomic.DistributedAtomicValue;
+import org.apache.curator.framework.recipes.locks.InterProcessMutex;
+import org.apache.curator.framework.recipes.locks.Locker;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+
+public class ZkSharedRefDatabase implements GlobalRefDatabase {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final CuratorFramework client;
+  private final RetryPolicy retryPolicy;
+
+  private final Long transactionLockTimeOut;
+
+  @Inject
+  public ZkSharedRefDatabase(CuratorFramework client, ZkConnectionConfig connConfig) {
+    this.client = client;
+    this.retryPolicy = connConfig.curatorRetryPolicy;
+    this.transactionLockTimeOut = connConfig.transactionLockTimeout;
+  }
+
+  @Override
+  public boolean isUpToDate(Project.NameKey project, Ref ref) throws GlobalRefDbLockException {
+    if (!exists(project, ref.getName())) {
+      return true;
+    }
+
+    try {
+      final byte[] valueInZk = client.getData().forPath(pathFor(project, ref.getName()));
+
+      // Assuming this is a delete node NULL_REF
+      if (valueInZk == null) {
+        logger.atInfo().log(
+            "%s:%s not found in Zookeeper, assumed as delete node NULL_REF",
+            project, ref.getName());
+        return false;
+      }
+
+      ObjectId objectIdInSharedRefDb = readObjectId(valueInZk);
+      Boolean isUpToDate = objectIdInSharedRefDb.equals(ref.getObjectId());
+
+      if (!isUpToDate) {
+        logger.atWarning().log(
+            "%s:%s is out of sync: local=%s zk=%s",
+            project, ref.getName(), ref.getObjectId(), objectIdInSharedRefDb);
+      }
+
+      return isUpToDate;
+    } catch (Exception e) {
+      throw new GlobalRefDbLockException(project.get(), ref.getName(), e);
+    }
+  }
+
+  @Override
+  public void remove(Project.NameKey project) throws GlobalRefDbSystemError {
+    try {
+      client.delete().deletingChildrenIfNeeded().forPath("/" + project);
+    } catch (Exception e) {
+      throw new GlobalRefDbSystemError(
+          String.format("Not able to delete project '%s'", project), e);
+    }
+  }
+
+  @Override
+  public boolean exists(Project.NameKey project, String refName) throws ZookeeperRuntimeException {
+    try {
+      return client.checkExists().forPath(pathFor(project, refName)) != null;
+    } catch (Exception e) {
+      throw new ZookeeperRuntimeException("Failed to check if path exists in Zookeeper", e);
+    }
+  }
+
+  @Override
+  public Locker lockRef(Project.NameKey project, String refName) throws GlobalRefDbLockException {
+    InterProcessMutex refPathMutex =
+        new InterProcessMutex(client, "/locks" + pathFor(project, refName));
+    try {
+      return new Locker(refPathMutex, transactionLockTimeOut, MILLISECONDS);
+    } catch (Exception e) {
+      throw new GlobalRefDbLockException(project.get(), refName, e);
+    }
+  }
+
+  @Override
+  public boolean compareAndPut(Project.NameKey projectName, Ref oldRef, ObjectId newRefValue)
+      throws GlobalRefDbSystemError {
+
+    final DistributedAtomicValue distributedRefValue =
+        new DistributedAtomicValue(client, pathFor(projectName, oldRef), retryPolicy);
+
+    try {
+      if (oldRef.getObjectId() == null || oldRef.getObjectId().equals(ObjectId.zeroId())) {
+        return distributedRefValue.initialize(writeObjectId(newRefValue));
+      }
+      final ObjectId newValue = newRefValue == null ? ObjectId.zeroId() : newRefValue;
+      final AtomicValue<byte[]> newDistributedValue =
+          distributedRefValue.compareAndSet(
+              writeObjectId(oldRef.getObjectId()), writeObjectId(newValue));
+
+      if (!newDistributedValue.succeeded() && refNotInZk(projectName, oldRef)) {
+        return distributedRefValue.initialize(writeObjectId(newRefValue));
+      }
+
+      return newDistributedValue.succeeded();
+    } catch (Exception e) {
+      logger.atWarning().withCause(e).log(
+          "Error trying to perform CAS at path %s", pathFor(projectName, oldRef));
+      throw new GlobalRefDbSystemError(
+          String.format("Error trying to perform CAS at path %s", pathFor(projectName, oldRef)), e);
+    }
+  }
+
+  private boolean refNotInZk(Project.NameKey projectName, Ref oldRef) throws Exception {
+    return client.checkExists().forPath(pathFor(projectName, oldRef)) == null;
+  }
+
+  static String pathFor(Project.NameKey projectName, Ref oldRef) {
+    return pathFor(projectName, oldRef.getName());
+  }
+
+  static String pathFor(Project.NameKey projectName, String refName) {
+    return "/" + projectName + "/" + refName;
+  }
+
+  static ObjectId readObjectId(byte[] value) {
+    return ObjectId.fromString(value, 0);
+  }
+
+  static byte[] writeObjectId(ObjectId value) {
+    return ObjectId.toString(value).getBytes(StandardCharsets.US_ASCII);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZkValidationModule.java b/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZkValidationModule.java
new file mode 100644
index 0000000..bbbddc9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZkValidationModule.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.validation.dfsrefdb.zookeeper;
+
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDatabase;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Scopes;
+import org.apache.curator.framework.CuratorFramework;
+
+public class ZkValidationModule extends AbstractModule {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private ZookeeperConfig cfg;
+
+  @Inject
+  public ZkValidationModule(ZookeeperConfig cfg) {
+    this.cfg = cfg;
+  }
+
+  @Override
+  protected void configure() {
+    logger.atInfo().log("Shared ref-db engine: Zookeeper");
+    DynamicItem.bind(binder(), GlobalRefDatabase.class)
+        .to(ZkSharedRefDatabase.class)
+        .in(Scopes.SINGLETON);
+    bind(CuratorFramework.class).toInstance(cfg.buildCurator());
+    bind(ZkConnectionConfig.class)
+        .toInstance(
+            new ZkConnectionConfig(cfg.buildCasRetryPolicy(), cfg.getZkInterProcessLockTimeOut()));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZookeeperConfig.java b/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZookeeperConfig.java
new file mode 100644
index 0000000..7523d1f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZookeeperConfig.java
@@ -0,0 +1,205 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.validation.dfsrefdb.zookeeper;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.Inject;
+import org.apache.commons.lang.StringUtils;
+import org.apache.curator.RetryPolicy;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.retry.BoundedExponentialBackoffRetry;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ZookeeperConfig {
+  private static final Logger log = LoggerFactory.getLogger(ZookeeperConfig.class);
+  public static final int defaultSessionTimeoutMs;
+  public static final int defaultConnectionTimeoutMs;
+  public static final String DEFAULT_ZK_CONNECT = "localhost:2181";
+  private final int DEFAULT_RETRY_POLICY_BASE_SLEEP_TIME_MS = 1000;
+  private final int DEFAULT_RETRY_POLICY_MAX_SLEEP_TIME_MS = 3000;
+  private final int DEFAULT_RETRY_POLICY_MAX_RETRIES = 3;
+  private final int DEFAULT_CAS_RETRY_POLICY_BASE_SLEEP_TIME_MS = 100;
+  private final int DEFAULT_CAS_RETRY_POLICY_MAX_SLEEP_TIME_MS = 300;
+  private final int DEFAULT_CAS_RETRY_POLICY_MAX_RETRIES = 3;
+  private final int DEFAULT_TRANSACTION_LOCK_TIMEOUT = 1000;
+
+  static {
+    CuratorFrameworkFactory.Builder b = CuratorFrameworkFactory.builder();
+    defaultSessionTimeoutMs = b.getSessionTimeoutMs();
+    defaultConnectionTimeoutMs = b.getConnectionTimeoutMs();
+  }
+
+  public static final String SUBSECTION = "zookeeper";
+  public static final String KEY_CONNECT_STRING = "connectString";
+  public static final String KEY_SESSION_TIMEOUT_MS = "sessionTimeoutMs";
+  public static final String KEY_CONNECTION_TIMEOUT_MS = "connectionTimeoutMs";
+  public static final String KEY_RETRY_POLICY_BASE_SLEEP_TIME_MS = "retryPolicyBaseSleepTimeMs";
+  public static final String KEY_RETRY_POLICY_MAX_SLEEP_TIME_MS = "retryPolicyMaxSleepTimeMs";
+  public static final String KEY_RETRY_POLICY_MAX_RETRIES = "retryPolicyMaxRetries";
+  public static final String KEY_ROOT_NODE = "rootNode";
+  public final String KEY_CAS_RETRY_POLICY_BASE_SLEEP_TIME_MS = "casRetryPolicyBaseSleepTimeMs";
+  public final String KEY_CAS_RETRY_POLICY_MAX_SLEEP_TIME_MS = "casRetryPolicyMaxSleepTimeMs";
+  public final String KEY_CAS_RETRY_POLICY_MAX_RETRIES = "casRetryPolicyMaxRetries";
+  public final String TRANSACTION_LOCK_TIMEOUT_KEY = "transactionLockTimeoutMs";
+
+  private final String connectionString;
+  private final String root;
+  private final int sessionTimeoutMs;
+  private final int connectionTimeoutMs;
+  private final int baseSleepTimeMs;
+  private final int maxSleepTimeMs;
+  private final int maxRetries;
+  private final int casBaseSleepTimeMs;
+  private final int casMaxSleepTimeMs;
+  private final int casMaxRetries;
+
+  public static final String SECTION = "ref-database";
+  private final Long transactionLockTimeOut;
+
+  private CuratorFramework build;
+
+  @Inject
+  public ZookeeperConfig(PluginConfigFactory cfgFactory, @PluginName String pluginName) {
+    Config zkConfig = cfgFactory.getGlobalPluginConfig(pluginName);
+    connectionString =
+        getString(zkConfig, SECTION, SUBSECTION, KEY_CONNECT_STRING, DEFAULT_ZK_CONNECT);
+    root = getString(zkConfig, SECTION, SUBSECTION, KEY_ROOT_NODE, "gerrit/multi-site");
+    sessionTimeoutMs =
+        getInt(zkConfig, SECTION, SUBSECTION, KEY_SESSION_TIMEOUT_MS, defaultSessionTimeoutMs);
+    connectionTimeoutMs =
+        getInt(
+            zkConfig, SECTION, SUBSECTION, KEY_CONNECTION_TIMEOUT_MS, defaultConnectionTimeoutMs);
+
+    baseSleepTimeMs =
+        getInt(
+            zkConfig,
+            SECTION,
+            SUBSECTION,
+            KEY_RETRY_POLICY_BASE_SLEEP_TIME_MS,
+            DEFAULT_RETRY_POLICY_BASE_SLEEP_TIME_MS);
+
+    maxSleepTimeMs =
+        getInt(
+            zkConfig,
+            SECTION,
+            SUBSECTION,
+            KEY_RETRY_POLICY_MAX_SLEEP_TIME_MS,
+            DEFAULT_RETRY_POLICY_MAX_SLEEP_TIME_MS);
+
+    maxRetries =
+        getInt(
+            zkConfig,
+            SECTION,
+            SUBSECTION,
+            KEY_RETRY_POLICY_MAX_RETRIES,
+            DEFAULT_RETRY_POLICY_MAX_RETRIES);
+
+    casBaseSleepTimeMs =
+        getInt(
+            zkConfig,
+            SECTION,
+            SUBSECTION,
+            KEY_CAS_RETRY_POLICY_BASE_SLEEP_TIME_MS,
+            DEFAULT_CAS_RETRY_POLICY_BASE_SLEEP_TIME_MS);
+
+    casMaxSleepTimeMs =
+        getInt(
+            zkConfig,
+            SECTION,
+            SUBSECTION,
+            KEY_CAS_RETRY_POLICY_MAX_SLEEP_TIME_MS,
+            DEFAULT_CAS_RETRY_POLICY_MAX_SLEEP_TIME_MS);
+
+    casMaxRetries =
+        getInt(
+            zkConfig,
+            SECTION,
+            SUBSECTION,
+            KEY_CAS_RETRY_POLICY_MAX_RETRIES,
+            DEFAULT_CAS_RETRY_POLICY_MAX_RETRIES);
+
+    transactionLockTimeOut =
+        getLong(
+            zkConfig,
+            SECTION,
+            SUBSECTION,
+            TRANSACTION_LOCK_TIMEOUT_KEY,
+            DEFAULT_TRANSACTION_LOCK_TIMEOUT);
+
+    checkArgument(StringUtils.isNotEmpty(connectionString), "zookeeper.%s contains no servers");
+  }
+
+  public CuratorFramework buildCurator() {
+    if (build == null) {
+      this.build =
+          CuratorFrameworkFactory.builder()
+              .connectString(connectionString)
+              .sessionTimeoutMs(sessionTimeoutMs)
+              .connectionTimeoutMs(connectionTimeoutMs)
+              .retryPolicy(
+                  new BoundedExponentialBackoffRetry(baseSleepTimeMs, maxSleepTimeMs, maxRetries))
+              .namespace(root)
+              .build();
+      this.build.start();
+    }
+
+    return this.build;
+  }
+
+  public Long getZkInterProcessLockTimeOut() {
+    return transactionLockTimeOut;
+  }
+
+  public RetryPolicy buildCasRetryPolicy() {
+    return new BoundedExponentialBackoffRetry(casBaseSleepTimeMs, casMaxSleepTimeMs, casMaxRetries);
+  }
+
+  private long getLong(
+      Config cfg, String section, String subSection, String name, long defaultValue) {
+    try {
+      return cfg.getLong(section, subSection, name, defaultValue);
+    } catch (IllegalArgumentException e) {
+      log.error("invalid value for {}; using default value {}", name, defaultValue);
+      log.debug("Failed to retrieve long value: {}", e.getMessage(), e);
+      return defaultValue;
+    }
+  }
+
+  private int getInt(Config cfg, String section, String subSection, String name, int defaultValue) {
+    try {
+      return cfg.getInt(section, subSection, name, defaultValue);
+    } catch (IllegalArgumentException e) {
+      log.error("invalid value for {}; using default value {}", name, defaultValue);
+      log.debug("Failed to retrieve integer value: {}", e.getMessage(), e);
+      return defaultValue;
+    }
+  }
+
+  private String getString(
+      Config cfg, String section, String subsection, String name, String defaultValue) {
+    String value = cfg.getString(section, subsection, name);
+    if (!Strings.isNullOrEmpty(value)) {
+      return value;
+    }
+    return defaultValue;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZookeeperRuntimeException.java b/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZookeeperRuntimeException.java
new file mode 100644
index 0000000..5e386ee
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZookeeperRuntimeException.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.validation.dfsrefdb.zookeeper;
+
+/** Unable to communicate with Zookeeper */
+public class ZookeeperRuntimeException extends RuntimeException {
+  private static final long serialVersionUID = 1L;
+
+  public ZookeeperRuntimeException(String description, Throwable t) {
+    super(description, t);
+  }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
new file mode 100644
index 0000000..03ffab4
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,31 @@
+This plugin is a Zookeeper based implementation of the Global Ref-DB interface.
+
+It is the responsibility of the plugin to store key/pairs of the most recent `sha`
+for each specific mutable refs, by the usage of some sort of atomic Compare and
+Set operation. This enables a series of use-cases, including the detection of
+out-of-sync refs across gerrit sites. It also enables concurrent writes on a
+multi-master setup by enabling a shared locking mechanism for refs across multiple
+nodes.
+
+Originally this code was a part of [multi-site plugin](https://gerrit.googlesource.com/plugins/multi-site/) but currently can be use independently.
+
+## Setup
+
+* Install @PLUGIN@ plugin
+
+Install the zookeeper plugin into the `$GERRIT_SITE/plugins` directory of all
+the Gerrit servers that are part of the cluster.
+
+* Configure @PLUGIN@ plugin
+
+Create the `$GERRIT_SITE/etc/@PLUGIN@.config` on all Gerrit servers with the
+following basic settings. Where `zookeeperhost` is the host that is running zookeeper
+and `2181` is the default zookeeper port, please change them accordingly:
+
+```
+[ref-database "zookeeper"]
+  connectString = "zookeeperhost:2181"
+```
+
+For further information and supported options, refer to [config](config.md)
+documentation.
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
new file mode 100644
index 0000000..9370f82
--- /dev/null
+++ b/src/main/resources/Documentation/build.md
@@ -0,0 +1,45 @@
+# Build
+
+This plugin is built with Bazel in-tree build.
+
+## Build in Gerrit tree
+
+Clone or link zookeeper plugin to the plugins directory of Gerrit's
+source tree. Put the external dependency Bazel build file into the
+Gerrit /plugins directory, replacing the existing empty one.
+
+```
+  cd gerrit/plugins
+  ln -sf @PLUGIN@/external_plugin_deps.bzl .
+```
+
+From the Gerrit source tree issue the command:
+
+```
+  bazel build plugins/@PLUGIN@
+```
+
+The output is created in
+
+```
+  bazel-bin/plugins/@PLUGIN@/@PLUGIN@.jar
+```
+
+This project can be imported into the Eclipse IDE:
+Add the plugin name to the `CUSTOM_PLUGINS_TEST_DEPS`
+set in Gerrit core in `tools/bzl/plugins.bzl`,
+and execute:
+
+```
+  ./tools/eclipse/project.py
+```
+
+To execute the tests run:
+
+```
+  bazel test --test_tag_filters=@PLUGIN@
+```
+
+[Back to @PLUGIN@ documentation index][index]
+
+[index]: index.html
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000..5cd89a5
--- /dev/null
+++ b/src/main/resources/Documentation/config.md
@@ -0,0 +1,83 @@
+
+@PLUGIN@ Configuration
+=========================
+
+Global configuration of the @PLUGIN@ plugin is done in the @PLUGIN@.config file in the site's etc directory.
+
+File '@PLUGIN@.config'
+--------------------
+
+## Sample configuration.
+
+```
+[ref-database "zookeeper"]
+  connectString = "zookeeperhost:2181"
+  rootNode = "/gerrit/multi-site"
+  transactionLockTimeoutMs = 1000
+```
+
+## Configuration parameters
+
+```ref-database.zookeeper.connectString```
+:   Connection string to Zookeeper
+
+```ref-database.zookeeper.rootNode```
+:   Root node to use in Zookeeper to store/retrieve information
+
+    Defaults: "/gerrit/multi-site"
+
+
+```ref-database.zookeeper.sessionTimeoutMs```
+:   Zookeeper session timeout in milliseconds
+
+    Defaults: 1000
+
+```ref-database.zookeeper.connectionTimeoutMs```
+:   Zookeeper connection timeout in milliseconds
+
+    Defaults: 1000
+
+```ref-database.zookeeper.retryPolicyBaseSleepTimeMs```
+:   Configuration for the base sleep timeout in milliseconds of the
+    [BoundedExponentialBackoffRetry](https://curator.apache.org/apidocs/org/apache/curator/retry/BoundedExponentialBackoffRetry.html) [policy](https://curator.apache.org/curator-client/index.html) used for the Zookeeper connection
+
+    Defaults: 1000
+
+```ref-database.zookeeper.retryPolicyMaxSleepTimeMs```
+:   Configuration for the maximum sleep timeout in milliseconds of the
+    [BoundedExponentialBackoffRetry](https://curator.apache.org/apidocs/org/apache/curator/retry/BoundedExponentialBackoffRetry.html) [policy](https://curator.apache.org/curator-client/index.html) used for the Zookeeper connection
+
+    Defaults: 3000
+
+```ref-database.zookeeper.retryPolicyMaxRetries```
+:   Configuration for the maximum number of retries of the
+    [BoundedExponentialBackoffRetry](https://curator.apache.org/apidocs/org/apache/curator/retry/BoundedExponentialBackoffRetry.html) [policy](https://curator.apache.org/curator-client/index.html) used for the Zookeeper connection
+
+    Defaults: 3
+
+```ref-database.zookeeper.casRetryPolicyBaseSleepTimeMs```
+:   Configuration for the base sleep timeout in milliseconds of the
+    [BoundedExponentialBackoffRetry](https://curator.apache.org/apidocs/org/apache/curator/retry/BoundedExponentialBackoffRetry.html) [policy](https://curator.apache.org/curator-client/index.html) used for the Compare and Swap
+    operations on Zookeeper
+
+    Defaults: 1000
+
+```ref-database.zookeeper.casRetryPolicyMaxSleepTimeMs```
+:   Configuration for the maximum sleep timeout in milliseconds of the
+    [BoundedExponentialBackoffRetry](https://curator.apache.org/apidocs/org/apache/curator/retry/BoundedExponentialBackoffRetry.html) [policy](https://curator.apache.org/curator-client/index.html) used for the Compare and Swap
+    operations on Zookeeper
+
+    Defaults: 3000
+
+```ref-database.zookeeper.casRetryPolicyMaxRetries```
+:   Configuration for the maximum number of retries of the
+    [BoundedExponentialBackoffRetry](https://curator.apache.org/apidocs/org/apache/curator/retry/BoundedExponentialBackoffRetry.html) [policy](https://curator.apache.org/curator-client/index.html) used for the Compare and Swap
+    operations on Zookeeper
+
+    Defaults: 3
+
+```ref-database.zookeeper.transactionLockTimeoutMs```
+:   Configuration for the Zookeeper Lock timeout (in milliseconds) used when
+    acquires the exclusive lock for a reference.
+
+    Defaults: 1000
diff --git a/src/test/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/RefFixture.java b/src/test/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/RefFixture.java
new file mode 100644
index 0000000..da79b3c
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/RefFixture.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.validation.dfsrefdb.zookeeper;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Ignore;
+
+@Ignore
+public interface RefFixture {
+
+  String A_TEST_PROJECT_NAME = "A_TEST_PROJECT_NAME";
+  Project.NameKey A_TEST_PROJECT_NAME_KEY = new Project.NameKey(A_TEST_PROJECT_NAME);
+  ObjectId AN_OBJECT_ID_1 = new ObjectId(1, 2, 3, 4, 5);
+  ObjectId AN_OBJECT_ID_2 = new ObjectId(1, 2, 3, 4, 6);
+  ObjectId AN_OBJECT_ID_3 = new ObjectId(1, 2, 3, 4, 7);
+  String A_TEST_REF_NAME = "refs/heads/master";
+
+  default String aBranchRef() {
+    return RefNames.REFS_HEADS + testBranch();
+  }
+
+  default String testBranch() {
+    return "aTestBranch";
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZkSharedRefDatabaseTest.java b/src/test/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZkSharedRefDatabaseTest.java
new file mode 100644
index 0000000..db7024b
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZkSharedRefDatabaseTest.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.validation.dfsrefdb.zookeeper;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.reviewdb.client.Project;
+import org.apache.curator.retry.RetryNTimes;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+public class ZkSharedRefDatabaseTest implements RefFixture {
+  @Rule public TestName nameRule = new TestName();
+
+  ZookeeperTestContainerSupport zookeeperContainer;
+
+  private ZkSharedRefDatabase zkSharedRefDatabase;
+
+  @Before
+  public void setup() {
+    zookeeperContainer = new ZookeeperTestContainerSupport();
+    int SLEEP_BETWEEN_RETRIES_MS = 30;
+    long TRANSACTION_LOCK_TIMEOUT = 1000l;
+    int NUMBER_OF_RETRIES = 5;
+
+    zkSharedRefDatabase =
+        new ZkSharedRefDatabase(
+            zookeeperContainer.getCurator(),
+            new ZkConnectionConfig(
+                new RetryNTimes(NUMBER_OF_RETRIES, SLEEP_BETWEEN_RETRIES_MS),
+                TRANSACTION_LOCK_TIMEOUT));
+  }
+
+  @After
+  public void cleanup() {
+    zookeeperContainer.cleanup();
+  }
+
+  @Test
+  public void shouldCompareAndPutSuccessfully() throws Exception {
+    Ref oldRef = refOf(AN_OBJECT_ID_1);
+    Ref newRef = refOf(AN_OBJECT_ID_2);
+    Project.NameKey projectName = A_TEST_PROJECT_NAME_KEY;
+
+    zookeeperContainer.createRefInZk(projectName, oldRef);
+
+    assertThat(zkSharedRefDatabase.compareAndPut(projectName, oldRef, newRef.getObjectId()))
+        .isTrue();
+  }
+
+  @Test
+  public void shouldFetchLatestObjectIdInZk() throws Exception {
+    Ref oldRef = refOf(AN_OBJECT_ID_1);
+    Ref newRef = refOf(AN_OBJECT_ID_2);
+    Project.NameKey projectName = A_TEST_PROJECT_NAME_KEY;
+
+    zookeeperContainer.createRefInZk(projectName, oldRef);
+
+    assertThat(zkSharedRefDatabase.compareAndPut(projectName, oldRef, newRef.getObjectId()))
+        .isTrue();
+
+    assertThat(zkSharedRefDatabase.isUpToDate(projectName, newRef)).isTrue();
+    assertThat(zkSharedRefDatabase.isUpToDate(projectName, oldRef)).isFalse();
+  }
+
+  @Test
+  public void shouldCompareAndPutWithNullOldRefSuccessfully() throws Exception {
+    Ref oldRef = refOf(null);
+    Ref newRef = refOf(AN_OBJECT_ID_2);
+    Project.NameKey projectName = A_TEST_PROJECT_NAME_KEY;
+
+    assertThat(zkSharedRefDatabase.compareAndPut(projectName, oldRef, newRef.getObjectId()))
+        .isTrue();
+  }
+
+  @Test
+  public void compareAndPutShouldFailIfTheObjectionHasNotTheExpectedValue() throws Exception {
+    Project.NameKey projectName = A_TEST_PROJECT_NAME_KEY;
+
+    Ref oldRef = refOf(AN_OBJECT_ID_1);
+    Ref expectedRef = refOf(AN_OBJECT_ID_2);
+
+    zookeeperContainer.createRefInZk(projectName, oldRef);
+
+    assertThat(zkSharedRefDatabase.compareAndPut(projectName, expectedRef, AN_OBJECT_ID_3))
+        .isFalse();
+  }
+
+  private Ref refOf(ObjectId objectId) {
+    return new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, aBranchRef(), objectId);
+  }
+
+  @Test
+  public void removeProjectShouldRemoveTheWholePathInZk() throws Exception {
+    Project.NameKey projectName = A_TEST_PROJECT_NAME_KEY;
+    Ref someRef = refOf(AN_OBJECT_ID_1);
+
+    zookeeperContainer.createRefInZk(projectName, someRef);
+
+    assertThat(zookeeperContainer.readRefValueFromZk(projectName, someRef))
+        .isEqualTo(AN_OBJECT_ID_1);
+
+    assertThat(getNumChildrenForPath("/")).isEqualTo(1);
+
+    zkSharedRefDatabase.remove(projectName);
+
+    assertThat(getNumChildrenForPath("/")).isEqualTo(0);
+  }
+
+  @Override
+  public String testBranch() {
+    return "branch_" + nameRule.getMethodName();
+  }
+
+  private int getNumChildrenForPath(String path) throws Exception {
+    return zookeeperContainer
+        .getCurator()
+        .checkExists()
+        .forPath(String.format(path))
+        .getNumChildren();
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZookeeperTestContainerSupport.java b/src/test/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZookeeperTestContainerSupport.java
new file mode 100644
index 0000000..3955644
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/validation/dfsrefdb/zookeeper/ZookeeperTestContainerSupport.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.validation.dfsrefdb.zookeeper;
+
+import static com.googlesource.gerrit.plugins.validation.dfsrefdb.zookeeper.ZkSharedRefDatabase.pathFor;
+import static com.googlesource.gerrit.plugins.validation.dfsrefdb.zookeeper.ZkSharedRefDatabase.writeObjectId;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import org.apache.curator.framework.CuratorFramework;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.junit.Ignore;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+@Ignore
+public class ZookeeperTestContainerSupport {
+
+  static class ZookeeperContainer extends GenericContainer<ZookeeperContainer> {
+    public static String ZOOKEEPER_VERSION = "3.4.13";
+
+    public ZookeeperContainer() {
+      super("zookeeper:" + ZOOKEEPER_VERSION);
+    }
+  }
+
+  private ZookeeperContainer container;
+  private ZookeeperConfig configuration;
+  private CuratorFramework curator;
+
+  public CuratorFramework getCurator() {
+    return curator;
+  }
+
+  public ZookeeperContainer getContainer() {
+    return container;
+  }
+
+  @SuppressWarnings("resource")
+  public ZookeeperTestContainerSupport() {
+    container = new ZookeeperContainer().withExposedPorts(2181).waitingFor(Wait.forListeningPort());
+    container.start();
+    Integer zkHostPort = container.getMappedPort(2181);
+    Config sharedRefDbConfig = new Config();
+    String connectString = container.getContainerIpAddress() + ":" + zkHostPort;
+    sharedRefDbConfig.setBoolean("ref-database", null, "enabled", true);
+    sharedRefDbConfig.setString("ref-database", "zookeeper", "connectString", connectString);
+    sharedRefDbConfig.setString(
+        "ref-database",
+        ZookeeperConfig.SUBSECTION,
+        ZookeeperConfig.KEY_CONNECT_STRING,
+        connectString);
+
+    PluginConfigFactory cfgFactory = mock(PluginConfigFactory.class);
+    when(cfgFactory.getGlobalPluginConfig("zookeeper")).thenReturn(sharedRefDbConfig);
+    configuration = new ZookeeperConfig(cfgFactory, "zookeeper");
+
+    this.curator = configuration.buildCurator();
+  }
+
+  public void cleanup() {
+    this.curator.delete();
+    this.container.stop();
+  }
+
+  public ObjectId readRefValueFromZk(Project.NameKey projectName, Ref ref) throws Exception {
+    final byte[] bytes = curator.getData().forPath(pathFor(projectName, ref));
+    return ZkSharedRefDatabase.readObjectId(bytes);
+  }
+
+  public void createRefInZk(Project.NameKey projectName, Ref ref) throws Exception {
+    curator
+        .create()
+        .creatingParentContainersIfNeeded()
+        .forPath(pathFor(projectName, ref), writeObjectId(ref.getObjectId()));
+  }
+}