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