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()));
+ }
+}