Initial version for Gerrit 2.14
This plugin provides an automated way of detecting, managing and
cleaning up (garbage collecting) the 'dirty' repositories in a Gerrit
instance.
It has two components: gc-conductor and gc-executor.
gc-conductor is a Gerrit plugin deployed in the plugins folder of a
Gerrit site. Its main function is to evaluate the dirtiness of
repositories and add them to a queue of repositories to be garbage
collected. This queue is maintained as a database in a postgresql
server.
gc-executor is a runnable jar that picks up the repositories from the
queue and performs the garbage collection operation on them. gc-executor
can be deployed in the same machine that hosts the Gerrit application or
in a different machine that has access to the repositories and the
postgresql server holding the queue.
This initial version is a squash of Ericsson's internal plugin. The code
(script, logging, unit tests) assume that a folder /opt/gerrit exist and
it modifiable by the user running Gerrit. This limitation will be lifted
in a following up change.
Another limitation is because of a bug in JGit fixed by [1], gc-executor
application can only run one GC at a time, even if more than one worker
is configured. One way to lift this limitation is to build a custom JGit
version including that change and use that custom version instead in
gc-executor.
[1]https://git.eclipse.org/r/#/c/127447/
Change-Id: Iadc0854cc0edff2ef42dc01e8e60e3c9c8d7226e
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..72f041f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+/.classpath
+/.primary_build_tool
+/.project
+/.settings/
+/bazel-*
+/eclipse-out/
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..98aa1c2
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,91 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+load(
+ "//tools/bzl:plugin.bzl",
+ "PLUGIN_TEST_DEPS",
+ "gerrit_plugin",
+)
+
+CONDUCTOR_DEPS = [
+ "@postgresql//jar",
+ "@dbcp//jar",
+ "@pool//jar",
+]
+
+EXECUTOR_DEPS = CONDUCTOR_DEPS + [
+ "@jgit//jar",
+ "@javaewah//jar",
+ "@guava//jar",
+ "@guice//jar",
+ "@guice-assistedinject//jar",
+ "@javax_inject//jar",
+ "@aopalliance//jar",
+ "@slf4j-api//jar",
+ "@log4j-slf4j-impl//jar",
+ "@log4j-api//jar",
+ "@log4j-core//jar",
+ "@retry//jar",
+]
+
+gerrit_plugin(
+ name = "gc-conductor",
+ srcs = glob(
+ ["src/main/java/**/*.java"],
+ exclude = ["**/executor/**"],
+ ),
+ manifest_entries = [
+ "Gerrit-PluginName: gc-conductor",
+ "Gerrit-Module: com.ericsson.gerrit.plugins.gcconductor.evaluator.EvaluatorModule",
+ "Gerrit-SshModule: com.ericsson.gerrit.plugins.gcconductor.command.SshModule",
+ "Implementation-Title: gc-conductor plugin",
+ "Implementation-URL: https://gerrit-review.googlesource.com/admin/repos/plugins/gc-conductor",
+ "Implementation-Vendor: Ericsson",
+ ],
+ resources = glob(
+ ["src/main/resources/**/*"],
+ exclude = ["src/main/resources/log4j2.xml"],
+ ),
+ deps = CONDUCTOR_DEPS,
+)
+
+java_library(
+ name = "gc-executor_lib",
+ srcs = glob([
+ "src/main/java/com/ericsson/gerrit/plugins/gcconductor/*.java",
+ "src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/*.java",
+ "src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/*.java",
+ ]),
+ resources = glob([
+ "bin/**/*",
+ "src/main/resources/log4j2.xml",
+ ]),
+ deps = EXECUTOR_DEPS,
+)
+
+java_binary(
+ name = "gc-executor",
+ main_class = "com.ericsson.gerrit.plugins.gcconductor.executor.GcExecutor",
+ runtime_deps = [":gc-executor_lib"],
+)
+
+junit_tests(
+ name = "gc_conductor_tests",
+ srcs = glob(["src/test/java/**/*.java"]),
+ resources = glob(["src/test/resources/**/*"]),
+ tags = ["gc-conductor"],
+ deps = [
+ ":gc-conductor__plugin_test_deps",
+ ],
+)
+
+java_library(
+ name = "gc-conductor__plugin_test_deps",
+ testonly = 1,
+ visibility = ["//visibility:public"],
+ exports = EXECUTOR_DEPS + PLUGIN_TEST_DEPS + [
+ ":gc-conductor__plugin",
+ ":gc-executor_lib",
+ "@byte-buddy//jar",
+ "@mockito//jar",
+ "@objenesis//jar",
+ ],
+)
diff --git a/WORKSPACE b/WORKSPACE
new file mode 100644
index 0000000..b90b611
--- /dev/null
+++ b/WORKSPACE
@@ -0,0 +1,30 @@
+workspace(name = "gc_executor")
+
+load("//:bazlets.bzl", "load_bazlets")
+
+load_bazlets(
+ commit = "11ce7521051ca73598d099aa8a396c9ffe932a74",
+ #local_path = "/home/ehugare/workspaces/bazlets",
+)
+
+#Snapshot Plugin API
+#load(
+# "@com_googlesource_gerrit_bazlets//:gerrit_api_maven_local.bzl",
+# "gerrit_api_maven_local",
+#)
+
+# Load snapshot Plugin API
+#gerrit_api_maven_local()
+
+# Release Plugin API
+load(
+ "@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
+ "gerrit_api",
+)
+
+# Load release Plugin API
+gerrit_api()
+
+load("//:external_plugin_deps.bzl", "external_plugin_deps")
+
+external_plugin_deps()
diff --git a/bazlets.bzl b/bazlets.bzl
new file mode 100644
index 0000000..f97b72c
--- /dev/null
+++ b/bazlets.bzl
@@ -0,0 +1,16 @@
+NAME = "com_googlesource_gerrit_bazlets"
+
+def load_bazlets(
+ commit,
+ local_path = None):
+ if not local_path:
+ native.git_repository(
+ name = NAME,
+ remote = "https://gerrit.googlesource.com/bazlets",
+ commit = commit,
+ )
+ else:
+ native.local_repository(
+ name = NAME,
+ path = local_path,
+ )
diff --git a/bin/gc_ctl b/bin/gc_ctl
new file mode 100755
index 0000000..fcbd38c
--- /dev/null
+++ b/bin/gc_ctl
@@ -0,0 +1,120 @@
+#!/bin/bash
+#
+# controlling script for gc-executor
+export LC_ALL="en_US.UTF-8"
+
+# assume that the current script is in the bin folder
+readonly SERVICE_HOME=$(dirname "$(dirname "$(readlink -f "$0")")")
+readonly SERVICE_NAME=gc-executor
+readonly CONFIG=${SERVICE_HOME}/gc.config
+readonly PATH_TO_JAR="${SERVICE_HOME}/${SERVICE_NAME}.jar"
+readonly PID_FILE="${SERVICE_HOME}/bin/${SERVICE_NAME}-pid"
+
+usage() {
+ me=$(basename "$0")
+ echo "Usage: $me {start|stop [--now]|restart [--now]|status|check}" >&2
+ exit 1
+}
+
+is_running() {
+ [[ -f ${PID_FILE} && $(cat "${PID_FILE}") == $(pgrep -f "${SERVICE_NAME}.jar") ]]
+}
+
+get_config() {
+ if [[ -f "${CONFIG}" ]]; then
+ git config --file "${CONFIG}" "$1" "$2"
+ fi
+}
+
+start() {
+ echo "Starting ${SERVICE_NAME} ..."
+ if is_running; then
+ echo "${SERVICE_NAME} is already running"
+ else
+ java_home=$(get_config --get jvm.javaHome)
+ if [[ -z "${java_home}" ]]; then
+ java_home="/opt/gerrit/jdk8"
+ fi
+ java_options=($(get_config --get-all jvm.javaOptions))
+
+ nohup "${java_home}/bin/java" "${java_options[@]}" -DconfigFile="${CONFIG}" \
+ -jar "${PATH_TO_JAR}" > "${SERVICE_HOME}"/startup.log 2>&1&
+ echo "${!}" > "${PID_FILE}"
+ echo "${SERVICE_NAME} started"
+ fi
+}
+
+stop(){
+ if is_running; then
+ PID=$(cat "${PID_FILE}");
+ echo -n "${SERVICE_NAME} stopping ..."
+ kill "${kill_options}" "${PID}";
+ # wait for the process to die
+ while kill -0 "${PID}" >/dev/null 2>&1; do
+ sleep 1
+ echo -n "."
+ done
+ echo -e "\n${SERVICE_NAME} stopped"
+ rm -f "${PID_FILE}"
+ else
+ echo "${SERVICE_NAME} is not running"
+ fi
+}
+
+restart(){
+ stop
+ start
+}
+
+status(){
+ if is_running; then
+ echo "${SERVICE_NAME} is up."
+ else
+ echo "Looks like ${SERVICE_NAME} is down!"
+ fi
+}
+
+check(){
+ tail -100f /opt/gerrit/review_site/logs/gc/gc.log
+}
+
+main(){
+ action=$1
+ shift
+ kill_options="-TERM"
+
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --now)
+ kill_options="-KILL"
+ shift
+ ;;
+ *)
+ usage
+ esac
+ done
+
+ case "${action}" in
+ start)
+ start
+ ;;
+ stop)
+ stop
+ ;;
+ restart)
+ restart
+ ;;
+ status)
+ status
+ ;;
+ check)
+ check
+ ;;
+ *)
+ echo "${action} is not a known command."
+ usage
+ esac
+ exit 0
+}
+
+main "$@"
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
new file mode 100644
index 0000000..d98226d
--- /dev/null
+++ b/external_plugin_deps.bzl
@@ -0,0 +1,115 @@
+load("//tools/bzl:maven_jar.bzl", "maven_jar")
+
+def external_plugin_deps():
+ maven_jar(
+ name = "mockito",
+ artifact = "org.mockito:mockito-core:2.20.1",
+ sha1 = "a0f57a2efd118bd0f4735cb213f76874cb87b78b",
+ )
+
+ maven_jar(
+ name = "byte-buddy",
+ artifact = "net.bytebuddy:byte-buddy:1.8.15",
+ sha1 = "cb36fe3c70ead5fcd016856a7efff908402d86b8",
+ )
+
+ maven_jar(
+ name = "objenesis",
+ artifact = "org.objenesis:objenesis:2.6",
+ sha1 = "639033469776fd37c08358c6b92a4761feb2af4b",
+ )
+
+ maven_jar(
+ name = "slf4j-api",
+ artifact = "org.slf4j:slf4j-api:1.7.25",
+ sha1 = "da76ca59f6a57ee3102f8f9bd9cee742973efa8a",
+ )
+
+ LOG4J2_VERS = "2.11.1"
+
+ maven_jar(
+ name = "log4j-slf4j-impl",
+ artifact = "org.apache.logging.log4j:log4j-slf4j-impl:" + LOG4J2_VERS,
+ sha1 = "4b41b53a3a2d299ce381a69d165381ca19f62912",
+ )
+
+ maven_jar(
+ name = "log4j-core",
+ artifact = "org.apache.logging.log4j:log4j-core:" + LOG4J2_VERS,
+ sha1 = "592a48674c926b01a9a747c7831bcd82a9e6d6e4",
+ )
+
+ maven_jar(
+ name = "log4j-api",
+ artifact = "org.apache.logging.log4j:log4j-api:" + LOG4J2_VERS,
+ sha1 = "268f0fe4df3eefe052b57c87ec48517d64fb2a10",
+ )
+
+ maven_jar(
+ name = "postgresql",
+ artifact = "org.postgresql:postgresql:42.2.4",
+ sha1 = "dff98730c28a4b3a3263f0cf4abb9a3392f815a7",
+ )
+
+ maven_jar(
+ name = "dbcp",
+ artifact = "commons-dbcp:commons-dbcp:1.4",
+ sha1 = "30be73c965cc990b153a100aaaaafcf239f82d39",
+ )
+
+ maven_jar(
+ name = "pool",
+ artifact = "commons-pool:commons-pool:1.5.5",
+ sha1 = "7d8ffbdc47aa0c5a8afe5dc2aaf512f369f1d19b",
+ )
+
+ maven_jar(
+ name = "guava",
+ artifact = "com.google.guava:guava:25.1-jre",
+ sha1 = "6c57e4b22b44e89e548b5c9f70f0c45fe10fb0b4",
+ )
+
+ GUICE_VERS = "4.2.0"
+
+ maven_jar(
+ name = "guice",
+ artifact = "com.google.inject:guice:" + GUICE_VERS,
+ sha1 = "25e1f4c1d528a1cffabcca0d432f634f3132f6c8",
+ )
+
+ maven_jar(
+ name = "guice-assistedinject",
+ artifact = "com.google.inject.extensions:guice-assistedinject:" + GUICE_VERS,
+ sha1 = "e7270305960ad7db56f7e30cb9df6be9ff1cfb45",
+ )
+
+ maven_jar(
+ name = "aopalliance",
+ artifact = "aopalliance:aopalliance:1.0",
+ sha1 = "0235ba8b489512805ac13a8f9ea77a1ca5ebe3e8",
+ )
+
+ maven_jar(
+ name = "javax_inject",
+ artifact = "javax.inject:javax.inject:1",
+ sha1 = "6975da39a7040257bd51d21a231b76c915872d38",
+ )
+
+ maven_jar(
+ name = "jgit",
+ artifact =
+ "org.eclipse.jgit:org.eclipse.jgit:4.7.2.201807261330-r",
+ sha1 = "6c08ef848fa5f7d5d49776fa25ec24d738ee457d",
+ )
+
+ maven_jar(
+ name = "javaewah",
+ artifact = "com.googlecode.javaewah:JavaEWAH:1.1.6",
+ sha1 = "94ad16d728b374d65bd897625f3fbb3da223a2b6",
+ )
+
+ maven_jar(
+ name = "retry",
+ artifact = "tech.huffman.re-retrying:re-retrying:3.0.0",
+ sha1 = "bd3ce1aaafc0f357354e76890f0a8199a0f42f3a",
+ )
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/CommonConfig.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/CommonConfig.java
new file mode 100644
index 0000000..4081ed0
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/CommonConfig.java
@@ -0,0 +1,114 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor;
+
+import com.google.common.base.Strings;
+import org.eclipse.jgit.lib.Config;
+
+/** Database configuration parameters. */
+public abstract class CommonConfig {
+
+ public static final String DB_URL_KEY = "databaseUrl";
+ public static final String DB_NAME_KEY = "databaseName";
+ public static final String DB_URL_OPTIONS_KEY = "databaseUrlOptions";
+ public static final String DB_USERNAME_KEY = "username";
+ public static final String DB_PASS_KEY = "password";
+ public static final String PACKED_KEY = "packed";
+ public static final String LOOSE_KEY = "loose";
+
+ public static final String DEFAULT_DB_URL = "jdbc:postgresql://localhost:5432/";
+ public static final String DEFAULT_DB_NAME = "gc";
+ public static final String DEFAULT_DB_USERNAME = DEFAULT_DB_NAME;
+ public static final String DEFAULT_DB_PASSWORD = DEFAULT_DB_NAME;
+ public static final int PACKED_DEFAULT = 40;
+ public static final int LOOSE_DEFAULT = 400;
+
+ private final String databaseUrl;
+ private final String databaseName;
+ private final String databaseUrlOptions;
+ private final String username;
+ private final String password;
+ private final int packed;
+ private final int loose;
+
+ /**
+ * Create CommonConfig from the specified parameters.
+ *
+ * @param databaseUrl The database server URL.
+ * @param databaseName The database name.
+ * @param databaseUrlOptions The database URL options.
+ * @param username The database server username.
+ * @param password The password of the database server user.
+ * @param loose The number of loose objects to consider a repo dirty
+ * @param packed The number of packs to consider a repo dirty
+ */
+ public CommonConfig(
+ String databaseUrl,
+ String databaseName,
+ String databaseUrlOptions,
+ String username,
+ String password,
+ int packed,
+ int loose) {
+ this.databaseUrl = databaseUrl.replaceFirst("/?$", "/");
+ this.databaseName = databaseName;
+ this.databaseUrlOptions = databaseUrlOptions;
+ this.username = username;
+ this.password = password;
+ this.packed = packed;
+ this.loose = loose;
+ }
+
+ /** @return the database server URL. */
+ public String getDatabaseUrl() {
+ return databaseUrl;
+ }
+
+ /** @return the database name. */
+ public String getDatabaseName() {
+ return databaseName;
+ }
+
+ /** @return the database URL options or empty if none. */
+ public String getDatabaseUrlOptions() {
+ return databaseUrlOptions;
+ }
+
+ /** @return the database server username. */
+ public String getUsername() {
+ return username;
+ }
+
+ /** @return the password of the database server user. */
+ public String getPassword() {
+ return password;
+ }
+
+ /** @return the number of pack file threshold. */
+ public int getPackedThreshold() {
+ return packed;
+ }
+
+ /** @return the number of loose objects threshold. */
+ public int getLooseThreshold() {
+ return loose;
+ }
+
+ protected static String getString(
+ Config config, String section, String subsection, String key, String defaultValue) {
+ String value = config.getString(section, subsection, key);
+ return Strings.isNullOrEmpty(value) ? defaultValue : value;
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/CommonModule.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/CommonModule.java
new file mode 100644
index 0000000..8606427
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/CommonModule.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor;
+
+import com.ericsson.gerrit.plugins.gcconductor.postgresqueue.PostgresModule;
+import com.google.inject.AbstractModule;
+import com.google.inject.Binding;
+import com.google.inject.Injector;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Module containing common bindings to evaluator and executor */
+public class CommonModule extends AbstractModule {
+
+ private Class<? extends CommonConfig> commonConfig;
+
+ public CommonModule(Class<? extends CommonConfig> commonConfig) {
+ this.commonConfig = commonConfig;
+ }
+
+ @Override
+ protected void configure() {
+ install(new PostgresModule(commonConfig));
+ install(new FactoryModuleBuilder().build(EvaluationTask.Factory.class));
+ }
+
+ @Provides
+ @Singleton
+ @Hostname
+ String provideHostname() throws Exception {
+ ProcessBuilder builder = new ProcessBuilder("hostname", "-s");
+ builder.redirectErrorStream(true);
+ Process process = builder.start();
+ process.waitFor();
+ try (BufferedReader buffer =
+ new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+ return buffer.readLine();
+ }
+ }
+
+ @Provides
+ List<ShutdownListener> provideShutdownListeners(Injector injector) {
+ List<ShutdownListener> listeners = new ArrayList<>();
+ for (Binding<ShutdownListener> shutdownListenerBinding :
+ injector.findBindingsByType(new TypeLiteral<ShutdownListener>() {})) {
+ listeners.add(shutdownListenerBinding.getProvider().get());
+ }
+ return listeners;
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/EvaluationTask.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/EvaluationTask.java
new file mode 100644
index 0000000..2bbade5
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/EvaluationTask.java
@@ -0,0 +1,230 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.internal.storage.file.GC;
+import org.eclipse.jgit.internal.storage.file.GC.RepoStatistics;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RepositoryCache;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.revwalk.ObjectWalk;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Evaluate the dirtiness of a repository. */
+public class EvaluationTask implements Runnable {
+ private static final Logger log = LoggerFactory.getLogger(EvaluationTask.class);
+
+ private final CommonConfig cfg;
+ private final GcQueue queue;
+ private final String hostname;
+
+ private String repositoryPath;
+
+ public interface Factory {
+ /**
+ * Instantiates EvaluationTask objects.
+ *
+ * @param repositoryPath path to the repository to consider.
+ * @return an instance of EvaluationTask.
+ */
+ EvaluationTask create(String repositoryPath);
+ }
+
+ /**
+ * Creates an EvaluationTask object.
+ *
+ * @param cfg The configuration where to read from dirtiness settings
+ * @param queue The queue to add the repository to be garbage collected
+ * @param hostname The hostname where the repository is evaluated.
+ * @param repositoryPath Path to the repository to evaluate.
+ */
+ @Inject
+ public EvaluationTask(
+ CommonConfig cfg, GcQueue queue, @Hostname String hostname, @Assisted String repositoryPath) {
+ this.cfg = cfg;
+ this.queue = queue;
+ this.hostname = hostname;
+ this.repositoryPath = repositoryPath;
+ }
+
+ @Override
+ public void run() {
+ if (!isAlreadyInQueue() && isDirty()) {
+ insertRepository();
+ }
+ }
+
+ private boolean isAlreadyInQueue() {
+ try {
+ return queue.contains(repositoryPath);
+ } catch (GcQueueException e) {
+ log.error("Error checking if repository is already in queue {}", repositoryPath, e);
+ return true;
+ }
+ }
+
+ private boolean isDirty() {
+ try (FileRepository repository =
+ (FileRepository)
+ RepositoryCache.open(FileKey.exact(new File(repositoryPath), FS.DETECTED))) {
+ RepoStatistics statistics = new GC(repository).getStatistics();
+ if (statistics.numberOfPackFiles >= cfg.getPackedThreshold()) {
+ log.debug(
+ "The number of packs ({}) exceeds the configured limit of {}",
+ statistics.numberOfPackFiles,
+ cfg.getPackedThreshold());
+ return true;
+ }
+ long looseObjects = statistics.numberOfLooseObjects;
+ int looseThreshold = cfg.getLooseThreshold();
+ if (looseObjects >= looseThreshold) {
+ long referencedLooseObjects = 0;
+ long unreferencedLooseObjects = 0;
+ long duration = 0;
+ long start = System.currentTimeMillis();
+ unreferencedLooseObjects = getUnreferencedLooseObjectsCount(repository);
+ duration = System.currentTimeMillis() - start;
+ referencedLooseObjects = looseObjects - unreferencedLooseObjects;
+ log.debug(
+ "{} of {} loose objects in repository {} were unreferenced. Evaluating unreferenced objects took {}ms.",
+ unreferencedLooseObjects,
+ looseObjects,
+ repositoryPath,
+ duration);
+ return referencedLooseObjects >= looseThreshold;
+ }
+ } catch (RepositoryNotFoundException rnfe) {
+ log.debug("Repository no longer exist, aborting evaluation.");
+ } catch (IOException e) {
+ log.error("Error gathering '{}' statistics.", repositoryPath, e);
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ int getUnreferencedLooseObjectsCount(FileRepository repo) throws IOException {
+ File objects = repo.getObjectsDirectory();
+ String[] fanout = objects.list();
+ if (fanout == null || fanout.length == 0) {
+ return 0;
+ }
+ Set<ObjectId> unreferencedCandidates = getUnreferencedCandidates(objects, fanout);
+ if (unreferencedCandidates.isEmpty()) {
+ return 0;
+ }
+ try (ObjectWalk walk = new ObjectWalk(repo)) {
+ for (Ref ref : getAllRefs(repo)) {
+ walk.markStart(walk.parseAny(ref.getObjectId()));
+ }
+ removeReferenced(unreferencedCandidates, walk);
+ }
+ return unreferencedCandidates.size();
+ }
+
+ private Set<ObjectId> getUnreferencedCandidates(File objects, String[] fanout) {
+ Set<ObjectId> candidates = new HashSet<>();
+ for (String dir : fanout) {
+ if (dir.length() != 2) {
+ continue;
+ }
+ File[] entries = new File(objects, dir).listFiles();
+ if (entries != null) {
+ addCandidates(candidates, dir, entries);
+ }
+ }
+ return candidates;
+ }
+
+ private void addCandidates(Set<ObjectId> candidates, String dir, File[] entries) {
+ for (File f : entries) {
+ String fileName = f.getName();
+ if (fileName.length() != Constants.OBJECT_ID_STRING_LENGTH - 2) {
+ continue;
+ }
+ try {
+ ObjectId id = ObjectId.fromString(dir + fileName);
+ candidates.add(id);
+ } catch (IllegalArgumentException notAnObject) {
+ // ignoring the file that does not represent loose object
+ }
+ }
+ }
+
+ private Collection<Ref> getAllRefs(FileRepository repo) throws IOException {
+ RefDatabase refdb = repo.getRefDatabase();
+ Collection<Ref> refs = refdb.getRefs(RefDatabase.ALL).values();
+ List<Ref> addl = refdb.getAdditionalRefs();
+ if (!addl.isEmpty()) {
+ List<Ref> all = new ArrayList<>(refs.size() + addl.size());
+ all.addAll(refs);
+ // add additional refs which start with refs/
+ for (Ref r : addl) {
+ if (r.getName().startsWith(Constants.R_REFS)) {
+ all.add(r);
+ }
+ }
+ return all;
+ }
+ return refs;
+ }
+
+ private void removeReferenced(Set<ObjectId> id2File, ObjectWalk w) throws IOException {
+ RevObject ro = w.next();
+ while (ro != null) {
+ if (id2File.remove(ro.getId()) && id2File.isEmpty()) {
+ return;
+ }
+ ro = w.next();
+ }
+ ro = w.nextObject();
+ while (ro != null) {
+ if (id2File.remove(ro.getId()) && id2File.isEmpty()) {
+ return;
+ }
+ ro = w.nextObject();
+ }
+ }
+
+ private void insertRepository() {
+ try {
+ queue.add(repositoryPath, hostname);
+ } catch (GcQueueException e) {
+ log.error("Error adding repository in queue {}", repositoryPath, e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Evaluate if repository need GC: " + repositoryPath;
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/GcQueue.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/GcQueue.java
new file mode 100644
index 0000000..cc2560e
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/GcQueue.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor;
+
+import com.google.inject.Singleton;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Queue that holds Git repositories to be Gc'ed.
+ *
+ * <p>Implementations of this interface should be registered in Guice as {@link Singleton}.
+ */
+public interface GcQueue {
+
+ /**
+ * Add repository to the queue.
+ *
+ * <p>Repositories are unique in the queue. This method is idempotent, adding an already existing
+ * repository will neither add it again nor throw an exception.
+ *
+ * @param repository The path to the repository.
+ * @param queuedFrom The hostname from which the repository is queued from.
+ * @throws GcQueueException if an error occur while adding the repository.
+ */
+ void add(String repository, String queuedFrom) throws GcQueueException;
+
+ /**
+ * Pick a repository from the queue.
+ *
+ * <p>If the queue contains an already picked repository by the specified executor, will return
+ * that repository.
+ *
+ * @param executor The name of the executor to assign repository to.
+ * @param queuedForLongerThan Only pick repository that were in the queue for longer than
+ * specified number of seconds.
+ * @param queuedFrom If specified, only pick repository if queued from the specified hostname.
+ * @return RepositoryInfo representing the repository if any, otherwise return <code>null</code>.
+ * @throws GcQueueException if an error occur while picking a repository.
+ */
+ RepositoryInfo pick(String executor, long queuedForLongerThan, Optional<String> queuedFrom)
+ throws GcQueueException;
+
+ /**
+ * Unpick a repository from the queue.
+ *
+ * @param repository The path to the repository to unpick.
+ * @throws GcQueueException if an error occur while unpicking the repository.
+ */
+ void unpick(String repository) throws GcQueueException;
+
+ /**
+ * Remove a repository from the queue.
+ *
+ * @param repository The path to the repository to remove.
+ * @throws GcQueueException if an error occur while removing the repository.
+ */
+ void remove(String repository) throws GcQueueException;
+
+ /**
+ * Returns <code>true</code> if the queue contains the specified repository.
+ *
+ * @param repository The path to the repository.
+ * @return <code>true</code> if the queue contains the specified repository, otherwise <code>false
+ * </code>.
+ * @throws GcQueueException if an error occur which checking if the repository is in the queue.
+ */
+ boolean contains(String repository) throws GcQueueException;
+
+ /**
+ * Reset all repositories queuedFrom to the specified hostname.
+ *
+ * @param queuedFrom The hostname to set queuedFrom to for every repository.
+ * @throws GcQueueException if an error occur while resetting queuedFrom.
+ */
+ void resetQueuedFrom(String queuedFrom) throws GcQueueException;
+
+ /**
+ * Returns list of repositories with their queuedAt timestamp, queuedFrom hostname and their
+ * associated executor, if any. The repositories are orderer from the oldest to the newest
+ * inserted.
+ *
+ * @return list of RepositoryInfo.
+ * @throws GcQueueException if an error occur while listing the repositories.
+ */
+ List<RepositoryInfo> list() throws GcQueueException;
+
+ /**
+ * Bump an existing repository to the top of the queue.
+ *
+ * @param repository The path to the repository.
+ * @throws GcQueueException if an error occurs while bumping the sequence of a repository.
+ */
+ void bumpToFirst(String repository) throws GcQueueException;
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/GcQueueException.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/GcQueueException.java
new file mode 100644
index 0000000..90702e1
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/GcQueueException.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor;
+
+/**
+ * Indicates that an error happened while interacting with the GcQueue.
+ *
+ * <p>Specific GcQueue implementation errors can be retrieved by calling getCause().
+ */
+public class GcQueueException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs an {@code GcQueueException} with the specified detail message and cause.
+ *
+ * @param message The detail message
+ * @param cause The cause (underlying exception) that explains why the error occurred.
+ */
+ public GcQueueException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/Hostname.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/Hostname.java
new file mode 100644
index 0000000..fd76130
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/Hostname.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface Hostname {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/RepositoryInfo.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/RepositoryInfo.java
new file mode 100644
index 0000000..a7eedec
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/RepositoryInfo.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor;
+
+import java.sql.Timestamp;
+
+/** Information about a queued repository. */
+public class RepositoryInfo {
+
+ private final String path;
+ private final Timestamp queuedAt;
+ private final String executor;
+ private final String queuedFrom;
+
+ public RepositoryInfo(String path, Timestamp queuedAt, String executor, String queuedFrom) {
+ this.path = path;
+ this.queuedAt = queuedAt;
+ this.executor = executor;
+ this.queuedFrom = queuedFrom;
+ }
+
+ /** @return the path to the repository. */
+ public String getPath() {
+ return path;
+ }
+
+ /** @return the time the repository was queued at. */
+ public Timestamp getQueuedAt() {
+ return queuedAt;
+ }
+
+ /** @return the executor running gc on the repository or <code>null</code> if none. */
+ public String getExecutor() {
+ return executor;
+ }
+
+ /** @return the hostname that inserted the repository in the queue. */
+ public String getQueuedFrom() {
+ return queuedFrom;
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownListener.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownListener.java
new file mode 100644
index 0000000..e3db5e3
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownListener.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor;
+
+/** Implement this interface to be notified on shutdown. */
+public interface ShutdownListener {
+
+ /** Called on shutdown. */
+ void onShutdown();
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownNotifier.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownNotifier.java
new file mode 100644
index 0000000..f6bea89
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownNotifier.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor;
+
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.List;
+
+/** Responsible to notify all {@link ShutdownListener} */
+@Singleton
+public class ShutdownNotifier {
+
+ private final List<ShutdownListener> listeners;
+
+ @Inject
+ public ShutdownNotifier(List<ShutdownListener> listeners) {
+ this.listeners = listeners;
+ }
+
+ /** Notify all {@link ShutdownListener}. */
+ public void notifyAllListeners() {
+ for (ShutdownListener listener : Lists.reverse(listeners)) {
+ listener.onShutdown();
+ }
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/AddToQueue.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/AddToQueue.java
new file mode 100644
index 0000000..d9fe107
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/AddToQueue.java
@@ -0,0 +1,117 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.command;
+
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.ericsson.gerrit.plugins.gcconductor.Hostname;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.util.FS;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+@AdminHighPriorityCommand
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(
+ name = "add-to-queue",
+ description = "Add a repository to gc queue",
+ runsAt = MASTER_OR_SLAVE)
+final class AddToQueue extends SshCommand {
+ @Argument(index = 0, required = true, metaVar = "REPOSITORY")
+ private String repository;
+
+ @Option(name = "--first", usage = "add repository as first priority in GC queue")
+ private boolean first;
+
+ @Inject private GcQueue queue;
+
+ @Inject @Hostname private String hostName;
+
+ @Inject private GitRepositoryManager gitRepositoryManager;
+
+ @Inject private ProjectCache projectCache;
+
+ @Override
+ protected void run() throws UnloggedFailure {
+ try {
+ Path repositoryPath = Paths.get(repository);
+ if (repositoryPath.toFile().exists()) {
+ repositoryPath = repositoryPath.toRealPath();
+ }
+ if (!FileKey.isGitRepository(repositoryPath.toFile(), FS.DETECTED)) {
+ repositoryPath = resolvePath();
+ }
+ repository = repositoryPath.toString();
+ queue.add(repository, hostName);
+ if (first) {
+ queue.bumpToFirst(repository);
+ }
+ stdout.println(String.format("%s was added to GC queue", repository));
+ } catch (IOException | GcQueueException e) {
+ throw die(e);
+ }
+ }
+
+ private Path resolvePath() throws UnloggedFailure {
+ if (!(gitRepositoryManager instanceof LocalDiskRepositoryManager)) {
+ throw die("Unable to resolve path to " + repository);
+ }
+ String projectName = extractFrom(repository);
+ Project.NameKey nameKey = new Project.NameKey(projectName);
+ if (projectCache.get(nameKey) == null) {
+ throw die(String.format("Repository %s not found", repository));
+ }
+ LocalDiskRepositoryManager localDiskRepositoryManager =
+ (LocalDiskRepositoryManager) gitRepositoryManager;
+ try {
+ return localDiskRepositoryManager
+ .getBasePath(nameKey)
+ .resolve(projectName.concat(Constants.DOT_GIT_EXT))
+ .toRealPath();
+ } catch (IOException e) {
+ throw die(e);
+ }
+ }
+
+ private String extractFrom(String path) {
+ String name = path;
+ if (name.startsWith("/")) {
+ name = name.substring(1);
+ }
+ if (name.endsWith("/")) {
+ name = name.substring(0, name.length() - 1);
+ }
+ if (name.endsWith(Constants.DOT_GIT_EXT)) {
+ name = name.substring(0, name.indexOf(Constants.DOT_GIT_EXT));
+ }
+ return name;
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/BumpToFirst.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/BumpToFirst.java
new file mode 100644
index 0000000..5f25fba
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/BumpToFirst.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.command;
+
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import org.kohsuke.args4j.Argument;
+
+@AdminHighPriorityCommand
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(
+ name = "bump-to-first",
+ description = "Bumps a repository to first priority for GC",
+ runsAt = MASTER_OR_SLAVE)
+final class BumpToFirst extends SshCommand {
+ @Argument(index = 0, required = true, metaVar = "REPOSITORY")
+ private String repository;
+
+ @Inject private GcQueue queue;
+
+ @Override
+ protected void run() throws UnloggedFailure {
+ try {
+ if (!queue.contains(repository)) {
+ throw die(String.format("%s is not in the queue", repository));
+ }
+ queue.bumpToFirst(repository);
+ } catch (GcQueueException e) {
+ throw die(e);
+ }
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/SetQueuedFrom.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/SetQueuedFrom.java
new file mode 100644
index 0000000..56b70b8
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/SetQueuedFrom.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.command;
+
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import org.kohsuke.args4j.Argument;
+
+@AdminHighPriorityCommand
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(
+ name = "set-queued-from",
+ description = "Set queued from for all unassigned repositories",
+ runsAt = MASTER_OR_SLAVE)
+final class SetQueuedFrom extends SshCommand {
+
+ @Argument(index = 0, required = true, metaVar = "HOSTAME")
+ private String hostname;
+
+ @Inject private GcQueue queue;
+
+ @Override
+ protected void run() throws UnloggedFailure {
+ try {
+ queue.resetQueuedFrom(hostname);
+ } catch (GcQueueException e) {
+ throw die(e);
+ }
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/ShowQueue.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/ShowQueue.java
new file mode 100644
index 0000000..fe41af0
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/ShowQueue.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.command;
+
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.RepositoryInfo;
+import com.google.common.base.Strings;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+@AdminHighPriorityCommand
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "show-queue", description = "Show GC queue", runsAt = MASTER_OR_SLAVE)
+final class ShowQueue extends SshCommand {
+
+ @Inject private GcQueue queue;
+
+ @Override
+ protected void run() throws UnloggedFailure {
+ try {
+ List<RepositoryInfo> repositories = queue.list();
+
+ // Find width of executor and queuedFrom column. Typical executor names
+ // consist of 11 characters hostname suffixed by "-n" and typical queued
+ // from hostname consist of 11 characters
+ int executorColumnWidth = 13;
+ int queuedFromColumnWidth = 11;
+ for (RepositoryInfo repositoryInfo : repositories) {
+ if (repositoryInfo.getExecutor() != null) {
+ executorColumnWidth =
+ Math.max(executorColumnWidth, repositoryInfo.getExecutor().length());
+ }
+ queuedFromColumnWidth =
+ Math.max(queuedFromColumnWidth, repositoryInfo.getQueuedFrom().length());
+ }
+
+ String format = "%-12s %-" + executorColumnWidth + "s %-" + queuedFromColumnWidth + "s %s\n";
+ stdout.print(String.format(format, "Queued At", "Executor", "Queued From", "Repository"));
+ stdout.print(
+ "------------------------------------------------------------------------------\n");
+ for (RepositoryInfo repositoryInfo : repositories) {
+ stdout.print(
+ String.format(
+ format,
+ queuedAt(repositoryInfo.getQueuedAt()),
+ Strings.nullToEmpty(repositoryInfo.getExecutor()),
+ repositoryInfo.getQueuedFrom(),
+ repositoryInfo.getPath()));
+ }
+ stdout.print(
+ "------------------------------------------------------------------------------\n");
+ stdout.print(
+ " "
+ + repositories.size()
+ + " repositor"
+ + (repositories.size() > 1 ? "ies" : "y")
+ + "\n");
+ } catch (Exception e) {
+ throw die(e);
+ }
+ }
+
+ private static String queuedAt(Date when) {
+ if (TimeUtil.nowMs() - when.getTime() < 24 * 60 * 60 * 1000L) {
+ return new SimpleDateFormat("HH:mm:ss.SSS").format(when);
+ }
+ return new SimpleDateFormat("MMM-dd HH:mm").format(when);
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/SshModule.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/SshModule.java
new file mode 100644
index 0000000..7940bba
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/command/SshModule.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.command;
+
+import com.google.gerrit.sshd.PluginCommandModule;
+
+class SshModule extends PluginCommandModule {
+
+ @Override
+ protected void configureCommands() {
+ command(SetQueuedFrom.class);
+ command(ShowQueue.class);
+ command(AddToQueue.class);
+ command(BumpToFirst.class);
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/Evaluator.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/Evaluator.java
new file mode 100644
index 0000000..69e0a37
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/Evaluator.java
@@ -0,0 +1,126 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import com.ericsson.gerrit.plugins.gcconductor.EvaluationTask;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.validators.UploadValidationListener;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.pack.PackStatistics;
+import org.eclipse.jgit.transport.PostUploadHook;
+import org.eclipse.jgit.transport.UploadPack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class Evaluator implements UploadValidationListener, PostUploadHook, GitReferenceUpdatedListener {
+ private static final Logger log = LoggerFactory.getLogger(Evaluator.class);
+ private static final ThreadLocal<String> uploadRepositoryPath = new ThreadLocal<String>() {};
+
+ private final ScheduledThreadPoolExecutor executor;
+ private final EvaluationTask.Factory evaluationTaskFactory;
+ private final GitRepositoryManager repoManager;
+ private final Map<String, Long> timestamps;
+
+ private long expireTime;
+
+ @Inject
+ Evaluator(
+ @EvaluatorExecutor ScheduledThreadPoolExecutor executor,
+ EvaluationTask.Factory evaluationTaskFactory,
+ GitRepositoryManager repoManager,
+ EvaluatorConfig config,
+ @GerritServerConfig Config gerritConfig) {
+ this.executor = executor;
+ this.evaluationTaskFactory = evaluationTaskFactory;
+ this.repoManager = repoManager;
+ this.expireTime = config.getExpireTimeRecheck();
+
+ int threads =
+ gerritConfig.getInt(
+ "receive", null, "threadPoolSize", Runtime.getRuntime().availableProcessors());
+ timestamps = new ConcurrentHashMap<>(10000, 0.75f, threads);
+ }
+
+ @Override
+ public void onPreUpload(
+ Repository repository,
+ Project project,
+ String remoteHost,
+ UploadPack up,
+ Collection<? extends ObjectId> wants,
+ Collection<? extends ObjectId> haves) {
+ uploadRepositoryPath.set(repository.getDirectory().getAbsolutePath());
+ }
+
+ @Override
+ public void onBeginNegotiate(
+ Repository repository,
+ Project project,
+ String remoteHost,
+ UploadPack up,
+ Collection<? extends ObjectId> wants,
+ int cntOffered) {
+ // Do nothing
+ }
+
+ @Override
+ public void onPostUpload(PackStatistics stats) {
+ String repositoryPath = uploadRepositoryPath.get();
+ if (repositoryPath != null && needsCheck(repositoryPath)) {
+ executor.execute(evaluationTaskFactory.create(repositoryPath));
+ uploadRepositoryPath.remove();
+ }
+ }
+
+ @Override
+ public void onGitReferenceUpdated(Event event) {
+ String projectName = event.getProjectName();
+ Project.NameKey projectNameKey = new Project.NameKey(projectName);
+ try (Repository repository = repoManager.openRepository(projectNameKey)) {
+ String repositoryPath = repository.getDirectory().getAbsolutePath();
+ if (needsCheck(repositoryPath)) {
+ executor.execute(evaluationTaskFactory.create(repositoryPath));
+ }
+ } catch (RepositoryNotFoundException e) {
+ log.error("Project not found {}", projectName, e);
+ } catch (IOException e) {
+ log.error("Error getting repository for project {}", projectName, e);
+ }
+ }
+
+ private boolean needsCheck(String repositoryPath) {
+ long now = System.currentTimeMillis();
+ if (!timestamps.containsKey(repositoryPath)
+ || now >= timestamps.get(repositoryPath) + expireTime) {
+ timestamps.put(repositoryPath, now);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorConfig.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorConfig.java
new file mode 100644
index 0000000..acb3600
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorConfig.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import com.ericsson.gerrit.plugins.gcconductor.CommonConfig;
+import com.google.common.base.Strings;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.concurrent.TimeUnit;
+
+/** Holds configuration values of the plugin. */
+@Singleton
+public class EvaluatorConfig extends CommonConfig {
+ static final String THREAD_POOL_KEY = "threadPoolSize";
+ static final String EXPIRE_TIME_RECHECK_KEY = "expireTimeRecheck";
+
+ static final int THREAD_POOL_DEFAULT = 4;
+ static final String EXPIRE_TIME_RECHECK_DEFAULT = "60s";
+
+ private final int threadPoolSize;
+ private final long expireTimeRecheck;
+
+ @Inject
+ EvaluatorConfig(PluginConfig cfg) {
+ super(
+ cfg.getString(DB_URL_KEY, DEFAULT_DB_URL),
+ cfg.getString(DB_NAME_KEY, DEFAULT_DB_NAME),
+ Strings.nullToEmpty(cfg.getString(DB_URL_OPTIONS_KEY)),
+ cfg.getString(DB_USERNAME_KEY, DEFAULT_DB_USERNAME),
+ cfg.getString(DB_PASS_KEY, DEFAULT_DB_PASSWORD),
+ cfg.getInt(PACKED_KEY, PACKED_DEFAULT),
+ cfg.getInt(LOOSE_KEY, LOOSE_DEFAULT));
+ threadPoolSize = cfg.getInt(THREAD_POOL_KEY, THREAD_POOL_DEFAULT);
+
+ String expireTimeRecheckString =
+ cfg.getString(EXPIRE_TIME_RECHECK_KEY, EXPIRE_TIME_RECHECK_DEFAULT);
+ expireTimeRecheck = ConfigUtil.getTimeUnit(expireTimeRecheckString, -1, TimeUnit.MILLISECONDS);
+ }
+
+ /** @return the number of threads to use for the plugin evaluation tasks. */
+ public int getThreadPoolSize() {
+ return threadPoolSize;
+ }
+
+ /** @return the time to wait before re-evaluating the same repository. */
+ public long getExpireTimeRecheck() {
+ return expireTimeRecheck;
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorExecutor.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorExecutor.java
new file mode 100644
index 0000000..6f029f0
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorExecutor.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+@interface EvaluatorExecutor {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorExecutorProvider.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorExecutorProvider.java
new file mode 100644
index 0000000..7cf13af
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorExecutorProvider.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownListener;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+
+@Singleton
+class EvaluatorExecutorProvider implements Provider<ScheduledThreadPoolExecutor>, ShutdownListener {
+ private WorkQueue.Executor executor;
+
+ @Inject
+ EvaluatorExecutorProvider(
+ WorkQueue workQueue, @PluginName String pluginName, EvaluatorConfig config) {
+ executor = workQueue.createQueue(config.getThreadPoolSize(), "[" + pluginName + " plugin]");
+ }
+
+ @Override
+ public void onShutdown() {
+ executor.shutdownNow();
+ executor.unregisterWorkQueue();
+ executor = null;
+ }
+
+ @Override
+ public ScheduledThreadPoolExecutor get() {
+ return executor;
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorModule.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorModule.java
new file mode 100644
index 0000000..8ef2a6b
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorModule.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import com.ericsson.gerrit.plugins.gcconductor.CommonModule;
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownListener;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.git.validators.UploadValidationListener;
+import com.google.inject.Provides;
+import com.google.inject.internal.UniqueAnnotations;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import org.eclipse.jgit.transport.PostUploadHook;
+
+/** Configures bindings of the evaluator. */
+public class EvaluatorModule extends LifecycleModule {
+ @Override
+ protected void configure() {
+ install(new CommonModule(EvaluatorConfig.class));
+ listener().to(OnPluginLoadUnload.class);
+
+ bind(ScheduledThreadPoolExecutor.class)
+ .annotatedWith(EvaluatorExecutor.class)
+ .toProvider(EvaluatorExecutorProvider.class);
+ bind(ShutdownListener.class)
+ .annotatedWith(UniqueAnnotations.create())
+ .to(EvaluatorExecutorProvider.class);
+
+ bind(Evaluator.class);
+ DynamicSet.bind(binder(), UploadValidationListener.class).to(Evaluator.class);
+ DynamicSet.bind(binder(), PostUploadHook.class).to(Evaluator.class);
+ DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(Evaluator.class);
+ bind(EvaluatorConfig.class);
+ }
+
+ @Provides
+ PluginConfig providePluginConfig(PluginConfigFactory config, @PluginName String pluginName) {
+ return config.getFromGerritConfig(pluginName, true);
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/OnPluginLoadUnload.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/OnPluginLoadUnload.java
new file mode 100644
index 0000000..d2de252
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/OnPluginLoadUnload.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownNotifier;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.inject.Inject;
+
+class OnPluginLoadUnload implements LifecycleListener {
+
+ private final ShutdownNotifier shutdownNotifier;
+
+ @Inject
+ OnPluginLoadUnload(ShutdownNotifier shutdownNotifier) {
+ this.shutdownNotifier = shutdownNotifier;
+ }
+
+ @Override
+ public void start() {
+ // nothing to do, only stop method is needed for now
+ }
+
+ @Override
+ public void stop() {
+ shutdownNotifier.notifyAllListeners();
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/CancellableProgressMonitor.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/CancellableProgressMonitor.java
new file mode 100644
index 0000000..3f5a419
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/CancellableProgressMonitor.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import org.eclipse.jgit.lib.EmptyProgressMonitor;
+
+class CancellableProgressMonitor extends EmptyProgressMonitor {
+
+ private boolean cancelled = false;
+
+ void cancel() {
+ cancelled = true;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ConfigUtil.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ConfigUtil.java
new file mode 100644
index 0000000..03b8fd1
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ConfigUtil.java
@@ -0,0 +1,122 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.eclipse.jgit.lib.Config;
+
+class ConfigUtil {
+
+ private ConfigUtil() {}
+
+ /**
+ * Parse a numerical time unit, such as "1 minute", from the configuration.
+ *
+ * @param config the configuration file to read.
+ * @param section section the key is in.
+ * @param setting name of the setting to read.
+ * @param defaultValue default value to return if no value was set in the configuration file.
+ * @param wantUnit the units of {@code defaultValue} and the return value, as well as the units to
+ * assume if the value does not contain an indication of the units.
+ * @return the setting, or {@code defaultValue} if not set, expressed in {@code units}.
+ */
+ public static long getTimeUnit(
+ Config config, String section, String setting, long defaultValue, final TimeUnit wantUnit) {
+ String value = config.getString(section, null, setting);
+ if (value == null) {
+ return defaultValue;
+ }
+
+ String s = value.trim();
+ if (s.length() == 0) {
+ return defaultValue;
+ }
+
+ if (s.startsWith("-") /* negative */) {
+ throw notTimeUnit(section, setting, value);
+ }
+
+ try {
+ return getTimeUnit(s, defaultValue, wantUnit);
+ } catch (IllegalArgumentException notTime) {
+ throw notTimeUnit(section, setting, value);
+ }
+ }
+
+ /**
+ * Parse a numerical time unit, such as "1 minute", from a string.
+ *
+ * @param valueString the string to parse.
+ * @param defaultValue default value to return if no value was set in the configuration file.
+ * @param wantUnit the units of {@code defaultValue} and the return value, as well as the units to
+ * assume if the value does not contain an indication of the units.
+ * @return the setting, or {@code defaultValue} if not set, expressed in {@code units}.
+ */
+ public static long getTimeUnit(String valueString, long defaultValue, TimeUnit wantUnit) {
+ Matcher m = Pattern.compile("^(0|[1-9][0-9]*)\\s*(.*)$").matcher(valueString);
+ if (!m.matches()) {
+ return defaultValue;
+ }
+
+ String digits = m.group(1);
+ String unitName = m.group(2).trim();
+
+ TimeUnit inputUnit;
+ int inputMul;
+
+ if (match(unitName, "h", "hour", "hours")) {
+ inputUnit = TimeUnit.HOURS;
+ inputMul = 1;
+ } else if ("".equals(unitName) || match(unitName, "d", "day", "days")) {
+ inputUnit = TimeUnit.DAYS;
+ inputMul = 1;
+ } else if (match(unitName, "w", "week", "weeks")) {
+ inputUnit = TimeUnit.DAYS;
+ inputMul = 7;
+ } else if (match(unitName, "mon", "month", "months")) {
+ inputUnit = TimeUnit.DAYS;
+ inputMul = 30;
+ } else {
+ throw notTimeUnit(valueString);
+ }
+
+ try {
+ return wantUnit.convert(Long.parseLong(digits) * inputMul, inputUnit);
+ } catch (NumberFormatException nfe) {
+ throw notTimeUnit(valueString);
+ }
+ }
+
+ private static boolean match(String a, String... cases) {
+ for (String b : cases) {
+ if (b.equalsIgnoreCase(a)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static IllegalArgumentException notTimeUnit(
+ String section, String setting, String valueString) {
+ return new IllegalArgumentException(
+ "Invalid time unit value: " + section + "." + setting + " = " + valueString);
+ }
+
+ private static IllegalArgumentException notTimeUnit(String val) {
+ return new IllegalArgumentException("Invalid time unit value: " + val);
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorConfig.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorConfig.java
new file mode 100644
index 0000000..f339302
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorConfig.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static java.time.ZoneId.systemDefault;
+
+import com.ericsson.gerrit.plugins.gcconductor.CommonConfig;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.time.DayOfWeek;
+import java.time.Duration;
+import java.time.LocalTime;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class ExecutorConfig extends CommonConfig {
+ private static final Logger log = LoggerFactory.getLogger(ExecutorConfig.class);
+
+ static final String CORE_SECTION = "core";
+ static final String DB_SECTION = "db";
+ static final String EVALUATION_SECTION = "evaluation";
+
+ static final String DELAY_KEY = "delay";
+ static final String EXECUTOR_KEY = "executors";
+ static final String PICK_OWN_HOST_KEY = "pickOwnHostOnly";
+ static final String REPOS_PATH_KEY = "repositoriesPath";
+ static final String INTERVAL_KEY = "interval";
+ static final String START_TIME_KEY = "startTime";
+
+ static final String EMPTY = "";
+ static final int DEFAULT_EXECUTORS = 2;
+ static final int DEFAULT_DELAY = 0;
+ static final String DEFAULT_REPOS_PATH = "/opt/gerrit/repos";
+ static final long DEFAULT_INTERVAL = -1;
+ static final long DEFAULT_INITIAL_DELAY = -1;
+
+ private final int delay;
+ private final int executors;
+ private final boolean pickOwnHostOnly;
+ private final String repositoriesPath;
+ private final long interval;
+ private final long initialDelay;
+
+ @Inject
+ ExecutorConfig(Config config) {
+ super(
+ getString(config, DB_SECTION, null, DB_URL_KEY, DEFAULT_DB_URL),
+ getString(config, DB_SECTION, null, DB_NAME_KEY, DEFAULT_DB_NAME),
+ Strings.nullToEmpty(getString(config, DB_SECTION, null, DB_URL_OPTIONS_KEY, EMPTY)),
+ getString(config, DB_SECTION, null, DB_USERNAME_KEY, DEFAULT_DB_USERNAME),
+ getString(config, DB_SECTION, null, DB_PASS_KEY, DEFAULT_DB_PASSWORD),
+ config.getInt(EVALUATION_SECTION, PACKED_KEY, PACKED_DEFAULT),
+ config.getInt(EVALUATION_SECTION, LOOSE_KEY, LOOSE_DEFAULT));
+ delay = config.getInt(CORE_SECTION, DELAY_KEY, DEFAULT_DELAY);
+ executors = config.getInt(CORE_SECTION, EXECUTOR_KEY, DEFAULT_EXECUTORS);
+ pickOwnHostOnly = config.getBoolean(CORE_SECTION, PICK_OWN_HOST_KEY, true);
+ repositoriesPath =
+ getString(config, EVALUATION_SECTION, null, REPOS_PATH_KEY, DEFAULT_REPOS_PATH);
+ interval = interval(config, EVALUATION_SECTION, INTERVAL_KEY);
+ initialDelay =
+ initialDelay(
+ config.getString(EVALUATION_SECTION, null, START_TIME_KEY),
+ ZonedDateTime.now(systemDefault()),
+ interval);
+ }
+
+ int getExecutors() {
+ return executors;
+ }
+
+ boolean isPickOwnHostOnly() {
+ return pickOwnHostOnly;
+ }
+
+ int getDelay() {
+ return delay;
+ }
+
+ String getRepositoriesPath() {
+ return repositoriesPath;
+ }
+
+ long getInitialDelay() {
+ return initialDelay;
+ }
+
+ long getInterval() {
+ return interval;
+ }
+
+ private long interval(Config rc, String section, String key) {
+ try {
+ return ConfigUtil.getTimeUnit(rc, section, key, DEFAULT_INTERVAL, TimeUnit.MILLISECONDS);
+ } catch (IllegalArgumentException e) {
+ log.debug("Invalid {}.{} setting. Periodic evaluation disabled", section, key, e);
+ return DEFAULT_INTERVAL;
+ }
+ }
+
+ @VisibleForTesting
+ long initialDelay(String start, ZonedDateTime now, long interval) {
+ if (start == null) {
+ return DEFAULT_INITIAL_DELAY;
+ }
+ try {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("[E ]HH:mm").withLocale(Locale.US);
+ LocalTime firstStartTime = LocalTime.parse(start, formatter);
+ ZonedDateTime startTime = now.with(firstStartTime);
+ Optional<DayOfWeek> dayOfWeek = getDayOfWeek(start, formatter);
+ if (dayOfWeek.isPresent()) {
+ startTime = startTime.with(dayOfWeek.get());
+ }
+ startTime = startTime.truncatedTo(ChronoUnit.MINUTES);
+ long firstDelay = Duration.between(now, startTime).toMillis() % interval;
+ if (firstDelay <= 0) {
+ firstDelay += interval;
+ }
+ return firstDelay;
+ } catch (DateTimeParseException e) {
+ log.debug(
+ "Invalid value {} for {} setting. Periodic evaluation disabled",
+ start,
+ START_TIME_KEY,
+ e);
+ return DEFAULT_INITIAL_DELAY;
+ }
+ }
+
+ private Optional<DayOfWeek> getDayOfWeek(String start, DateTimeFormatter formatter) {
+ try {
+ return Optional.of(formatter.parse(start, DayOfWeek::from));
+ } catch (DateTimeParseException ignored) {
+ // Day of week is an optional parameter.
+ return Optional.empty();
+ }
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorModule.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorModule.java
new file mode 100644
index 0000000..9ca5c5c
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorModule.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import com.ericsson.gerrit.plugins.gcconductor.CommonModule;
+import com.ericsson.gerrit.plugins.gcconductor.Hostname;
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownListener;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+import com.google.inject.internal.UniqueAnnotations;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Config;
+
+/** Configures bindings of the executor. */
+class ExecutorModule extends AbstractModule {
+
+ private final Config config;
+
+ ExecutorModule(Config config) {
+ this.config = config;
+ }
+
+ @Override
+ protected void configure() {
+ install(new CommonModule(ExecutorConfig.class));
+ bind(RuntimeShutdown.class);
+ bind(Config.class).toInstance(config);
+ bind(ExecutorConfig.class);
+ install(new FactoryModuleBuilder().build(GcWorker.Factory.class));
+ bind(ShutdownListener.class).annotatedWith(UniqueAnnotations.create()).to(GcExecutor.class);
+ bind(CancellableProgressMonitor.class);
+ bind(GarbageCollector.class);
+ bind(ScheduledEvaluator.class);
+ bind(ScheduledEvaluationTask.class);
+ }
+
+ @Provides
+ @Singleton
+ @QueuedFrom
+ Optional<String> wasQueuedFrom(ExecutorConfig config, @Hostname String hostname) {
+ return config.isPickOwnHostOnly() ? Optional.of(hostname) : Optional.empty();
+ }
+
+ @Provides
+ @Singleton
+ @QueuedForLongerThan
+ int queuedForLongerThan(ExecutorConfig config) {
+ return config.getDelay();
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GarbageCollector.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GarbageCollector.java
new file mode 100644
index 0000000..bc825d0
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GarbageCollector.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.Callable;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.ProgressMonitor;
+
+class GarbageCollector implements Callable<Boolean> {
+ private String repositoryPath;
+ private ProgressMonitor pm;
+
+ void setRepositoryPath(String repositoryPath) {
+ this.repositoryPath = repositoryPath;
+ }
+
+ void setPm(ProgressMonitor pm) {
+ this.pm = pm;
+ }
+
+ @Override
+ public Boolean call() throws Exception {
+ try (Git git = Git.open(new File(repositoryPath))) {
+ git.gc()
+ .setAggressive(true)
+ .setPreserveOldPacks(true)
+ .setPrunePreserved(true)
+ .setProgressMonitor(pm)
+ .call();
+ return true;
+ } catch (GitAPIException e) {
+ throw new IOException(e);
+ }
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcExecutor.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcExecutor.java
new file mode 100644
index 0000000..e0fb506
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcExecutor.java
@@ -0,0 +1,151 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.ericsson.gerrit.plugins.gcconductor.Hostname;
+import com.ericsson.gerrit.plugins.gcconductor.RepositoryInfo;
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownListener;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Singleton;
+import com.google.inject.Stage;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.logging.log4j.LogManager;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class GcExecutor implements ShutdownListener {
+ private static final Logger log = LoggerFactory.getLogger(GcExecutor.class);
+
+ static final String CONFIG_FILE_PROPERTY = "configFile";
+
+ private final List<GcWorker> workers = new ArrayList<>();
+
+ private final GcQueue queue;
+
+ private final String hostname;
+
+ @Inject
+ GcExecutor(
+ GcQueue queue,
+ ExecutorConfig config,
+ GcWorker.Factory gcWorkerFactory,
+ ScheduledEvaluator scheduledEvaluator,
+ @Hostname String hostname) {
+ this.hostname = hostname;
+ this.queue = queue;
+ unpickRepositories(queue, hostname);
+ startExecutors(config, gcWorkerFactory, hostname);
+ scheduleEvaluation(config, scheduledEvaluator);
+ }
+
+ private void unpickRepositories(GcQueue queue, String hostname) {
+ try {
+ for (RepositoryInfo queuedRepo : queue.list()) {
+ String executor = queuedRepo.getExecutor();
+ if (executor != null && executor.startsWith(hostname)) {
+ queue.unpick(queuedRepo.getPath());
+ }
+ }
+ } catch (GcQueueException e) {
+ log.error("Failed to clear assigned repositories {}", e.getMessage(), e);
+ }
+ }
+
+ private void startExecutors(
+ ExecutorConfig config, GcWorker.Factory gcWorkerFactory, String hostname) {
+ log.info("Starting executors...");
+ synchronized (this) {
+ for (int i = 0; i < config.getExecutors(); i++) {
+ GcWorker worker = gcWorkerFactory.create(hostname + "-" + i);
+ worker.start();
+ workers.add(worker);
+ }
+ }
+ }
+
+ private void scheduleEvaluation(ExecutorConfig config, ScheduledEvaluator scheduledEvaluator) {
+ if (shouldScheduleEvaluation(config)) {
+ scheduledEvaluator.scheduleWith(config.getInitialDelay(), config.getInterval());
+ }
+ }
+
+ private boolean shouldScheduleEvaluation(ExecutorConfig config) {
+ return config.getInitialDelay() > 0 && config.getInterval() > 0;
+ }
+
+ @Override
+ public void onShutdown() {
+ log.info("Shutting down executors...");
+ synchronized (this) {
+ for (GcWorker worker : workers) {
+ worker.shutdown();
+ }
+ for (GcWorker worker : workers) {
+ try {
+ worker.join(1_000);
+ } catch (InterruptedException e) {
+ log.warn("Wait for executors to shutdown interrupted");
+ Thread.currentThread().interrupt();
+ }
+ }
+ unpickRepositories(queue, hostname);
+ }
+ log.info("Executors shut down OK.");
+ }
+
+ public static void main(String[] args) {
+ try {
+ final Injector injector =
+ Guice.createInjector(Stage.PRODUCTION, new ExecutorModule(loadConfig()));
+ // get the GcExecutor class to force start up
+ injector.getInstance(GcExecutor.class);
+ injector.getInstance(RuntimeShutdown.class).waitFor();
+ } catch (Throwable t) {
+ log.error("Uncaught error:", t);
+ }
+ LogManager.shutdown();
+ }
+
+ @VisibleForTesting
+ static Config loadConfig() {
+ String configPath = System.getProperty(CONFIG_FILE_PROPERTY);
+ if (configPath != null) {
+ FileBasedConfig config = new FileBasedConfig(new File(configPath), FS.DETECTED);
+ try {
+ config.load();
+ return config;
+ } catch (IOException | ConfigInvalidException e) {
+ log.error(
+ "Unable to load configuration from file {}. Default values will be used.",
+ configPath,
+ e);
+ }
+ }
+ return new Config();
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcWorker.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcWorker.java
new file mode 100644
index 0000000..14ea8fc
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcWorker.java
@@ -0,0 +1,149 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.ericsson.gerrit.plugins.gcconductor.RepositoryInfo;
+import com.github.rholder.retry.Retryer;
+import com.github.rholder.retry.RetryerBuilder;
+import com.github.rholder.retry.StopStrategies;
+import com.github.rholder.retry.WaitStrategies;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class GcWorker extends Thread {
+ private static final Logger log = LoggerFactory.getLogger(GcWorker.class);
+
+ private final GcQueue queue;
+ private final GarbageCollector gc;
+ private final Optional<String> queuedFrom;
+ private final int queuedForLongerThan;
+ private final String name;
+ private final CancellableProgressMonitor cpm;
+ private final Retryer<Boolean> retryer =
+ RetryerBuilder.<Boolean>newBuilder()
+ .retryIfException()
+ .retryIfRuntimeException()
+ .withWaitStrategy(WaitStrategies.fixedWait(5, TimeUnit.SECONDS))
+ .withStopStrategy(StopStrategies.stopAfterAttempt(3))
+ .build();
+
+ interface Factory {
+ GcWorker create(String name);
+ }
+
+ @Inject
+ GcWorker(
+ GcQueue queue,
+ GarbageCollector gc,
+ CancellableProgressMonitor cpm,
+ @QueuedFrom Optional<String> queuedFrom,
+ @QueuedForLongerThan int queuedForLongerThan,
+ @Assisted String name) {
+ this.queue = queue;
+ this.gc = gc;
+ this.cpm = cpm;
+ this.queuedFrom = queuedFrom;
+ this.queuedForLongerThan = queuedForLongerThan;
+ this.name = name;
+ setName(name);
+ }
+
+ @Override
+ public void run() {
+ while (!cpm.isCancelled()) {
+ String repoPath = pickRepository();
+ if (repoPath != null) {
+ runGc(repoPath);
+ } else {
+ log.debug("No repository picked, going to sleep");
+ try {
+ Thread.sleep(5000);
+ } catch (InterruptedException e) {
+ log.debug("Gc task was interrupted while waiting to pick a repository");
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ }
+ }
+
+ private String pickRepository() {
+ try {
+ RepositoryInfo repoInfo = queue.pick(name, queuedForLongerThan, queuedFrom);
+ if (repoInfo != null) {
+ return repoInfo.getPath();
+ }
+ } catch (GcQueueException e) {
+ log.error("Unable to pick repository from the queue", e);
+ }
+ return null;
+ }
+
+ private void runGc(String repoPath) {
+ try {
+ log.info("Starting gc on repository {}", repoPath);
+ gc.setRepositoryPath(repoPath);
+ gc.setPm(cpm);
+ retryer.call(gc);
+ log.info("Gc completed on repository {}", repoPath);
+ } catch (Throwable e) {
+ if (!cpm.isCancelled()) {
+ log.error(
+ "Gc failed on repository {}. Error Message: {} Cause: {}: {}",
+ repoPath,
+ e.getMessage(),
+ e.getCause(),
+ e.getCause().getStackTrace(),
+ e);
+ }
+ } finally {
+ if (cpm.isCancelled()) {
+ log.warn("Gc on repository {} was cancelled", repoPath);
+ unpickRepository(repoPath);
+ } else {
+ removeRepoFromQueue(repoPath);
+ }
+ }
+ }
+
+ void shutdown() {
+ cpm.cancel();
+ this.interrupt();
+ }
+
+ private void unpickRepository(String repoPath) {
+ try {
+ queue.unpick(repoPath);
+ log.debug("Executor was removed for repository {}", repoPath);
+ } catch (GcQueueException e) {
+ log.error("Unable to remove executor for repository {}", repoPath, e);
+ }
+ }
+
+ private void removeRepoFromQueue(String repoPath) {
+ try {
+ queue.remove(repoPath);
+ log.debug("Repository {} was removed", repoPath);
+ } catch (GcQueueException e) {
+ log.error("Unable to remove repository {} from the queue", repoPath, e);
+ }
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/QueuedForLongerThan.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/QueuedForLongerThan.java
new file mode 100644
index 0000000..48f636c
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/QueuedForLongerThan.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+@interface QueuedForLongerThan {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/QueuedFrom.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/QueuedFrom.java
new file mode 100644
index 0000000..ee4c7ed
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/QueuedFrom.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+@interface QueuedFrom {}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/RuntimeShutdown.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/RuntimeShutdown.java
new file mode 100644
index 0000000..2766a1e
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/RuntimeShutdown.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownNotifier;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class RuntimeShutdown {
+ private static final Logger log = LoggerFactory.getLogger(RuntimeShutdown.class);
+
+ private final ShutdownHook hook;
+ private final ShutdownNotifier shutdownNotifier;
+
+ @Inject
+ RuntimeShutdown(ShutdownNotifier shutdownNotifier) {
+ this.shutdownNotifier = shutdownNotifier;
+ hook = new ShutdownHook();
+ Runtime.getRuntime().addShutdownHook(hook);
+ }
+
+ public void waitFor() {
+ hook.waitForShutdown();
+ }
+
+ private class ShutdownHook extends Thread {
+ private boolean completed;
+
+ ShutdownHook() {
+ setName("ShutdownCallback");
+ }
+
+ @Override
+ public void run() {
+ log.debug("Graceful shutdown requested");
+ shutdownNotifier.notifyAllListeners();
+ log.debug("Shutdown complete");
+ synchronized (this) {
+ completed = true;
+ notifyAll();
+ }
+ }
+
+ void waitForShutdown() {
+ synchronized (this) {
+ while (!completed) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluationTask.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluationTask.java
new file mode 100644
index 0000000..cc0e837
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluationTask.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import com.ericsson.gerrit.plugins.gcconductor.EvaluationTask;
+import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class ScheduledEvaluationTask implements Runnable {
+ private static final Logger log = LoggerFactory.getLogger(ScheduledEvaluationTask.class);
+
+ private final EvaluationTask.Factory evaluationTaskFactory;
+ private final Path repositoriesPath;
+
+ @Inject
+ public ScheduledEvaluationTask(
+ EvaluationTask.Factory evaluationTaskFactory, ExecutorConfig config) {
+ this.evaluationTaskFactory = evaluationTaskFactory;
+ try {
+ repositoriesPath = Paths.get(config.getRepositoriesPath()).normalize().toRealPath();
+ } catch (IOException e) {
+ log.error("Failed to resolve repositoriesPath.", e);
+ throw new ProvisionException("Failed to resolve repositoriesPath: " + e.getMessage());
+ }
+ }
+
+ @Override
+ public void run() {
+ for (String repositoryPath : repositories()) {
+ if (Thread.currentThread().isInterrupted()) {
+ return;
+ }
+ evaluationTaskFactory.create(repositoryPath).run();
+ }
+ }
+
+ private Collection<String> repositories() {
+ ProjectVisitor visitor = new ProjectVisitor(repositoriesPath);
+ try {
+ Files.walkFileTree(
+ visitor.startFolder,
+ EnumSet.of(FileVisitOption.FOLLOW_LINKS),
+ Integer.MAX_VALUE,
+ visitor);
+ } catch (IOException e) {
+ log.error("Error walking repository tree {}", visitor.startFolder.toAbsolutePath(), e);
+ }
+ return Collections.unmodifiableSortedSet(visitor.found);
+ }
+
+ class ProjectVisitor extends SimpleFileVisitor<Path> {
+ private final SortedSet<String> found = new TreeSet<>();
+ private Path startFolder;
+
+ private ProjectVisitor(Path startFolder) {
+ this.startFolder = startFolder;
+ }
+
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
+ if (!dir.equals(startFolder) && isRepo(dir)) {
+ found.add(dir.toString());
+ return FileVisitResult.SKIP_SUBTREE;
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ private boolean isRepo(Path p) {
+ return FileKey.isGitRepository(p.toFile(), FS.DETECTED);
+ }
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluator.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluator.java
new file mode 100644
index 0000000..7997e40
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluator.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownListener;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+class ScheduledEvaluator implements ShutdownListener {
+
+ private final ScheduledEvaluationTask evaluationTask;
+
+ private ScheduledThreadPoolExecutor scheduledExecutor;
+
+ @Inject
+ public ScheduledEvaluator(ScheduledEvaluationTask evaluationTask) {
+ this.evaluationTask = evaluationTask;
+ }
+
+ public void scheduleWith(long initialDelay, long interval) {
+ ThreadFactory threadFactory =
+ new ThreadFactoryBuilder().setNameFormat("ScheduledEvaluator-%d").build();
+ scheduledExecutor = new ScheduledThreadPoolExecutor(1, threadFactory);
+ scheduledExecutor.scheduleAtFixedRate(
+ evaluationTask, initialDelay, interval, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public void onShutdown() {
+ if (scheduledExecutor != null) {
+ scheduledExecutor.shutdownNow();
+ }
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/DatabaseConstants.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/DatabaseConstants.java
new file mode 100644
index 0000000..0b66cf3
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/DatabaseConstants.java
@@ -0,0 +1,145 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.postgresqueue;
+
+import static java.lang.String.format;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Optional;
+import org.apache.commons.dbcp.BasicDataSource;
+
+final class DatabaseConstants {
+
+ static final String DRIVER = "org.postgresql.Driver";
+ static final String INITIAL_DATABASE = "postgres";
+
+ private static final String REPOSITORIES_TABLE = "repositories";
+ static final String REPOSITORY = "repository";
+ static final String EXECUTOR = "executor";
+ private static final String SEQUENCE = "sequence";
+ static final String QUEUED_AT = "queued_at";
+ static final String HOSTNAME = "hostname";
+
+ static final String CREATE_OR_UPDATE_SCHEMA =
+ "DO"
+ + " $$"
+ + " BEGIN"
+ + " CREATE TABLE IF NOT EXISTS "
+ + REPOSITORIES_TABLE
+ + "("
+ + REPOSITORY
+ + " VARCHAR(4096) PRIMARY KEY, "
+ + SEQUENCE
+ + " BIGSERIAL UNIQUE, "
+ + QUEUED_AT
+ + " TIMESTAMP WITHOUT TIME ZONE DEFAULT localtimestamp, "
+ + EXECUTOR
+ + " VARCHAR(258), "
+ + HOSTNAME
+ + " VARCHAR(255) NOT NULL);"
+ // This section is temporary to support migrating live, next version will
+ // drop the executor tables, only drop the foreign key for now.
+ + " IF EXISTS ("
+ + " SELECT constraint_name FROM information_schema.table_constraints"
+ + " WHERE table_name='"
+ + REPOSITORIES_TABLE
+ + "'"
+ + " AND constraint_name='repositories_executor_fkey')"
+ + " THEN"
+ + " ALTER TABLE "
+ + REPOSITORIES_TABLE
+ + " DROP CONSTRAINT repositories_executor_fkey;"
+ + " END IF;"
+ + " END "
+ + " $$";
+
+ private static final String SELECT_REPOSITORIES = "SELECT * FROM " + REPOSITORIES_TABLE;
+
+ static final String SELECT_REPOSITORIES_ORDERED = SELECT_REPOSITORIES + " ORDER BY " + SEQUENCE;
+
+ private DatabaseConstants() {}
+
+ static final String databaseExists(String name) {
+ return "SELECT 1 from pg_database WHERE datname='" + name + "'";
+ }
+
+ static final String createDatabase(String name) {
+ return "CREATE DATABASE " + name;
+ }
+
+ static final String dropDatabase(String name) {
+ return "DROP DATABASE " + name;
+ }
+
+ static final String select(String repository) {
+ return SELECT_REPOSITORIES + " WHERE " + REPOSITORY + "='" + repository + "'";
+ }
+
+ static final String insert(String repository, String hostname) {
+ return format(
+ "INSERT INTO %s (%s,%s) SELECT '%s','%s' WHERE NOT EXISTS (%s)",
+ REPOSITORIES_TABLE, REPOSITORY, HOSTNAME, repository, hostname, select(repository));
+ }
+
+ static final String delete(String repository) {
+ return "DELETE FROM " + REPOSITORIES_TABLE + " WHERE " + REPOSITORY + "='" + repository + "'";
+ }
+
+ static final String updateExecutor(
+ String executor, long queuedForLongerThan, Optional<String> queuedFrom) {
+ return format(
+ "UPDATE %s SET %s='%s' WHERE %s IN (SELECT %s FROM %s WHERE (%s is null OR %s='' OR %s='%s') AND age(localtimestamp, queued_at) > '%s' %s ORDER BY %s LIMIT 1 FOR UPDATE OF %s) RETURNING *",
+ REPOSITORIES_TABLE,
+ EXECUTOR,
+ executor,
+ REPOSITORY,
+ REPOSITORY,
+ REPOSITORIES_TABLE,
+ EXECUTOR,
+ EXECUTOR,
+ EXECUTOR,
+ executor,
+ queuedForLongerThan,
+ (queuedFrom.isPresent() ? " AND '" + queuedFrom.get() + "' LIKE hostname||'%'" : ""),
+ SEQUENCE,
+ REPOSITORIES_TABLE);
+ }
+
+ static final String updateQueuedFrom(String hostname) {
+ return format(
+ "UPDATE %s SET %s='%s' WHERE %s is NULL", REPOSITORIES_TABLE, HOSTNAME, hostname, EXECUTOR);
+ }
+
+ static final String clearExecutor(String repository) {
+ return format(
+ "UPDATE %s SET %s=NULL WHERE %s='%s'",
+ REPOSITORIES_TABLE, EXECUTOR, REPOSITORY, repository);
+ }
+
+ static final String bumpRepository(String repository) {
+ return format(
+ "UPDATE %s SET %s=(SELECT min(%s) FROM %s) -1 WHERE %s='%s'",
+ REPOSITORIES_TABLE, SEQUENCE, SEQUENCE, REPOSITORIES_TABLE, REPOSITORY, repository);
+ }
+
+ static boolean executeStatement(BasicDataSource ds, String query) throws SQLException {
+ try (Connection conn = ds.getConnection();
+ Statement stat = conn.createStatement()) {
+ return stat.execute(query);
+ }
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresModule.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresModule.java
new file mode 100644
index 0000000..24ceba9
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresModule.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.postgresqueue;
+
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.DRIVER;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.INITIAL_DATABASE;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.createDatabase;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.databaseExists;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.executeStatement;
+
+import com.ericsson.gerrit.plugins.gcconductor.CommonConfig;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownListener;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.internal.UniqueAnnotations;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import org.apache.commons.dbcp.BasicDataSource;
+
+/** Configures bindings of the Postgres queue implementation. */
+public class PostgresModule extends AbstractModule {
+
+ private static final long EVICT_IDLE_TIME_MS = 1000L * 60;
+ private Class<? extends CommonConfig> commonConfig;
+
+ public PostgresModule(Class<? extends CommonConfig> commonConfig) {
+ this.commonConfig = commonConfig;
+ }
+
+ @Override
+ protected void configure() {
+ bind(CommonConfig.class).to(commonConfig);
+ bind(GcQueue.class).to(PostgresQueue.class);
+ bind(ShutdownListener.class)
+ .annotatedWith(UniqueAnnotations.create())
+ .to(DatabaseAccessCleanUp.class);
+ }
+
+ /**
+ * Provide access to the gc database. Database will be created if it does not exist.
+ *
+ * @param cfg The database configuration
+ * @return BasicDataSource to access gc database.
+ * @throws SQLException If an error occur while creating the database.
+ */
+ @Provides
+ @Singleton
+ BasicDataSource provideGcDatabaseAccess(CommonConfig cfg) throws SQLException {
+ BasicDataSource adminDataSource = createDataSource(cfg, INITIAL_DATABASE);
+ try (Connection conn = adminDataSource.getConnection();
+ Statement stat = conn.createStatement();
+ ResultSet resultSet = stat.executeQuery(databaseExists(cfg.getDatabaseName()))) {
+ if (!resultSet.next()) {
+ executeStatement(adminDataSource, createDatabase(cfg.getDatabaseName()));
+ }
+ } finally {
+ adminDataSource.close();
+ }
+ return createDataSource(cfg, cfg.getDatabaseName());
+ }
+
+ private static BasicDataSource createDataSource(CommonConfig cfg, String database) {
+ BasicDataSource ds = new BasicDataSource();
+ ds.setDriverClassName(DRIVER);
+ ds.setUrl(cfg.getDatabaseUrl() + database + cfg.getDatabaseUrlOptions());
+ ds.setUsername(cfg.getUsername());
+ ds.setPassword(cfg.getPassword());
+ ds.setMinEvictableIdleTimeMillis(EVICT_IDLE_TIME_MS);
+ ds.setTimeBetweenEvictionRunsMillis(EVICT_IDLE_TIME_MS / 2);
+ return ds;
+ }
+
+ /** Close the database access when plugin is unloaded */
+ @VisibleForTesting
+ static class DatabaseAccessCleanUp implements ShutdownListener {
+ private final BasicDataSource dataSource;
+
+ @Inject
+ public DatabaseAccessCleanUp(BasicDataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ @Override
+ public void onShutdown() {
+ try {
+ dataSource.close();
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to close database connection: " + e.getMessage(), e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresQueue.java b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresQueue.java
new file mode 100644
index 0000000..d2dca5a
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresQueue.java
@@ -0,0 +1,161 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.postgresqueue;
+
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.CREATE_OR_UPDATE_SCHEMA;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.EXECUTOR;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.HOSTNAME;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.QUEUED_AT;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.REPOSITORY;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.SELECT_REPOSITORIES_ORDERED;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.bumpRepository;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.clearExecutor;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.delete;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.executeStatement;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.insert;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.select;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.updateExecutor;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.updateQueuedFrom;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.ericsson.gerrit.plugins.gcconductor.RepositoryInfo;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import org.apache.commons.dbcp.BasicDataSource;
+
+@Singleton
+public class PostgresQueue implements GcQueue {
+
+ private final BasicDataSource dataSource;
+
+ @Inject
+ public PostgresQueue(BasicDataSource dataSource) throws SQLException {
+ this.dataSource = dataSource;
+ executeStatement(dataSource, CREATE_OR_UPDATE_SCHEMA);
+ }
+
+ @Override
+ public void add(String repository, String queuedFrom) throws GcQueueException {
+ try {
+ executeStatement(dataSource, insert(repository, queuedFrom));
+ } catch (SQLException e) {
+ if (!"23505".equals(e.getSQLState())) {
+ // UNIQUE CONSTRAINT violation means repository is already in the queue
+ throw new GcQueueException("Failed to add repository " + repository, e);
+ }
+ }
+ }
+
+ @Override
+ public RepositoryInfo pick(String executor, long queuedForLongerThan, Optional<String> queuedFrom)
+ throws GcQueueException {
+ try (Connection conn = dataSource.getConnection();
+ Statement stat = conn.createStatement();
+ ResultSet resultSet =
+ stat.executeQuery(updateExecutor(executor, queuedForLongerThan, queuedFrom))) {
+ if (resultSet.next()) {
+ return toRepositoryInfo(resultSet);
+ }
+ } catch (SQLException e) {
+ throw new GcQueueException("Failed to pick repository", e);
+ }
+ return null;
+ }
+
+ @Override
+ public void unpick(String repository) throws GcQueueException {
+ try {
+ executeStatement(dataSource, clearExecutor(repository));
+ } catch (SQLException e) {
+ throw new GcQueueException("Failed to unpick repository " + repository, e);
+ }
+ }
+
+ @Override
+ public void remove(String repository) throws GcQueueException {
+ try {
+ executeStatement(dataSource, delete(repository));
+ } catch (SQLException e) {
+ throw new GcQueueException("Failed to remove repository " + repository, e);
+ }
+ }
+
+ @Override
+ public boolean contains(String repository) throws GcQueueException {
+ try (Connection conn = dataSource.getConnection();
+ Statement stat = conn.createStatement();
+ ResultSet resultSet = stat.executeQuery(select(repository))) {
+ if (resultSet.next()) {
+ return true;
+ }
+ } catch (SQLException e) {
+ throw new GcQueueException("Failed to check if queue contains repository " + repository, e);
+ }
+ return false;
+ }
+
+ @Override
+ public void resetQueuedFrom(String queuedFrom) throws GcQueueException {
+ try {
+ executeStatement(dataSource, updateQueuedFrom(queuedFrom));
+ } catch (SQLException e) {
+ throw new GcQueueException("Failed to reset queuedFrom", e);
+ }
+ }
+
+ @Override
+ public List<RepositoryInfo> list() throws GcQueueException {
+ try (Connection conn = dataSource.getConnection();
+ Statement stat = conn.createStatement();
+ ResultSet resultSet = stat.executeQuery(SELECT_REPOSITORIES_ORDERED)) {
+ List<RepositoryInfo> repositories = new ArrayList<>();
+ while (resultSet.next()) {
+ repositories.add(toRepositoryInfo(resultSet));
+ }
+ return repositories;
+ } catch (SQLException e) {
+ throw new GcQueueException("Failed to list repositories ", e);
+ }
+ }
+
+ @Override
+ public void bumpToFirst(String repository) throws GcQueueException {
+ try {
+ executeStatement(dataSource, bumpRepository(repository));
+ } catch (SQLException e) {
+ throw new GcQueueException("Failed to update repository priority ", e);
+ }
+ }
+
+ private RepositoryInfo toRepositoryInfo(ResultSet resultSet) throws SQLException {
+ int repositoryColumn = resultSet.findColumn(REPOSITORY);
+ int queuedAtColumn = resultSet.findColumn(QUEUED_AT);
+ int executorColumn = resultSet.findColumn(EXECUTOR);
+ int hostnameColumn = resultSet.findColumn(HOSTNAME);
+ return new RepositoryInfo(
+ resultSet.getString(repositoryColumn),
+ resultSet.getTimestamp(queuedAtColumn),
+ resultSet.getString(executorColumn),
+ resultSet.getString(hostnameColumn));
+ }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
new file mode 100644
index 0000000..870a2ad
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,37 @@
+This plugin provides an automated way of detecting, managing and cleaning up
+(garbage collecting) the 'dirty' repositories in a Gerrit instance.
+
+It has two components: gc-conductor and gc-executor.
+
+gc-conductor is a Gerrit plugin deployed in the plugins folder of a Gerrit site.
+Its main function is to evaluate the dirtiness of repositories and add them to a
+queue of repositories to be garbage collected. This queue is maintained as a
+database in a postgresql server.
+
+gc-executor is a runnable jar that picks up the repositories from the queue and
+performs the garbage collection operation on them. gc-executor can be deployed
+in the same machine that hosts the Gerrit application or in a different machine
+that has access to the repositories and the postgresql server holding the queue.
+
+Instructions to build gc-conductor and gc-executor can be found in the
+[build documentation][build]. For configuring the two components, see the
+[configuration file][config].
+
+The plugin also provides SSH commands to help managing the repositories queue:
+
+* _add-to-queue_ allows to manually add a repository to the queue.
+* _bump-to-first_ increases the priority of a repository by promoting it to the
+ front of the queue.
+* _show-queue_ shows the list of repositories in the queue.
+* _set-queued-from_ allows to change the identifier of the Gerrit instance that
+ added the repository to the queue. This command is mainly useful in the context
+ of an active-passive redundant configuration when the gc-executor runs in the
+ same machine as the Gerrit application: by default in these cases, gc-executor
+ only picks the repositories that have been added to the queue by the Gerrit
+ instance running in the same host. If one of the executors goes down, it can be
+ helpful to be able to change the _set_queued_from_ field, so that the running
+ one can pick up repositories that were not initially added to the queue by its
+ corresponding Gerrit instance.
+
+[build]: build.html
+[config]: config.html
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
new file mode 100644
index 0000000..e6e9ec1
--- /dev/null
+++ b/src/main/resources/Documentation/build.md
@@ -0,0 +1,113 @@
+Build
+=====
+
+This plugin is built with Bazel and two build modes are supported:
+
+* Standalone
+* In Gerrit tree.
+
+Standalone build mode is recommended, as this mode doesn't require local Gerrit
+tree to exist.
+
+## Build standalone
+
+To build the plugin, issue the following command:
+
+```
+ bazel build @PLUGIN@
+```
+
+The output is created in
+
+```
+ bazel-genfiles/@PLUGIN@.jar
+```
+
+To package the plugin sources run:
+
+```
+ bazel build lib@PLUGIN@__plugin-src.jar
+```
+
+The output is created in:
+
+```
+ bazel-bin/lib@PLUGIN@__plugin-src.jar
+```
+
+To execute the tests run:
+
+```
+ bazel test //...
+```
+
+This project can be imported into the Eclipse IDE. Execute:
+
+```
+ ./tools/eclipse/project.py
+```
+
+to generate the required files and then import the project.
+
+
+To build the executor, issue the following command:
+
+```
+ bazel build gc-executor_deploy.jar
+```
+
+The output is created in:
+
+```
+ /bazel-bin/gc-executor_deploy.jar
+```
+
+This jar should be renamed to gc-executor.jar before deployment.
+
+## Build in Gerrit tree
+
+Clone or link this 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
+ rm external_plugin_deps.bzl
+ ln -s @PLUGIN@/external_plugin_deps.bzl .
+```
+
+From Gerrit source tree issue the command:
+
+```
+ bazel build plugins/@PLUGIN@
+```
+
+The output is created in
+
+```
+ bazel-genfiles/plugins/@PLUGIN@/@PLUGIN@.jar
+```
+
+To execute the tests run:
+
+```
+ bazel test --test_tag_filters=@PLUGIN@
+```
+
+or filtering using the comma separated tags:
+
+````
+ bazel test --test_tag_filters=@PLUGIN@ --strict_java_deps=off //...
+````
+
+This project can be imported into the Eclipse IDE.
+Add the plugin name to the `CUSTOM_PLUGINS` set in
+Gerrit core in `tools/bzl/plugins.bzl`, and execute:
+
+```
+ ./tools/eclipse/project.py
+```
+
+[Back to @PLUGIN@ documentation index][index]
+
+[index]: index.html
diff --git a/src/main/resources/Documentation/cmd-add-to-queue.md b/src/main/resources/Documentation/cmd-add-to-queue.md
new file mode 100644
index 0000000..326174f
--- /dev/null
+++ b/src/main/resources/Documentation/cmd-add-to-queue.md
@@ -0,0 +1,58 @@
+add-to-queue
+=====================
+
+NAME
+----
+add-to-queue - Add a repository to the GC queue
+
+SYNOPSIS
+--------
+> ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ add-to-queue <REPOSITORY> [--first]
+
+DESCRIPTION
+-----------
+Add a repository to the GC queue.
+
+An absolute path to the repository (including the .git suffix) or the project
+name are accepted. A symlink pointing to a repository is also admitted.
+
+Adding a repository to the GC queue is an idempotent operation, i.e., executing
+the command multiple times only add the repository to the queue once.
+
+ACCESS
+------
+Any user who has configured an SSH key and has been granted the
+`Administrate Server` global capability.
+
+SCRIPTING
+---------
+This command is intended to be used in a script.
+
+OPTIONS
+---------
+`--first`
+: Add repository as first priority in GC queue.
+
+EXAMPLES
+--------
+Absolute path to a repository:
+
+```
+$ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ add-to-queue /repos/my/repo.git
+```
+
+Symlink pointing to a repository:
+
+```
+$ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ add-to-queue /opt/gerrit/repos/my/repo.git
+```
+
+Name of the project:
+
+```
+$ ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ add-to-queue my/repo
+```
+
+GERRIT
+------
+Part of [Gerrit Code Review](../../../Documentation/index.html)
diff --git a/src/main/resources/Documentation/cmd-bump-to-first.md b/src/main/resources/Documentation/cmd-bump-to-first.md
new file mode 100644
index 0000000..f05b932
--- /dev/null
+++ b/src/main/resources/Documentation/cmd-bump-to-first.md
@@ -0,0 +1,27 @@
+bump-to-first
+=====================
+
+NAME
+----
+bump-to-first - Update a repository to be first priority in GC queue.
+
+SYNOPSIS
+--------
+> ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ bump-to-first <REPOSITORY>
+
+DESCRIPTION
+-----------
+Update a repository to be first priority in GC queue.
+
+ACCESS
+------
+Any user who has configured an SSH key and has been granted the
+`Administrate Server` global capability.
+
+SCRIPTING
+---------
+This command is intended to be used in a script.
+
+GERRIT
+------
+Part of [Gerrit Code Review](../../../Documentation/index.html)
diff --git a/src/main/resources/Documentation/cmd-set-queued-from.md b/src/main/resources/Documentation/cmd-set-queued-from.md
new file mode 100644
index 0000000..09293f2
--- /dev/null
+++ b/src/main/resources/Documentation/cmd-set-queued-from.md
@@ -0,0 +1,27 @@
+set-queued-from
+=====================
+
+NAME
+----
+set-queued-from - Set queued from for all unassigned repositories
+
+SYNOPSIS
+--------
+> ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ set-queued-from <HOSTNAME>
+
+DESCRIPTION
+-----------
+Set queued from for all unassigned repositories.
+
+ACCESS
+------
+Any user who has configured an SSH key and has been granted the
+`Administrate Server` global capability.
+
+SCRIPTING
+---------
+This command is intended to be used in a script.
+
+GERRIT
+------
+Part of [Gerrit Code Review](../../../Documentation/index.html)
diff --git a/src/main/resources/Documentation/cmd-show-queue.md b/src/main/resources/Documentation/cmd-show-queue.md
new file mode 100644
index 0000000..ab20a85
--- /dev/null
+++ b/src/main/resources/Documentation/cmd-show-queue.md
@@ -0,0 +1,28 @@
+show-queue
+=====================
+
+NAME
+----
+show-queue - Show GC queue
+
+SYNOPSIS
+--------
+> ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ show-queue
+
+DESCRIPTION
+-----------
+Display repositories in gc queue and the executor handling the repository.
+
+ACCESS
+------
+Any user who has configured an SSH key and has been granted the
+`Administrate Server` global capability.
+
+SCRIPTING
+---------
+This command is intended to be used on a need basis by the admins but could also
+be used in a script.
+
+GERRIT
+------
+Part of [Gerrit Code Review](../../../Documentation/index.html)
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000..e0049aa
--- /dev/null
+++ b/src/main/resources/Documentation/config.md
@@ -0,0 +1,190 @@
+@PLUGIN@ Configuration
+======================
+
+gc-conductor uses a postgresql database to manage the queue of repositories that
+need to be garbage collected. The following commands can be used on the postgresql
+database host to create the configured username/password; superuser is required:
+
+```
+ sudo /etc/init.d/postgresql start
+ sudo su - postgres
+ createuser -P -s -e username
+```
+
+File `gerrit.config`
+--------------------
+
+`plugin.@PLUGIN@.packed`
+: Packed threshold. By default, `40`.
+
+`plugin.@PLUGIN@.loose`
+: Loose threshold. By default, `400`.
+
+`plugin.@PLUGIN@.databaseUrl`
+: Database url. By default, `jdbc:postgresql://localhost:5432/`.
+
+`plugin.@PLUGIN@.databaseName`
+: Database name. By default, `gc`.
+
+`plugin.@PLUGIN@.databaseUrlOptions`
+: jdbc option properties to append to the database URL, example
+`?ssl=true&loglevel=org.postgresql.Driver.DEBUG`. Empty by default.
+
+`plugin.@PLUGIN@.username`
+: Database username. By default, `gc`.
+
+`plugin.@PLUGIN@.password`
+: Database password. By default, `gc`.
+
+`plugin.@PLUGIN@.threadPoolSize`
+: Thread pool size. By default, `4`.
+
+`plugin.@PLUGIN@.expireTimeRecheck`
+: Time before a check is considered expired. By default, `60s`.
+
+GC executor
+--------------------
+
+GC executor is packaged as a runnable java jar. The [build documentation][build]
+details the steps to build gc-executor.jar. Once built, gc-executor.jar is deployed
+to the node charged of doing the garbage collection (GC) process.
+
+The configuration can be passed to the gc executor jar by using:
+
+```
+ java -DconfigFile=/path/to/config_file.config -jar executor.jar
+```
+
+### File `gc.config`
+
+The file `gc.config` is a Git-style config file that controls several settings for
+gc executor. The contents of the `gc.config` file are cached at startup. If this
+file is modified, gc executor needs to be restarted in order to be able to use the
+new values.
+
+
+#### Sample `gc.config`:
+
+```
+[jvm]
+ javaHome = /opt/gerrit/jdk8
+ javaOptions = -Xrunjdwp:transport=dt_socket,address=localhost:8788,server=y,suspend=n
+ javaOptions = -Xms1g
+ javaOptions = -Xmx32g
+ javaOptions = -XX:+UseG1GC
+ javaOptions = -XX:MaxGCPauseMillis=2000
+[core]
+ executors = 2
+ pickOwnHostOnly = false
+ delay = 0
+
+[db]
+ databaseUrl = jdbc:postgresql://same_host_as_plugin:5432/
+ databaseName = testDb
+ databaseUrlOptions = ?ssl=true
+ username = testUser
+ password = testPass
+
+[evaluation]
+ packed = 40
+ loose = 400
+ repositoriesPath = /path/to/repositories
+ startTime = Sat 22:00
+ interval = 1 week
+```
+
+#### Section `jvm`
+
+`jvm.javaHome`
+: Location of java. By default /opt/gerrit/jdk8
+
+`jvm.javaOptions`
+: Options to pass along to the Java runtime. If multiple values are
+configured, they are passed in order on the command line, separated by spaces.
+
+#### Section `core`
+
+`core.executors`
+: Number of executors. By default, 2.
+
+`core.pickOwnHostOnly`
+: Whether to pick repositories added to queue from same host only or not.
+By default, true.
+
+`core.delay`
+: minimal delay in seconds a repository must be in queue before it can be
+picked. By default, 0.
+
+#### Section `db`
+
+`db.databaseUrl`
+: Database URL. By default, `jdbc:postgresql://localhost:5432/`.
+
+`db.databaseName`
+: Database name. By default, `gc`.
+
+`db.databaseUrlOptions`
+: jdbc option properties to append to the database URL. For example,
+`?ssl=true&loglevel=org.postgresql.Driver.DEBUG`. Empty by default.
+
+`db.username`
+: Username to connect to the database. By default, `gc`.
+
+`db.password`
+: Password associated to that username. By default, `gc`.
+
+It is important to note here that the parameters used in this section should be
+the same used in `gerrit.config` to define the database settings.
+
+#### Section `evaluation`
+
+This section allows to configure dirtiness evaluation for a list of repositories.
+
+`evaluation.packed`
+: number of pack files in a repository for it to be considered dirty. By
+default, 40.
+
+`evaluation.loose`
+: number of loose objects in a repository for it to be considered dirty.
+By default, 400.
+
+`evaluation.repositoriesPath`
+: path to the repositories to be evaluated for dirtiness. By default,
+/opt/gerrit/repos.
+
+`evaluation.startTime`
+: start time to define the first execution of the repositories dirtiness
+evaluation. Expressed as <day of week> <hours>:<minutes>. By default,
+disabled.
+
+This setting should be expressed using the following time units:
+
+ * <day of week> : Mon, Tue, Wed, Thu, Fri, Sat, Sun
+ * <hours> : 00-23
+ * <minutes> : 00-59
+
+`evaluation.interval`
+: interval for periodic repetition of dirtiness evaluation. By default,
+disabled.
+
+The following suffixes are supported to define the time unit for the interval:
+
+ * h, hour, hours
+ * d, day, days
+ * w, week, weeks (1 week is treated as 7 days)
+ * mon, month, months (1 month is treated as 30 days)
+
+If no time unit is specified, days are assumed.
+
+### GC execution
+
+Executors can be started/stopped using gcctl.sh script.
+
+```
+ /opt/gerrit/gc-conductor/gcctl.sh {start|stop|restart|status|check}
+```
+
+[Back to @PLUGIN@ documentation index][index]
+
+[build]: build.html
+[index]: index.html
diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml
new file mode 100644
index 0000000..7f63ec5
--- /dev/null
+++ b/src/main/resources/log4j2.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN" shutdownHook="disable">
+ <Properties>
+ <Property name="log.path">/opt/gerrit/review_site/logs/gc</Property>
+ </Properties>
+ <Appenders>
+ <RollingFile name="RollingFile" fileName="${log.path}/gc.log"
+ filePattern="${log.path}/gc.log.%d{yyyy-MM-dd}.gz"
+ ignoreExceptions="false">
+ <PatternLayout
+ pattern="%d{yyy-MM-dd HH:mm:ss.SSS} %-5level %logger{1} - %t - %msg%n" />
+ <Policies>
+ <TimeBasedTriggeringPolicy interval="1" modulate="true" />
+ <SizeBasedTriggeringPolicy size="30 MB" />
+ </Policies>
+ </RollingFile>
+ </Appenders>
+ <Loggers>
+ <Logger name="com.ericsson.gerrit.plugins.gcconductor" level="info" />
+ <Root level="error">
+ <AppenderRef ref="RollingFile" />
+ </Root>
+ </Loggers>
+</Configuration>
\ No newline at end of file
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/EvaluationTaskTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/EvaluationTaskTest.java
new file mode 100644
index 0000000..8106a71
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/EvaluationTaskTest.java
@@ -0,0 +1,175 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.gcconductor.evaluator.EvaluatorConfig;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class EvaluationTaskTest {
+
+ @Rule public TemporaryFolder dir = new TemporaryFolder();
+
+ private static final String SOME_HOSTNAME = "hostname";
+
+ @Mock private EvaluatorConfig cfg;
+ @Mock private GcQueue queue;
+
+ private EvaluationTask task;
+ private Repository repository;
+ private String repositoryPath;
+
+ @Before
+ public void setUp() throws Exception {
+ repository = createRepository("someRepo.git");
+ repositoryPath = repository.getDirectory().getAbsolutePath();
+ task = new EvaluationTask(cfg, queue, SOME_HOSTNAME, repositoryPath);
+ }
+
+ @Test
+ public void dirtyRepositoryObjectsShouldBeAddedToTheQueue() throws Exception {
+ when(cfg.getPackedThreshold()).thenReturn(1);
+ addFileTo(repository);
+ when(queue.contains(repositoryPath)).thenReturn(false);
+ task = new EvaluationTask(cfg, queue, SOME_HOSTNAME, repositoryPath);
+ task.run();
+ verify(queue).add(repositoryPath, SOME_HOSTNAME);
+ }
+
+ @Test
+ public void dirtyRepositoryPacksShouldBeAddedToTheQueue() throws Exception {
+ when(cfg.getPackedThreshold()).thenReturn(1);
+ when(queue.contains(repositoryPath)).thenReturn(false);
+ addFileTo(repository);
+ gc(repository);
+ task = new EvaluationTask(cfg, queue, SOME_HOSTNAME, repositoryPath);
+ task.run();
+ verify(queue).add(repositoryPath, SOME_HOSTNAME);
+ }
+
+ @Test
+ public void repositoryShouldNotBeAddedIfAlreadyInQueue() throws Exception {
+ when(queue.contains(repositoryPath)).thenReturn(true);
+ task.run();
+ verify(queue, never()).add(repositoryPath, SOME_HOSTNAME);
+ }
+
+ @Test
+ public void cleanRepositoryShouldNotBeAddedToQueue() throws Exception {
+ when(cfg.getLooseThreshold()).thenReturn(1);
+ when(cfg.getPackedThreshold()).thenReturn(1);
+ when(queue.contains(repositoryPath)).thenReturn(false);
+ task.run();
+ verify(queue, never()).add(repositoryPath, SOME_HOSTNAME);
+ }
+
+ @Test
+ public void queueThrowsErrorCheckingIfRepositoryExists() throws Exception {
+ doThrow(new GcQueueException("some message", new Throwable()))
+ .when(queue)
+ .contains(repositoryPath);
+ task.run();
+ verify(queue, never()).add(repositoryPath, SOME_HOSTNAME);
+ }
+
+ @Test
+ public void queueThrowsErrorInsertingRepository() throws Exception {
+ when(queue.contains(repositoryPath)).thenReturn(false);
+ doThrow(new GcQueueException("some message", new Throwable()))
+ .when(queue)
+ .add(repositoryPath, SOME_HOSTNAME);
+ task.run();
+ }
+
+ @Test
+ public void repositoryNoLongerExist() throws Exception {
+ when(queue.contains(repositoryPath)).thenReturn(false);
+ dir.delete();
+ task.run();
+ verify(queue, never()).add(repositoryPath, SOME_HOSTNAME);
+ }
+
+ @Test
+ public void toStringReturnsProperMessage() {
+ assertThat(task.toString()).isEqualTo("Evaluate if repository need GC: " + repositoryPath);
+ }
+
+ @Test
+ public void noUnreferencedObjects() throws Exception {
+ addFileTo(repository);
+ FileRepository fileRepository = (FileRepository) repository;
+ assertThat(task.getUnreferencedLooseObjectsCount(fileRepository)).isEqualTo(0);
+ }
+
+ @Test
+ public void unreferencedObjectsCountShouldBeOne() throws Exception {
+ try (Git git = new Git(repository)) {
+ RevCommit initialCommit = git.commit().setMessage("initial commit").call();
+ addFileTo(repository);
+ git.reset().setMode(ResetType.HARD).setRef(initialCommit.getName()).call();
+ }
+ FileRepository fileRepository = (FileRepository) repository;
+ removeReflog(fileRepository.getDirectory());
+ assertThat(task.getUnreferencedLooseObjectsCount(fileRepository)).isEqualTo(1);
+ }
+
+ private void removeReflog(File directory) throws IOException {
+ Path logs = directory.toPath().resolve("logs");
+ Files.walk(logs).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
+ }
+
+ private Repository createRepository(String repoName) throws Exception {
+ File repo = dir.newFolder(repoName);
+ try (Git git = Git.init().setDirectory(repo).call()) {
+ return git.getRepository();
+ }
+ }
+
+ private void addFileTo(Repository repository) throws Exception {
+ try (Git git = new Git(repository)) {
+ new File(repository.getDirectory().getParent(), "test");
+ git.add().addFilepattern("test").call();
+ git.commit().setMessage("Add test file").call();
+ }
+ }
+
+ private void gc(Repository repository) throws Exception {
+ try (Git git = new Git(repository)) {
+ git.gc().setAggressive(true).call();
+ }
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/RepositoryInfoTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/RepositoryInfoTest.java
new file mode 100644
index 0000000..ba78afc
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/RepositoryInfoTest.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.sql.Timestamp;
+import org.junit.Test;
+
+public class RepositoryInfoTest {
+
+ @Test
+ public void shouldReturnPath() {
+ String path = "/path/someRepo.git";
+ RepositoryInfo repoInfo = new RepositoryInfo(path, null, null, null);
+ assertThat(repoInfo.getPath()).isEqualTo(path);
+ }
+
+ @Test
+ public void shouldReturnQueuedAt() {
+ Timestamp time = new Timestamp(System.currentTimeMillis());
+ RepositoryInfo repoInfo = new RepositoryInfo(null, time, null, null);
+ assertThat(repoInfo.getQueuedAt()).isEqualTo(time);
+ }
+
+ @Test
+ public void shouldReturnExecutor() {
+ String executor = "someHost-1";
+ RepositoryInfo repoInfo = new RepositoryInfo(null, null, executor, null);
+ assertThat(repoInfo.getExecutor()).isEqualTo(executor);
+ }
+
+ @Test
+ public void shouldReturnQueuedFrom() {
+ String hostname = "someHost-2";
+ RepositoryInfo repoInfo = new RepositoryInfo(null, null, null, hostname);
+ assertThat(repoInfo.getQueuedFrom()).isEqualTo(hostname);
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownNotifierTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownNotifierTest.java
new file mode 100644
index 0000000..59f3498
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/ShutdownNotifierTest.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import com.google.common.collect.Lists;
+import java.util.ArrayList;
+import org.junit.Test;
+
+public class ShutdownNotifierTest {
+
+ @Test
+ public void testNolistener() {
+ ShutdownNotifier notifier = new ShutdownNotifier(new ArrayList<>());
+ notifier.notifyAllListeners();
+ }
+
+ @Test
+ public void testWithlisteners() {
+ ShutdownListener listenerMock = mock(ShutdownListener.class);
+ ShutdownNotifier notifier = new ShutdownNotifier(Lists.newArrayList(listenerMock));
+ notifier.notifyAllListeners();
+ verify(listenerMock).onShutdown();
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorConfigTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorConfigTest.java
new file mode 100644
index 0000000..c63f248
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorConfigTest.java
@@ -0,0 +1,129 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_NAME_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_PASS_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_URL_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_URL_OPTIONS_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_USERNAME_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DEFAULT_DB_NAME;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DEFAULT_DB_PASSWORD;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DEFAULT_DB_URL;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DEFAULT_DB_USERNAME;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.LOOSE_DEFAULT;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.LOOSE_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.PACKED_DEFAULT;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.PACKED_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.evaluator.EvaluatorConfig.EXPIRE_TIME_RECHECK_DEFAULT;
+import static com.ericsson.gerrit.plugins.gcconductor.evaluator.EvaluatorConfig.EXPIRE_TIME_RECHECK_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.evaluator.EvaluatorConfig.THREAD_POOL_DEFAULT;
+import static com.ericsson.gerrit.plugins.gcconductor.evaluator.EvaluatorConfig.THREAD_POOL_KEY;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.PluginConfig;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class EvaluatorConfigTest {
+
+ private static final int PACKED_NOT_DEFAULT = 41;
+ private static final int LOOSE_NOT_DEFAULT = 401;
+ private static final String URL_NOT_DEFAULT = "jdbc:postgresql:test/";
+ private static final String DATABASE_NAME_NOT_DEFAULT = "jdbc:postgresql:test/";
+ private static final String URL_OPTION_NOT_DEFAULT = "?someOption=false";
+ private static final String USER_NOT_DEFAULT = "user";
+ private static final String PASS_NOT_DEFAULT = "pass";
+ private static final int THREAD_POOL_NOT_DEFAULT = 5;
+ private static final String EXPIRE_TIME_RECHECK_NOT_DEFAULT = "10s";
+ private static final boolean USING_DEFAULT_VALUES = true;
+ private static final boolean USING_CUSTOM_VALUES = false;
+
+ @Mock private PluginConfig pluginConfigMock;
+ private EvaluatorConfig configuration;
+
+ @Test
+ public void testValuesNotPresentInGerritConfig() {
+ buildMocks(USING_DEFAULT_VALUES);
+ assertThat(configuration.getPackedThreshold()).isEqualTo(PACKED_DEFAULT);
+ assertThat(configuration.getLooseThreshold()).isEqualTo(LOOSE_DEFAULT);
+ assertThat(configuration.getDatabaseUrl()).isEqualTo(DEFAULT_DB_URL);
+ assertThat(configuration.getDatabaseName()).isEqualTo(DEFAULT_DB_NAME);
+ assertThat(configuration.getDatabaseUrlOptions()).isEmpty();
+ assertThat(configuration.getUsername()).isEqualTo(DEFAULT_DB_USERNAME);
+ assertThat(configuration.getPassword()).isEqualTo(DEFAULT_DB_PASSWORD);
+ assertThat(configuration.getThreadPoolSize()).isEqualTo(THREAD_POOL_DEFAULT);
+ assertThat(configuration.getExpireTimeRecheck())
+ .isEqualTo(convertTimeUnitStringToMilliseconds(EXPIRE_TIME_RECHECK_DEFAULT));
+ }
+
+ @Test
+ public void testValuesPresentInGerritConfig() {
+ buildMocks(USING_CUSTOM_VALUES);
+ assertThat(configuration.getPackedThreshold()).isEqualTo(PACKED_NOT_DEFAULT);
+ assertThat(configuration.getLooseThreshold()).isEqualTo(LOOSE_NOT_DEFAULT);
+ assertThat(configuration.getDatabaseUrl()).isEqualTo(URL_NOT_DEFAULT);
+ assertThat(configuration.getDatabaseName()).isEqualTo(DATABASE_NAME_NOT_DEFAULT);
+ assertThat(configuration.getDatabaseUrlOptions()).isEqualTo(URL_OPTION_NOT_DEFAULT);
+ assertThat(configuration.getUsername()).isEqualTo(USER_NOT_DEFAULT);
+ assertThat(configuration.getPassword()).isEqualTo(PASS_NOT_DEFAULT);
+ assertThat(configuration.getThreadPoolSize()).isEqualTo(THREAD_POOL_NOT_DEFAULT);
+ assertThat(configuration.getExpireTimeRecheck())
+ .isEqualTo(convertTimeUnitStringToMilliseconds(EXPIRE_TIME_RECHECK_NOT_DEFAULT));
+ }
+
+ @Test
+ public void testDatabaseUrlAlwaysEndWithSlash() {
+ when(pluginConfigMock.getString(EXPIRE_TIME_RECHECK_KEY, EXPIRE_TIME_RECHECK_DEFAULT))
+ .thenReturn(EXPIRE_TIME_RECHECK_DEFAULT);
+ when(pluginConfigMock.getString(DB_URL_KEY, DEFAULT_DB_URL)).thenReturn("someUrl");
+
+ configuration = new EvaluatorConfig(pluginConfigMock);
+ assertThat(configuration.getDatabaseUrl()).isEqualTo("someUrl/");
+ }
+
+ private void buildMocks(boolean useDefaults) {
+ when(pluginConfigMock.getInt(PACKED_KEY, PACKED_DEFAULT))
+ .thenReturn(useDefaults ? PACKED_DEFAULT : PACKED_NOT_DEFAULT);
+ when(pluginConfigMock.getInt(LOOSE_KEY, LOOSE_DEFAULT))
+ .thenReturn(useDefaults ? LOOSE_DEFAULT : LOOSE_NOT_DEFAULT);
+ when(pluginConfigMock.getString(DB_URL_KEY, DEFAULT_DB_URL))
+ .thenReturn(useDefaults ? DEFAULT_DB_URL : URL_NOT_DEFAULT);
+ when(pluginConfigMock.getString(DB_NAME_KEY, DEFAULT_DB_NAME))
+ .thenReturn(useDefaults ? DEFAULT_DB_NAME : DATABASE_NAME_NOT_DEFAULT);
+ when(pluginConfigMock.getString(DB_URL_OPTIONS_KEY))
+ .thenReturn(useDefaults ? null : URL_OPTION_NOT_DEFAULT);
+ when(pluginConfigMock.getString(DB_USERNAME_KEY, DEFAULT_DB_USERNAME))
+ .thenReturn(useDefaults ? DEFAULT_DB_USERNAME : USER_NOT_DEFAULT);
+ when(pluginConfigMock.getString(DB_PASS_KEY, DEFAULT_DB_PASSWORD))
+ .thenReturn(useDefaults ? DEFAULT_DB_PASSWORD : PASS_NOT_DEFAULT);
+ when(pluginConfigMock.getInt(THREAD_POOL_KEY, THREAD_POOL_DEFAULT))
+ .thenReturn(useDefaults ? THREAD_POOL_DEFAULT : THREAD_POOL_NOT_DEFAULT);
+ when(pluginConfigMock.getString(EXPIRE_TIME_RECHECK_KEY, EXPIRE_TIME_RECHECK_DEFAULT))
+ .thenReturn(useDefaults ? EXPIRE_TIME_RECHECK_DEFAULT : EXPIRE_TIME_RECHECK_NOT_DEFAULT);
+
+ configuration = new EvaluatorConfig(pluginConfigMock);
+ }
+
+ private long convertTimeUnitStringToMilliseconds(String string) {
+ return ConfigUtil.getTimeUnit(string, -1, TimeUnit.MILLISECONDS);
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorTest.java
new file mode 100644
index 0000000..356e8cf
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/EvaluatorTest.java
@@ -0,0 +1,150 @@
+// Copyright (C) 2016 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.gcconductor.EvaluationTask;
+import com.ericsson.gerrit.plugins.gcconductor.EvaluationTask.Factory;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class EvaluatorTest {
+ private static final String REPOSITORY_PATH = "/path/someRepo.git";
+ private static final Project.NameKey NAME_KEY = new Project.NameKey("testProject");
+
+ @Mock private EvaluationTask task;
+ @Mock private GitReferenceUpdatedListener.Event event;
+ @Mock private GitRepositoryManager repoManager;
+ @Mock private Repository repository;
+ @Mock private ScheduledThreadPoolExecutor executor;
+ @Mock private EvaluatorConfig config;
+ @Mock private Config gerritConfig;
+
+ private Evaluator evaluator;
+
+ @Before
+ public void createEvaluator() {
+ when(event.getProjectName()).thenReturn(NAME_KEY.get());
+ task = new EvaluationTask(null, null, null, REPOSITORY_PATH);
+ when(repository.getDirectory()).thenReturn(new File(REPOSITORY_PATH));
+ Factory eventTaskFactory = mock(Factory.class);
+ when(eventTaskFactory.create(REPOSITORY_PATH)).thenReturn(task);
+ when(config.getExpireTimeRecheck()).thenReturn(0L);
+ when(gerritConfig.getInt(
+ "receive", null, "threadPoolSize", Runtime.getRuntime().availableProcessors()))
+ .thenReturn(1);
+ evaluator = new Evaluator(executor, eventTaskFactory, repoManager, config, gerritConfig);
+ }
+
+ @Test
+ public void onPostUploadShouldCreateTaskOnlyIfPreUploadCalled() {
+ evaluator.onPreUpload(repository, null, null, null, null, null);
+ evaluator.onPostUpload(null);
+ verify(executor).execute(task);
+ }
+
+ @Test
+ public void onPostUploadShouldNotCreateTaskIfRepositoryIsNull() {
+ File fileMock = mock(File.class);
+ when(fileMock.getAbsolutePath()).thenReturn(null);
+ when(repository.getDirectory()).thenReturn(fileMock);
+ evaluator.onPreUpload(repository, null, null, null, null, null);
+ evaluator.onPostUpload(null);
+ verify(executor, never()).execute(task);
+ }
+
+ @Test
+ public void onPostUploadShouldNotCreateTaskRepoIsNull() {
+ evaluator.onPreUpload(repository, null, null, null, null, null);
+ evaluator.onPostUpload(null);
+ evaluator.onPostUpload(null);
+ verify(executor, times(1)).execute(task);
+ }
+
+ @Test
+ public void onPostUploadShouldCreateTaskExpired() {
+ evaluator.onPreUpload(repository, null, null, null, null, null);
+ evaluator.onPostUpload(null);
+ evaluator.onPreUpload(repository, null, null, null, null, null);
+ evaluator.onPostUpload(null);
+ verify(executor, times(2)).execute(task);
+ }
+
+ @Test
+ public void onPostUploadShouldNotCreateTaskNotExpired() {
+ when(config.getExpireTimeRecheck()).thenReturn(1000L);
+ Factory eventTaskFactory = mock(Factory.class);
+ when(eventTaskFactory.create(REPOSITORY_PATH)).thenReturn(task);
+ evaluator = new Evaluator(executor, eventTaskFactory, repoManager, config, gerritConfig);
+ evaluator.onPreUpload(repository, null, null, null, null, null);
+ evaluator.onPostUpload(null);
+ evaluator.onPreUpload(repository, null, null, null, null, null);
+ evaluator.onPostUpload(null);
+ verify(executor, times(1)).execute(task);
+ }
+
+ @Test
+ public void onGitReferenceUpdatedShouldCreateTaskExpired() throws Exception {
+ when(repoManager.openRepository(NAME_KEY)).thenReturn(repository);
+ evaluator.onGitReferenceUpdated(event);
+ evaluator.onGitReferenceUpdated(event);
+ verify(executor, times(2)).execute(task);
+ }
+
+ @Test
+ public void onGitReferenceUpdatedShouldNotCreateTaskNotExpired() throws Exception {
+ when(repoManager.openRepository(NAME_KEY)).thenReturn(repository);
+ when(config.getExpireTimeRecheck()).thenReturn(1000L);
+ Factory eventTaskFactory = mock(Factory.class);
+ when(eventTaskFactory.create(REPOSITORY_PATH)).thenReturn(task);
+ evaluator = new Evaluator(executor, eventTaskFactory, repoManager, config, gerritConfig);
+ evaluator.onGitReferenceUpdated(event);
+ evaluator.onGitReferenceUpdated(event);
+ verify(executor, times(1)).execute(task);
+ }
+
+ @Test
+ public void onGitReferenceUpdatedThrowsRepositoryNotFoundException() throws Exception {
+ doThrow(new RepositoryNotFoundException("")).when(repoManager).openRepository(NAME_KEY);
+ evaluator.onGitReferenceUpdated(event);
+ verify(executor, never()).execute(task);
+ }
+
+ @Test
+ public void onGitReferenceUpdatedThrowsIOException() throws Exception {
+ doThrow(new IOException()).when(repoManager).openRepository(NAME_KEY);
+ evaluator.onGitReferenceUpdated(event);
+ verify(executor, never()).execute(task);
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/OnPluginLoadUnloadTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/OnPluginLoadUnloadTest.java
new file mode 100644
index 0000000..17f92f2
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/evaluator/OnPluginLoadUnloadTest.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.evaluator;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import com.ericsson.gerrit.plugins.gcconductor.ShutdownNotifier;
+import org.junit.Before;
+import org.junit.Test;
+
+public class OnPluginLoadUnloadTest {
+
+ private OnPluginLoadUnload onPluginLoadUnload;
+ private ShutdownNotifier shutdownNotifierMock;
+
+ @Before
+ public void setUp() {
+ shutdownNotifierMock = mock(ShutdownNotifier.class);
+ onPluginLoadUnload = new OnPluginLoadUnload(shutdownNotifierMock);
+ }
+
+ @Test
+ public void testStart() {
+ onPluginLoadUnload.start();
+ verifyZeroInteractions(shutdownNotifierMock);
+ }
+
+ @Test
+ public void testStop() {
+ onPluginLoadUnload.stop();
+ verify(shutdownNotifierMock).notifyAllListeners();
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/CancellableProgressMonitorTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/CancellableProgressMonitorTest.java
new file mode 100644
index 0000000..b235a29
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/CancellableProgressMonitorTest.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class CancellableProgressMonitorTest {
+
+ @Test
+ public void shouldReturnTrueIfThreadIsInterrupted() {
+ CancellableProgressMonitor cpm = new CancellableProgressMonitor();
+ cpm.cancel();
+ assertThat(cpm.isCancelled()).isTrue();
+ }
+
+ @Test
+ public void shouldReturnFalseIfThreadIsNotInterrupted() {
+ CancellableProgressMonitor cpm = new CancellableProgressMonitor();
+ assertThat(cpm.isCancelled()).isFalse();
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ConfigUtilTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ConfigUtilTest.java
new file mode 100644
index 0000000..c99201a
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ConfigUtilTest.java
@@ -0,0 +1,114 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ConfigUtilTest {
+
+ private static final int DEFAULT_VALUE = 1;
+
+ @Mock private Config config;
+
+ @Test
+ public void testGetTimeShouldReturnValue() {
+ when(config.getString("", null, "")).thenReturn("1 WEEK");
+ assertThat(ConfigUtil.getTimeUnit(config, "", "", DEFAULT_VALUE, DAYS)).isEqualTo(7);
+ }
+
+ @Test
+ public void testGetTimeShouldReturnDefault() {
+ when(config.getString("", null, "")).thenReturn(null);
+ assertThat(ConfigUtil.getTimeUnit(config, "", "", DEFAULT_VALUE, DAYS))
+ .isEqualTo(DEFAULT_VALUE);
+
+ when(config.getString("", null, "")).thenReturn("");
+ assertThat(ConfigUtil.getTimeUnit(config, "", "", DEFAULT_VALUE, DAYS))
+ .isEqualTo(DEFAULT_VALUE);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetTimeThrowsExceptionIfNegativeValue() {
+ when(config.getString("", null, "")).thenReturn("-1");
+ assertThat(ConfigUtil.getTimeUnit(config, "", "", DEFAULT_VALUE, DAYS))
+ .isEqualTo(DEFAULT_VALUE);
+
+ when(config.getString("", null, "")).thenReturn("");
+ assertThat(ConfigUtil.getTimeUnit(config, "", "", DEFAULT_VALUE, DAYS))
+ .isEqualTo(DEFAULT_VALUE);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetTimeThrowsExceptionIfBadTimeUnit() {
+ when(config.getString("", null, "")).thenReturn("1s");
+ assertThat(ConfigUtil.getTimeUnit(config, "", "", DEFAULT_VALUE, DAYS))
+ .isEqualTo(DEFAULT_VALUE);
+ }
+
+ @Test
+ public void testTimeUnit() {
+ assertEquals(ms(5, HOURS), parse("5h"));
+ assertEquals(ms(1, HOURS), parse("1hour"));
+ assertEquals(ms(48, HOURS), parse("48hours"));
+
+ assertEquals(ms(5, HOURS), parse("5 h"));
+ assertEquals(ms(1, HOURS), parse("1 hour"));
+ assertEquals(ms(48, HOURS), parse("48 hours"));
+ assertEquals(ms(48, HOURS), parse("48 \t \r hours"));
+
+ assertEquals(ms(4, DAYS), parse("4 d"));
+ assertEquals(ms(1, DAYS), parse("1day"));
+ assertEquals(ms(14, DAYS), parse("14days"));
+
+ assertEquals(ms(7, DAYS), parse("1 w"));
+ assertEquals(ms(7, DAYS), parse("1week"));
+ assertEquals(ms(14, DAYS), parse("2w"));
+ assertEquals(ms(14, DAYS), parse("2weeks"));
+
+ assertEquals(ms(30, DAYS), parse("1 mon"));
+ assertEquals(ms(30, DAYS), parse("1month"));
+ assertEquals(ms(60, DAYS), parse("2mon"));
+ assertEquals(ms(60, DAYS), parse("2months"));
+
+ assertEquals(ms(60, DAYS), parse("60"));
+ assertEquals(ms(1, MILLISECONDS), parse(""));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testUnsupportedTimeUnit() {
+ parse("1 min");
+ }
+
+ private static long ms(int cnt, TimeUnit unit) {
+ return MILLISECONDS.convert(cnt, unit);
+ }
+
+ private static long parse(String string) {
+ return ConfigUtil.getTimeUnit(string, 1, MILLISECONDS);
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorConfigTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorConfigTest.java
new file mode 100644
index 0000000..198eca9
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorConfigTest.java
@@ -0,0 +1,169 @@
+package com.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_NAME_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_PASS_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_URL_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_URL_OPTIONS_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DB_USERNAME_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DEFAULT_DB_NAME;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DEFAULT_DB_PASSWORD;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DEFAULT_DB_URL;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.DEFAULT_DB_USERNAME;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.LOOSE_DEFAULT;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.LOOSE_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.PACKED_DEFAULT;
+import static com.ericsson.gerrit.plugins.gcconductor.CommonConfig.PACKED_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.CORE_SECTION;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.DB_SECTION;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.DEFAULT_DELAY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.DEFAULT_EXECUTORS;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.DEFAULT_INITIAL_DELAY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.DEFAULT_INTERVAL;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.DEFAULT_REPOS_PATH;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.DELAY_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.EMPTY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.EVALUATION_SECTION;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.EXECUTOR_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.INTERVAL_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.PICK_OWN_HOST_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.REPOS_PATH_KEY;
+import static com.ericsson.gerrit.plugins.gcconductor.executor.ExecutorConfig.START_TIME_KEY;
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ExecutorConfigTest {
+ private static final String CUSTOM_REPOS_PATH = "/other/path/to/repos";
+ private static final String CUSTOM_DB_URL = "customDbUrl/";
+ private static final String CUSTOM_DB_NAME = "customDbName";
+ private static final String URL_OPTIONS = "urlOptions";
+ private static final String CUSTOM_DB_USER = "customDbUser";
+ private static final String CUSTOM_DB_PASS = "customDbPass";
+ private static final int CUSTOM_DELAY = 5;
+ private static final int CUSTOM_EXECUTORS = 10;
+ private static final int CUSTOM_PACKED = 41;
+ private static final int CUSTOM_LOOSE = 401;
+ private static final int ONE_DAY_AS_MS = 86_400_000;
+
+ // Friday June 13, 2014 10:00 UTC
+ private static final ZonedDateTime NOW =
+ LocalDateTime.of(2014, Month.JUNE, 13, 10, 0, 0).atOffset(ZoneOffset.UTC).toZonedDateTime();
+
+ private Config config;
+ private ExecutorConfig executorConfig;
+
+ @Before
+ public void setUp() {
+ config = new Config();
+ }
+
+ @Test
+ public void shouldSetDefaultValues() {
+ executorConfig = new ExecutorConfig(config);
+
+ assertThat(executorConfig.getDatabaseUrl()).isEqualTo(DEFAULT_DB_URL);
+ assertThat(executorConfig.getDatabaseName()).isEqualTo(DEFAULT_DB_NAME);
+ assertThat(executorConfig.getDatabaseUrlOptions()).isEqualTo(EMPTY);
+ assertThat(executorConfig.getUsername()).isEqualTo(DEFAULT_DB_USERNAME);
+ assertThat(executorConfig.getPassword()).isEqualTo(DEFAULT_DB_PASSWORD);
+ assertThat(executorConfig.getDelay()).isEqualTo(DEFAULT_DELAY);
+ assertThat(executorConfig.getExecutors()).isEqualTo(DEFAULT_EXECUTORS);
+ assertThat(executorConfig.isPickOwnHostOnly()).isEqualTo(true);
+ assertThat(executorConfig.getLooseThreshold()).isEqualTo(LOOSE_DEFAULT);
+ assertThat(executorConfig.getPackedThreshold()).isEqualTo(PACKED_DEFAULT);
+ assertThat(executorConfig.getRepositoriesPath()).isEqualTo(DEFAULT_REPOS_PATH);
+ assertThat(executorConfig.getInterval()).isEqualTo(DEFAULT_INTERVAL);
+ assertThat(executorConfig.getInitialDelay()).isEqualTo(DEFAULT_INITIAL_DELAY);
+ }
+
+ @Test
+ public void shouldReadValuesFromConfig() {
+ config.setString(DB_SECTION, null, DB_URL_KEY, CUSTOM_DB_URL);
+ config.setString(DB_SECTION, null, DB_NAME_KEY, CUSTOM_DB_NAME);
+ config.setString(DB_SECTION, null, DB_URL_OPTIONS_KEY, URL_OPTIONS);
+ config.setString(DB_SECTION, null, DB_USERNAME_KEY, CUSTOM_DB_USER);
+ config.setString(DB_SECTION, null, DB_PASS_KEY, CUSTOM_DB_PASS);
+ config.setInt(CORE_SECTION, null, DELAY_KEY, CUSTOM_DELAY);
+ config.setInt(CORE_SECTION, null, EXECUTOR_KEY, CUSTOM_EXECUTORS);
+ config.setBoolean(CORE_SECTION, null, PICK_OWN_HOST_KEY, false);
+ config.setInt(EVALUATION_SECTION, null, LOOSE_KEY, CUSTOM_LOOSE);
+ config.setInt(EVALUATION_SECTION, null, PACKED_KEY, CUSTOM_PACKED);
+ config.setString(EVALUATION_SECTION, null, REPOS_PATH_KEY, CUSTOM_REPOS_PATH);
+ config.setString(EVALUATION_SECTION, null, INTERVAL_KEY, "1 day");
+ config.setString(EVALUATION_SECTION, null, START_TIME_KEY, "Sun 00:00");
+ executorConfig = new ExecutorConfig(config);
+
+ assertThat(executorConfig.getDatabaseUrl()).isEqualTo(CUSTOM_DB_URL);
+ assertThat(executorConfig.getDatabaseName()).isEqualTo(CUSTOM_DB_NAME);
+ assertThat(executorConfig.getDatabaseUrlOptions()).isEqualTo(URL_OPTIONS);
+ assertThat(executorConfig.getUsername()).isEqualTo(CUSTOM_DB_USER);
+ assertThat(executorConfig.getPassword()).isEqualTo(CUSTOM_DB_PASS);
+ assertThat(executorConfig.getDelay()).isEqualTo(CUSTOM_DELAY);
+ assertThat(executorConfig.getExecutors()).isEqualTo(CUSTOM_EXECUTORS);
+ assertThat(executorConfig.isPickOwnHostOnly()).isEqualTo(false);
+ assertThat(executorConfig.getLooseThreshold()).isEqualTo(CUSTOM_LOOSE);
+ assertThat(executorConfig.getPackedThreshold()).isEqualTo(CUSTOM_PACKED);
+ assertThat(executorConfig.getRepositoriesPath()).isEqualTo(CUSTOM_REPOS_PATH);
+ assertThat(executorConfig.getInterval()).isEqualTo(ONE_DAY_AS_MS);
+ assertThat(executorConfig.getInitialDelay()).isAtLeast(1L);
+ }
+
+ @Test
+ public void shouldParseTimeOnly() {
+ config.setString(EVALUATION_SECTION, null, INTERVAL_KEY, "1 hour");
+ config.setString(EVALUATION_SECTION, null, START_TIME_KEY, "15:00");
+ executorConfig = new ExecutorConfig(config);
+
+ assertThat(executorConfig.getInitialDelay()).isAtLeast(1L);
+ }
+
+ @Test
+ public void shouldUseDefaultValuesIfConfigInvalid() {
+ config.setString(EVALUATION_SECTION, null, INTERVAL_KEY, "1 x");
+ config.setString(EVALUATION_SECTION, null, START_TIME_KEY, "123 ab:cd");
+ executorConfig = new ExecutorConfig(config);
+
+ assertThat(executorConfig.getInterval()).isEqualTo(DEFAULT_INTERVAL);
+ assertThat(executorConfig.getInitialDelay()).isEqualTo(DEFAULT_INITIAL_DELAY);
+ }
+
+ @Test
+ public void checkInitialDelayGivesExpectedTime() {
+ assertThat(initialDelayFor("11:00", "1h")).isEqualTo(ms(1, HOURS));
+ assertThat(initialDelayFor("05:30", "1h")).isEqualTo(ms(30, MINUTES));
+ assertThat(initialDelayFor("13:59", "1h")).isEqualTo(ms(59, MINUTES));
+
+ assertThat(initialDelayFor("11:00", "1d")).isEqualTo(ms(1, HOURS));
+ assertThat(initialDelayFor("05:30", "1d")).isEqualTo(ms(19, HOURS) + ms(30, MINUTES));
+ assertThat(initialDelayFor("Sat 10:00", "1d")).isEqualTo(ms(1, DAYS));
+ assertThat(initialDelayFor("Mon 09:00", "1d")).isEqualTo(ms(23, HOURS));
+
+ assertThat(initialDelayFor("11:00", "1w")).isEqualTo(ms(1, HOURS));
+ assertThat(initialDelayFor("05:30", "1w"))
+ .isEqualTo(ms(7, DAYS) - ms(4, HOURS) - ms(30, MINUTES));
+ assertThat(initialDelayFor("Fri 11:00", "1w")).isEqualTo(ms(1, HOURS));
+ assertThat(initialDelayFor("Mon 11:00", "1w")).isEqualTo(ms(3, DAYS) + ms(1, HOURS));
+ }
+
+ private long initialDelayFor(String startTime, String interval) {
+ config.setString(EVALUATION_SECTION, null, INTERVAL_KEY, interval);
+ config.setString(EVALUATION_SECTION, null, START_TIME_KEY, startTime);
+ executorConfig = new ExecutorConfig(config);
+ return executorConfig.initialDelay(startTime, NOW, executorConfig.getInterval());
+ }
+
+ private long ms(int cnt, TimeUnit unit) {
+ return MILLISECONDS.convert(cnt, unit);
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorModuleTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorModuleTest.java
new file mode 100644
index 0000000..f4d3cef
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ExecutorModuleTest.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.when;
+
+import com.google.inject.Binder;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ExecutorModuleTest {
+ @Mock private Binder binder;
+ @Mock private ExecutorConfig execCfg;
+ @Mock private Config config;
+
+ private ExecutorModule executorModule;
+
+ @Before
+ public void setUp() {
+ executorModule = new ExecutorModule(config);
+ }
+
+ @Test
+ public void testWasQueuedFrom() {
+ String host = "hostname";
+ when(execCfg.isPickOwnHostOnly()).thenReturn(true);
+ Optional<String> wasQueuedFrom = executorModule.wasQueuedFrom(execCfg, host);
+ if (!wasQueuedFrom.isPresent()) {
+ fail("wasQueued from is empty");
+ }
+ assertThat(wasQueuedFrom.get()).isEqualTo(host);
+
+ when(execCfg.isPickOwnHostOnly()).thenReturn(false);
+ wasQueuedFrom = executorModule.wasQueuedFrom(execCfg, host);
+ assertThat(wasQueuedFrom.isPresent()).isFalse();
+ }
+
+ @Test
+ public void testQueuedForLongerThan() {
+ when(execCfg.getDelay()).thenReturn(5);
+ assertThat(executorModule.queuedForLongerThan(execCfg)).isEqualTo(5);
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GCTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GCTest.java
new file mode 100644
index 0000000..6d08f55
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GCTest.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.internal.storage.file.GC;
+import org.eclipse.jgit.internal.storage.file.GC.RepoStatistics;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class GCTest {
+
+ @Rule public TemporaryFolder dir = new TemporaryFolder();
+
+ @Test
+ public void testGc() throws Exception {
+ Repository repository = createRepository("repoTest");
+ RepoStatistics stats = computeFor(repository);
+ assertThat(stats.numberOfLooseObjects).isEqualTo(0);
+
+ addFileTo(repository);
+ stats = computeFor(repository);
+ assertThat(stats.numberOfLooseObjects).isEqualTo(2);
+ ProgressMonitor cpm = new CancellableProgressMonitor();
+ GarbageCollector gc = new GarbageCollector();
+ gc.setRepositoryPath(repository.getDirectory().getAbsolutePath());
+ gc.setPm(cpm);
+
+ assertThat(gc.call()).isTrue();
+ stats = computeFor(repository);
+ assertThat(stats.numberOfLooseObjects).isEqualTo(0);
+ assertThat(stats.numberOfPackedRefs).isEqualTo(1);
+ }
+
+ private Repository createRepository(String repoName) throws Exception {
+ File repo = dir.newFolder(repoName);
+ try (Git git = Git.init().setDirectory(repo).call()) {
+ return git.getRepository();
+ }
+ }
+
+ private void addFileTo(Repository repository) throws Exception {
+ try (Git git = new Git(repository)) {
+ new File(repository.getDirectory().getParent(), "test");
+ git.add().addFilepattern("test").call();
+ git.commit().setMessage("Add test file").call();
+ }
+ }
+
+ private RepoStatistics computeFor(Repository repository) throws IOException {
+ return new GC((FileRepository) repository).getStatistics();
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcExecutorTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcExecutorTest.java
new file mode 100644
index 0000000..4c74510
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcExecutorTest.java
@@ -0,0 +1,183 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static com.ericsson.gerrit.plugins.gcconductor.executor.GcExecutor.CONFIG_FILE_PROPERTY;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.ericsson.gerrit.plugins.gcconductor.RepositoryInfo;
+import com.google.common.collect.ImmutableList;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class GcExecutorTest {
+
+ private static final String HOSTNAME = "hostname";
+ private static final String EXECUTOR = "hostname-0";
+ private static final String REPOSITORY = "repository";
+
+ @Mock private ExecutorConfig config;
+ @Mock GcWorker.Factory gcWorkerFactory;
+ @Mock private GcWorker gcWorker;
+ @Mock private GcQueue gcQueue;
+ @Mock private ScheduledEvaluator scheduledEvaluator;
+
+ @Rule public TemporaryFolder testTempFolder = new TemporaryFolder();
+
+ @Test
+ public void testGcExecutor() {
+ when(config.getExecutors()).thenReturn(1);
+ when(gcWorkerFactory.create(EXECUTOR)).thenReturn(gcWorker);
+ GcExecutor gcExecutor =
+ new GcExecutor(gcQueue, config, gcWorkerFactory, scheduledEvaluator, HOSTNAME);
+ verify(gcWorker).start();
+ gcExecutor.onShutdown();
+ verify(gcWorker).shutdown();
+ }
+
+ @Test
+ public void testLeftOverReposAreUnpickedWhenStarting() throws Exception {
+ when(config.getExecutors()).thenReturn(1);
+ when(gcWorkerFactory.create(EXECUTOR)).thenReturn(gcWorker);
+ when(gcQueue.list())
+ .thenReturn(ImmutableList.of(new RepositoryInfo(REPOSITORY, null, EXECUTOR, HOSTNAME)));
+ GcExecutor gcExecutor =
+ new GcExecutor(gcQueue, config, gcWorkerFactory, scheduledEvaluator, HOSTNAME);
+ verify(gcQueue).unpick(REPOSITORY);
+ verify(gcWorker).start();
+ gcExecutor.onShutdown();
+ verify(gcWorker).shutdown();
+ }
+
+ @Test
+ public void testUnassignedReposAreNotUnpickedWhenStarting() throws Exception {
+ when(config.getExecutors()).thenReturn(1);
+ when(gcWorkerFactory.create(EXECUTOR)).thenReturn(gcWorker);
+ when(gcQueue.list())
+ .thenReturn(ImmutableList.of(new RepositoryInfo(REPOSITORY, null, null, HOSTNAME)));
+ GcExecutor gcExecutor =
+ new GcExecutor(gcQueue, config, gcWorkerFactory, scheduledEvaluator, HOSTNAME);
+ verify(gcQueue, never()).unpick(REPOSITORY);
+ verify(gcWorker).start();
+ gcExecutor.onShutdown();
+ verify(gcWorker).shutdown();
+ }
+
+ @Test
+ public void testExecutorDoesNotUnpickNotOwnedRepo() throws Exception {
+ when(config.getExecutors()).thenReturn(1);
+ when(gcWorkerFactory.create(EXECUTOR)).thenReturn(gcWorker);
+ when(gcQueue.list())
+ .thenReturn(
+ ImmutableList.of(
+ new RepositoryInfo(REPOSITORY, null, "another executor", "another hostname")));
+ GcExecutor gcExecutor =
+ new GcExecutor(gcQueue, config, gcWorkerFactory, scheduledEvaluator, HOSTNAME);
+ verify(gcQueue, never()).unpick(REPOSITORY);
+ verify(gcWorker).start();
+ gcExecutor.onShutdown();
+ verify(gcWorker).shutdown();
+ }
+
+ @Test
+ public void testQueueThrowsExceptionUnpickingAtStart() throws Exception {
+ when(config.getExecutors()).thenReturn(1);
+ when(gcWorkerFactory.create(EXECUTOR)).thenReturn(gcWorker);
+ doThrow(new GcQueueException("", new Throwable())).when(gcQueue).list();
+ GcExecutor gcExecutor =
+ new GcExecutor(gcQueue, config, gcWorkerFactory, scheduledEvaluator, HOSTNAME);
+ verify(gcQueue, never()).unpick(REPOSITORY);
+ verify(gcWorker).start();
+ gcExecutor.onShutdown();
+ verify(gcWorker).shutdown();
+ }
+
+ @Test
+ public void testScheduledEvaluationIsConfigured() throws Exception {
+ when(config.getExecutors()).thenReturn(1);
+ when(config.getInitialDelay()).thenReturn(1L);
+ when(config.getInterval()).thenReturn(1L);
+ when(gcWorkerFactory.create(EXECUTOR)).thenReturn(gcWorker);
+ when(gcQueue.list())
+ .thenReturn(ImmutableList.of(new RepositoryInfo(REPOSITORY, null, null, HOSTNAME)));
+ GcExecutor gcExecutor =
+ new GcExecutor(gcQueue, config, gcWorkerFactory, scheduledEvaluator, HOSTNAME);
+ verify(gcWorker).start();
+ verify(scheduledEvaluator).scheduleWith(1L, 1L);
+ gcExecutor.onShutdown();
+ verify(gcWorker).shutdown();
+ }
+
+ @Test
+ public void testScheduledEvaluationBothParametersRequired() throws Exception {
+ when(config.getExecutors()).thenReturn(1);
+ when(config.getInitialDelay()).thenReturn(1L);
+ when(gcWorkerFactory.create(EXECUTOR)).thenReturn(gcWorker);
+ when(gcQueue.list())
+ .thenReturn(ImmutableList.of(new RepositoryInfo(REPOSITORY, null, null, HOSTNAME)));
+ GcExecutor gcExecutor =
+ new GcExecutor(gcQueue, config, gcWorkerFactory, scheduledEvaluator, HOSTNAME);
+ verify(gcWorker).start();
+ verifyZeroInteractions(scheduledEvaluator);
+ gcExecutor.onShutdown();
+ verify(gcWorker).shutdown();
+ }
+
+ @Test
+ public void testloadConfigWhenNotSpecified() {
+ Config config = GcExecutor.loadConfig();
+ assertThat(config.toText()).isEmpty();
+ }
+
+ @Test
+ public void testloadConfigFromWhenSpecifiedByProperty() throws Exception {
+ File configFile = testTempFolder.newFile();
+ FileBasedConfig specifiedConfig = new FileBasedConfig(configFile, FS.DETECTED);
+ specifiedConfig.setString("otherSection", null, "otherKey", "otherValue");
+ specifiedConfig.save();
+ System.setProperty(CONFIG_FILE_PROPERTY, configFile.getAbsolutePath());
+ Config config = GcExecutor.loadConfig();
+ assertThat(config.toText()).isEqualTo(specifiedConfig.toText());
+ System.clearProperty(CONFIG_FILE_PROPERTY);
+ }
+
+ @Test
+ public void shouldReturnEmptyConfigIfAnErrorOccur() throws Exception {
+ File file = testTempFolder.newFile();
+ Files.write(file.toPath(), "[section]\n invalid!@$@#%#$".getBytes(), StandardOpenOption.CREATE);
+ System.setProperty(CONFIG_FILE_PROPERTY, file.getAbsolutePath());
+ Config config = GcExecutor.loadConfig();
+ assertThat(config.toText()).isEmpty();
+ System.clearProperty(CONFIG_FILE_PROPERTY);
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcWorkerTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcWorkerTest.java
new file mode 100644
index 0000000..c9b8e5d
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/GcWorkerTest.java
@@ -0,0 +1,144 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueue;
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.ericsson.gerrit.plugins.gcconductor.RepositoryInfo;
+import java.io.IOException;
+import java.util.Optional;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class GcWorkerTest {
+ private static final String EXEC_NAME = Thread.currentThread().getName();
+ private static final String REPO_PATH = "repo";
+ private static final String HOSTNAME = "hostname";
+ private static final Optional<String> QUEUED_FROM = Optional.empty();
+
+ @Mock private GcQueue queue;
+ @Mock private GarbageCollector garbageCollector;
+ @Mock private CancellableProgressMonitor cpm;
+
+ private RepositoryInfo repoInfo;
+
+ private GcWorker gcTask;
+
+ @Before
+ public void setUp() {
+ Thread.interrupted(); // reset the flag
+ repoInfo = new RepositoryInfo(REPO_PATH, null, EXEC_NAME, HOSTNAME);
+ gcTask = new GcWorker(queue, garbageCollector, cpm, QUEUED_FROM, 0, EXEC_NAME);
+ }
+
+ @Test
+ public void shouldPickAndRemoveRepository() throws Exception {
+ when(queue.pick(EXEC_NAME, 0, QUEUED_FROM)).thenReturn(repoInfo);
+ when(cpm.isCancelled()).thenReturn(false).thenReturn(false).thenReturn(true);
+ gcTask.run();
+ verify(garbageCollector).call();
+ verify(queue).remove(REPO_PATH);
+ cpm.cancel();
+ }
+
+ @Test
+ public void noRepositoryPicked() throws Exception {
+ when(cpm.isCancelled()).thenReturn(false).thenReturn(true);
+ gcTask.run();
+ verifyZeroInteractions(garbageCollector);
+ verify(queue, never()).remove(any(String.class));
+ verify(queue, never()).unpick(any(String.class));
+ }
+
+ @Test
+ public void interruptedWhenWaitingToPickRepository() throws Exception {
+ when(cpm.isCancelled()).thenReturn(false).thenReturn(true);
+ Thread t = new Thread(() -> gcTask.run());
+ t.start();
+ t.interrupt();
+ verifyZeroInteractions(garbageCollector);
+ verify(queue, never()).remove(any(String.class));
+ verify(queue, never()).unpick(any(String.class));
+ }
+
+ @Test
+ public void gcFailsOnRepository() throws Exception {
+ when(cpm.isCancelled()).thenReturn(false).thenReturn(false).thenReturn(false).thenReturn(true);
+ when(queue.pick(EXEC_NAME, 0, QUEUED_FROM)).thenReturn(repoInfo);
+ doThrow(new IOException()).when(garbageCollector).call();
+ gcTask.run();
+ verify(queue).remove(REPO_PATH);
+ verify(queue, never()).unpick(any(String.class));
+ }
+
+ @Test
+ public void queueThrowsExceptionWhenPickingRepository() throws Exception {
+ when(cpm.isCancelled()).thenReturn(false).thenReturn(true);
+ doThrow(new GcQueueException("", new Throwable())).when(queue).pick(EXEC_NAME, 0, QUEUED_FROM);
+ gcTask.run();
+ verifyZeroInteractions(garbageCollector);
+ verify(queue, never()).remove(any(String.class));
+ verify(queue, never()).unpick(any(String.class));
+ }
+
+ @Test
+ public void gcRepositoryIsInterrupted() throws Exception {
+ when(cpm.isCancelled()).thenReturn(false).thenReturn(true).thenReturn(true).thenReturn(true);
+ when(queue.pick(EXEC_NAME, 0, QUEUED_FROM)).thenReturn(repoInfo);
+ doThrow(new IOException()).when(garbageCollector).call();
+ gcTask.run();
+ verify(queue).unpick(REPO_PATH);
+ verify(queue, never()).remove(REPO_PATH);
+ }
+
+ @Test
+ public void queueThrowsExceptionWhenRemovingRepository() throws Exception {
+ when(cpm.isCancelled()).thenReturn(false).thenReturn(false).thenReturn(true);
+ when(queue.pick(EXEC_NAME, 0, QUEUED_FROM)).thenReturn(repoInfo);
+ doThrow(new GcQueueException("", new Throwable())).when(queue).remove(REPO_PATH);
+ gcTask.run();
+ verify(garbageCollector).call();
+ verify(queue).remove(REPO_PATH);
+ verify(queue, never()).unpick(REPO_PATH);
+ }
+
+ @Test
+ public void queueThrowsExceptionWhenUnpickingRepository() throws Exception {
+ when(cpm.isCancelled()).thenReturn(false).thenReturn(true).thenReturn(true).thenReturn(true);
+ when(queue.pick(EXEC_NAME, 0, QUEUED_FROM)).thenReturn(repoInfo);
+ doThrow(new IOException()).when(garbageCollector).call();
+ doThrow(new GcQueueException("", new Throwable())).when(queue).unpick(REPO_PATH);
+ gcTask.run();
+ verify(queue).unpick(REPO_PATH);
+ verify(queue, never()).remove(REPO_PATH);
+ }
+
+ @Test
+ public void callingShutdownSetsCancellableToTrue() {
+ gcTask.shutdown();
+ verify(cpm).cancel();
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluationTaskTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluationTaskTest.java
new file mode 100644
index 0000000..c560fbf
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluationTaskTest.java
@@ -0,0 +1,90 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.gcconductor.EvaluationTask;
+import com.ericsson.gerrit.plugins.gcconductor.EvaluationTask.Factory;
+import com.google.inject.ProvisionException;
+import java.io.File;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ScheduledEvaluationTaskTest {
+
+ @Rule public TemporaryFolder dir = new TemporaryFolder();
+
+ @Mock private ExecutorConfig config;
+ @Mock private Factory evaluationTaskFactory;
+ @Mock private EvaluationTask evaluationTask;
+
+ private ScheduledEvaluationTask periodicEvaluator;
+
+ @Before
+ public void setUp() {
+ when(config.getRepositoriesPath()).thenReturn(dir.getRoot().getAbsolutePath());
+ periodicEvaluator = new ScheduledEvaluationTask(evaluationTaskFactory, config);
+ }
+
+ @Test(expected = ProvisionException.class)
+ public void shouldThrowAnExceptionOnInvalidrepositoryPath() {
+ when(config.getRepositoriesPath()).thenReturn("unexistingPath");
+ periodicEvaluator = new ScheduledEvaluationTask(evaluationTaskFactory, config);
+ }
+
+ @Test
+ public void shouldAddRepository() throws Exception {
+ Repository repository = createRepository("repoTest");
+ when(evaluationTaskFactory.create(repository.getDirectory().getAbsolutePath()))
+ .thenReturn(evaluationTask);
+ periodicEvaluator.run();
+ verify(evaluationTask).run();
+ }
+
+ @Test
+ public void shouldNotAddFolderIfNotRepository() throws Exception {
+ File notARepo = dir.newFolder("notRepository");
+ periodicEvaluator.run();
+ verify(evaluationTaskFactory, never()).create(notARepo.getAbsolutePath());
+ }
+
+ @Test
+ public void shouldHonorInterruption() throws Exception {
+ createRepository("repoTest");
+ Thread t = new Thread(() -> periodicEvaluator.run());
+ t.start();
+ t.interrupt();
+ verifyZeroInteractions(evaluationTaskFactory);
+ }
+
+ private Repository createRepository(String repoName) throws Exception {
+ File repo = dir.newFolder(repoName);
+ try (Git git = Git.init().setDirectory(repo).call()) {
+ return git.getRepository();
+ }
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluatorTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluatorTest.java
new file mode 100644
index 0000000..f04cdf3
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/executor/ScheduledEvaluatorTest.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.executor;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ScheduledEvaluatorTest {
+
+ @Mock private ScheduledEvaluationTask evaluationTask;
+
+ private ScheduledEvaluator scheduledEvaluator;
+
+ @Test
+ public void testScheduleWith() throws Exception {
+ scheduledEvaluator = new ScheduledEvaluator(evaluationTask);
+ scheduledEvaluator.scheduleWith(1, 2000);
+ TimeUnit.MILLISECONDS.sleep(100);
+ verify(evaluationTask).run();
+ scheduledEvaluator.onShutdown();
+ }
+
+ @Test
+ public void testOnShutdown() {
+ scheduledEvaluator = new ScheduledEvaluator(evaluationTask);
+ scheduledEvaluator.onShutdown();
+ verifyZeroInteractions(evaluationTask);
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresModuleTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresModuleTest.java
new file mode 100644
index 0000000..fa2593f
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresModuleTest.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.postgresqueue;
+
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.TestUtil.configMockFor;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.TestUtil.deleteDatabase;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+import com.ericsson.gerrit.plugins.gcconductor.evaluator.EvaluatorConfig;
+import com.ericsson.gerrit.plugins.gcconductor.postgresqueue.PostgresModule.DatabaseAccessCleanUp;
+import java.sql.SQLException;
+import org.apache.commons.dbcp.BasicDataSource;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PostgresModuleTest {
+
+ private static final String TEST_DATABASE_NAME = "gc_test_module";
+ private EvaluatorConfig configMock;
+ private PostgresModule module;
+ private BasicDataSource dataSource;
+
+ @Before
+ public void setUp() throws SQLException {
+ configMock = configMockFor(TEST_DATABASE_NAME);
+ module = new PostgresModule(null);
+ dataSource = module.provideGcDatabaseAccess(configMock);
+ }
+
+ @After
+ public void tearDown() throws SQLException {
+ if (dataSource != null) {
+ dataSource.close();
+ }
+ deleteDatabase(TEST_DATABASE_NAME);
+ }
+
+ @Test
+ public void shouldCreateGcDatabase() {
+ assertThat(dataSource).isNotNull();
+ assertThat(dataSource.isClosed()).isFalse();
+ }
+
+ @Test
+ public void shouldNotComplainsIfGcDatabaseAlreadyExists() throws SQLException {
+ dataSource.close();
+ dataSource = module.provideGcDatabaseAccess(configMock);
+ assertThat(dataSource).isNotNull();
+ assertThat(dataSource.isClosed()).isFalse();
+ }
+
+ @Test(expected = SQLException.class)
+ public void shouldFailIfDatabaseNameIsInvalid() throws SQLException {
+ module.provideGcDatabaseAccess(configMockFor("invalid!@#$@$%^-name"));
+ }
+
+ @Test
+ public void shouldCloseDatabaseAccessOnStop() {
+ DatabaseAccessCleanUp dbCleanUp = new DatabaseAccessCleanUp(dataSource);
+ assertThat(dataSource).isNotNull();
+ assertThat(dataSource.isClosed()).isFalse();
+ dbCleanUp.onShutdown();
+ assertThat(dataSource.isClosed()).isTrue();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void shouldThrowExceptionIfFailsToCloseDatabaseAccess() throws SQLException {
+ BasicDataSource dataSouceMock = mock(BasicDataSource.class);
+ doThrow(new SQLException("somme error")).when(dataSouceMock).close();
+ DatabaseAccessCleanUp dbCleanUp = new DatabaseAccessCleanUp(dataSouceMock);
+ dbCleanUp.onShutdown();
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresQueueTest.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresQueueTest.java
new file mode 100644
index 0000000..2b60076
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/PostgresQueueTest.java
@@ -0,0 +1,496 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.postgresqueue;
+
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.TestUtil.configMockFor;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.TestUtil.deleteDatabase;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.gcconductor.GcQueueException;
+import com.ericsson.gerrit.plugins.gcconductor.RepositoryInfo;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.dbcp.BasicDataSource;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PostgresQueueTest {
+
+ private static final String TEST_DATABASE_NAME = "gc_test_queue";
+ private BasicDataSource dataSource;
+ private PostgresQueue queue;
+
+ @Before
+ public void setUp() throws SQLException {
+ dataSource =
+ new PostgresModule(null).provideGcDatabaseAccess(configMockFor(TEST_DATABASE_NAME));
+ queue = new PostgresQueue(dataSource);
+ }
+
+ @After
+ public void tearDown() throws SQLException {
+ if (dataSource != null) {
+ dataSource.close();
+ }
+ deleteDatabase(TEST_DATABASE_NAME);
+ }
+
+ @Test
+ public void shouldCreateSchemaOnInit() throws GcQueueException {
+ assertThat(queue.list()).isEmpty();
+ }
+
+ @Test(expected = SQLException.class)
+ public void shouldThrowExceptionIfFailsToCreateSchemaOnInit() throws Exception {
+ BasicDataSource dataSouceMock = mock(BasicDataSource.class);
+ when(dataSouceMock.getConnection()).thenThrow(new SQLException("some message"));
+ queue = new PostgresQueue(dataSouceMock);
+ }
+
+ @Test
+ public void testAddContainsAndRemove() throws GcQueueException {
+ String repoPath = "/some/path/to/some/repository";
+ String hostname = "someHostname";
+
+ assertThat(queue.list()).isEmpty();
+ assertThat(queue.contains(repoPath)).isFalse();
+
+ queue.add(repoPath, hostname);
+ assertThat(queue.list().size()).isEqualTo(1);
+ assertThat(queue.contains(repoPath)).isTrue();
+
+ queue.add(repoPath, hostname);
+ assertThat(queue.list().size()).isEqualTo(1);
+ assertThat(queue.contains(repoPath)).isTrue();
+
+ String repoPath2 = "/some/path/to/some/repository2";
+ String hostname2 = "someHostname2";
+
+ queue.add(repoPath2, hostname2);
+ assertThat(queue.list().size()).isEqualTo(2);
+ assertThat(queue.contains(repoPath)).isTrue();
+ assertThat(queue.contains(repoPath2)).isTrue();
+
+ queue.remove(repoPath2);
+ assertThat(queue.list().size()).isEqualTo(1);
+ assertThat(queue.contains(repoPath)).isTrue();
+ assertThat(queue.contains(repoPath2)).isFalse();
+
+ queue.remove(repoPath);
+ assertThat(queue.list().size()).isEqualTo(0);
+ assertThat(queue.contains(repoPath)).isFalse();
+
+ queue.remove(repoPath);
+ assertThat(queue.list().size()).isEqualTo(0);
+ assertThat(queue.contains(repoPath)).isFalse();
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testAddThatFailsWhenGettingConnection() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenGettingConnection());
+ queue.add("repo", "hostname");
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testAddThatFailsWhenCreatingStatement() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenCreatingStatement());
+ queue.add("repo", "hostname");
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testAddThatFailsWhenExecutingQuery() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenExecutingQuery());
+ queue.add("repo", "hostname");
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testContainsThatFailsWhenGettingConnection() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenGettingConnection());
+ queue.contains("repo");
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testContainsThatFailsWhenCreatingStatement() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenCreatingStatement());
+ queue.contains("repo");
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testContainsThatFailsWhenExecutingQuery() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenExecutingQuery());
+ queue.contains("repo");
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testContainsThatFailsWhenIteratingResults() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenIteratingResults());
+ queue.contains("repo");
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testRemoveThatFailsWhenGettingConnection() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenGettingConnection());
+ queue.remove("repo");
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testRemoveThatFailsWhenCreatingStatement() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenCreatingStatement());
+ queue.remove("repo");
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testRemoveThatFailsWhenExecutingQuery() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenExecutingQuery());
+ queue.remove("repo");
+ }
+
+ @Test
+ public void testList() throws GcQueueException {
+ String repoPath = "/some/path/to/some/repository.git";
+ String repoPath2 = "/some/path/to/some/repository2.git";
+ String hostname = "hostname";
+ String executor = "hostname-1";
+
+ assertThat(queue.list()).isEmpty();
+ Timestamp before = new Timestamp(System.currentTimeMillis());
+ queue.add(repoPath, hostname);
+ queue.add(repoPath2, hostname);
+ queue.pick(executor, 0, Optional.empty());
+
+ assertThat(queue.list().size()).isEqualTo(2);
+
+ assertThat(queue.list().get(0).getPath()).isEqualTo(repoPath);
+ assertThat(queue.list().get(0).getExecutor()).isEqualTo(executor);
+ assertThat(queue.list().get(0).getQueuedAt()).isAtLeast(before);
+ assertThat(queue.list().get(0).getQueuedAt())
+ .isAtMost(new Timestamp(System.currentTimeMillis()));
+ assertThat(queue.list().get(0).getQueuedFrom()).isEqualTo(hostname);
+
+ assertThat(queue.list().get(1).getPath()).isEqualTo(repoPath2);
+ assertThat(queue.list().get(1).getExecutor()).isNull();
+ assertThat(queue.list().get(1).getQueuedAt()).isAtLeast(queue.list().get(0).getQueuedAt());
+ assertThat(queue.list().get(1).getQueuedAt())
+ .isAtMost(new Timestamp(System.currentTimeMillis()));
+ assertThat(queue.list().get(1).getQueuedFrom()).isEqualTo(hostname);
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testListThatFailsWhenGettingConnection() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenGettingConnection());
+ queue.list();
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testListThatFailsWhenCreatingStatement() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenCreatingStatement());
+ queue.list();
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testListThatFailsWhenExecutingQuery() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenExecutingQuery());
+ queue.list();
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testListThatFailsWhenIteratingResults() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenIteratingResults());
+ queue.list();
+ }
+
+ @Test
+ public void testPick() throws GcQueueException {
+ String repoPath = "/some/path/to/some/repository";
+ String hostname = "someHostname";
+ String executor = "someExecutor";
+ String executor2 = "someExecutor2";
+
+ // queue is empty nothing to pick
+ assertThat(queue.list()).isEmpty();
+ assertThat(queue.pick(executor, 0, Optional.empty())).isNull();
+
+ // queue contains 1 repository, should pick that one
+ queue.add(repoPath, hostname);
+ RepositoryInfo picked = queue.pick(executor, 0, Optional.empty());
+ assertThat(picked).isNotNull();
+ assertThat(picked.getPath()).isEqualTo(repoPath);
+ assertThat(picked.getExecutor()).isEqualTo(executor);
+
+ // queue contains 1 already picked repository, should pick same one
+ picked = queue.pick(executor, 0, Optional.empty());
+ assertThat(picked).isNotNull();
+ assertThat(picked.getPath()).isEqualTo(repoPath);
+ assertThat(picked.getExecutor()).isEqualTo(executor);
+
+ // queue contains 1 already picked repository, nothing to pick for other
+ // executors
+ assertThat(queue.pick(executor2, 0, Optional.empty())).isNull();
+ }
+
+ @Test
+ public void testPickRepositoriesInOrder() throws GcQueueException {
+ String repositoryFormat = "my/path%s.git";
+ for (int i = 0; i < 100; i++) {
+ queue.add(String.format(repositoryFormat, i), "someHostname");
+ }
+ for (int i = 0; i < 100; i++) {
+ String pickedRepo = queue.pick("someExecutor", 0, Optional.empty()).getPath();
+ assertThat(pickedRepo).isEqualTo(String.format(repositoryFormat, i));
+ queue.remove(pickedRepo);
+ }
+ }
+
+ @Test
+ public void testPickInQueueForLongerThan() throws GcQueueException, InterruptedException {
+ String repoPath = "/some/path/to/some/repository";
+ String hostname = "someHostname";
+ String executor = "someExecutor";
+
+ // pick repository older than 10 seconds, nothing to pick
+ queue.add(repoPath, hostname);
+ assertThat(queue.pick(executor, 10, Optional.empty())).isNull();
+ assertThat(queue.list().get(0).getExecutor()).isNull();
+
+ // make 2 seconds elapse and pick repository older than 1 second, should pick one
+ TimeUnit.SECONDS.sleep((2));
+ RepositoryInfo picked = queue.pick(executor, 1, Optional.empty());
+ assertThat(picked.getPath()).isEqualTo(repoPath);
+ assertThat(picked.getExecutor()).isEqualTo(executor);
+ }
+
+ @Test
+ public void testPickQueuedFrom() throws GcQueueException {
+ String repoPath = "/some/path/to/some/repository";
+ String hostname = "hostname";
+ String otherHostname = "otherHostname";
+ String executor = "hostname-1";
+
+ // pick repository queued from otherHostname, nothing to pick
+ queue.add(repoPath, hostname);
+ assertThat(queue.pick(executor, 0, Optional.of(otherHostname))).isNull();
+ assertThat(queue.list().get(0).getExecutor()).isNull();
+
+ // pick repository queued from hostname, should pick one
+ RepositoryInfo picked = queue.pick(executor, 0, Optional.of(hostname));
+ assertThat(picked.getPath()).isEqualTo(repoPath);
+ assertThat(picked.getExecutor()).isEqualTo(executor);
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testPickThatFailsWhenGettingConnection() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenGettingConnection());
+ queue.pick("executor", 0, Optional.empty());
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testPickThatFailsWhenCreatingStatement() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenCreatingStatement());
+ queue.pick("executor", 0, Optional.empty());
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testPickThatFailsWhenExecutingQuery() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenExecutingQuery());
+ queue.pick("executor", 0, Optional.empty());
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testPickThatFailsWhenIteratingResults() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenIteratingResults());
+ queue.pick("executor", 0, Optional.empty());
+ }
+
+ @Test
+ public void testUnpick() throws GcQueueException {
+ String repoPath = "/some/path/to/some/repository";
+ String hostname = "someHostname";
+ String executor = "someExecutor";
+
+ // queue contains 1 repository, should pick that one
+ queue.add(repoPath, hostname);
+ RepositoryInfo picked = queue.pick(executor, 0, Optional.empty());
+ assertThat(picked.getPath()).isEqualTo(repoPath);
+ assertThat(picked.getExecutor()).isEqualTo(executor);
+
+ queue.unpick(repoPath);
+ // unpick repo so should pick that one again
+ queue.unpick(repoPath);
+ picked = queue.pick(executor, 0, Optional.empty());
+ assertThat(picked.getPath()).isEqualTo(repoPath);
+ assertThat(picked.getExecutor()).isEqualTo(executor);
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testUnpickThatFailsWhenGettingConnection() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenGettingConnection());
+ queue.unpick("/some/path/to/some/repository.git");
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testUnpickFailsWhenCreatingStatement() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenCreatingStatement());
+ queue.unpick("/some/path/to/some/repository.git");
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testUnpickThatFailsWhenExecutingQuery() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenExecutingQuery());
+ queue.unpick("/some/path/to/some/repository.git");
+ }
+
+ @Test
+ public void testResetQueuedFrom() throws GcQueueException {
+ String repoPath = "/some/path/to/some/repository";
+ String repoPath2 = "/some/path/to/some/repository2";
+ String hostname = "hostname";
+ String otherHostname = "otherHostname";
+
+ queue.add(repoPath, hostname);
+ queue.add(repoPath2, hostname);
+ assertThat(queue.list().get(0).getQueuedFrom()).isEqualTo(hostname);
+ assertThat(queue.list().get(1).getQueuedFrom()).isEqualTo(hostname);
+
+ queue.resetQueuedFrom(otherHostname);
+ assertThat(queue.list().get(0).getQueuedFrom()).isEqualTo(otherHostname);
+ assertThat(queue.list().get(1).getQueuedFrom()).isEqualTo(otherHostname);
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testResetQueuedFromThatFailsWhenGettingConnection() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenGettingConnection());
+ queue.resetQueuedFrom("someHostname");
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testResetQueuedFromFailsWhenCreatingStatement() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenCreatingStatement());
+ queue.resetQueuedFrom("someHostname");
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testResetQueuedFromThatFailsWhenExecutingQuery() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenExecutingQuery());
+ queue.resetQueuedFrom("someHostname");
+ }
+
+ @Test
+ public void testBumpToFirst() throws GcQueueException {
+ String repoPath = "/some/path/to/some/repository";
+ String repoPath2 = "/some/path/to/some/repository2";
+ String repoPath3 = "/some/path/to/some/repository3";
+ String hostname = "hostname";
+
+ // Queue contains 1 repository, bumping should have no effect
+ queue.add(repoPath, hostname);
+ assertThat(queue.list().get(0).getPath()).isEqualTo(repoPath);
+ queue.bumpToFirst(repoPath);
+ assertThat(queue.list().get(0).getPath()).isEqualTo(repoPath);
+
+ // Queue has 3 repositories, should be able to change their order
+ queue.add(repoPath2, hostname);
+ queue.add(repoPath3, hostname);
+ assertThat(queue.list().get(1).getPath()).isEqualTo(repoPath2);
+ assertThat(queue.list().get(2).getPath()).isEqualTo(repoPath3);
+
+ // repoPath3 should be first, all other repositories should be shifted down
+ queue.bumpToFirst(repoPath3);
+ assertThat(queue.list().get(0).getPath()).isEqualTo(repoPath3);
+ assertThat(queue.list().get(1).getPath()).isEqualTo(repoPath);
+ assertThat(queue.list().get(2).getPath()).isEqualTo(repoPath2);
+
+ // Bumping a repository that is already first priority should have no effect
+ queue.bumpToFirst(repoPath3);
+ assertThat(queue.list().get(0).getPath()).isEqualTo(repoPath3);
+ assertThat(queue.list().get(1).getPath()).isEqualTo(repoPath);
+ assertThat(queue.list().get(2).getPath()).isEqualTo(repoPath2);
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testBumpToFirstThatFailsWhenGettingConnection() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenGettingConnection());
+ queue.bumpToFirst("someHostname");
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testBumpToFirstFailsWhenCreatingStatement() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenCreatingStatement());
+ queue.bumpToFirst("someHostname");
+ }
+
+ @Test(expected = GcQueueException.class)
+ public void testBumpToFirstThatFailsWhenExecutingQuery() throws Exception {
+ queue = new PostgresQueue(createDataSourceThatFailsWhenExecutingQuery());
+ queue.bumpToFirst("someHostname");
+ }
+
+ private BasicDataSource createDataSourceThatFailsWhenGettingConnection() throws SQLException {
+ BasicDataSource dataSouceMock = mock(BasicDataSource.class);
+ Connection connectionMock = mock(Connection.class);
+ Statement statementMock = mock(Statement.class);
+
+ when(dataSouceMock.getConnection()).thenReturn(connectionMock).thenThrow(new SQLException());
+ when(connectionMock.createStatement()).thenReturn(statementMock);
+
+ return dataSouceMock;
+ }
+
+ private BasicDataSource createDataSourceThatFailsWhenCreatingStatement() throws SQLException {
+ BasicDataSource dataSouceMock = mock(BasicDataSource.class);
+ Connection connectionMock = mock(Connection.class);
+ Statement statementMock = mock(Statement.class);
+
+ when(dataSouceMock.getConnection()).thenReturn(connectionMock);
+ when(connectionMock.createStatement()).thenReturn(statementMock).thenThrow(new SQLException());
+
+ return dataSouceMock;
+ }
+
+ private BasicDataSource createDataSourceThatFailsWhenExecutingQuery() throws SQLException {
+ BasicDataSource dataSouceMock = mock(BasicDataSource.class);
+ Connection connectionMock = mock(Connection.class);
+ Statement statementMock = mock(Statement.class);
+
+ when(dataSouceMock.getConnection()).thenReturn(connectionMock);
+ when(connectionMock.createStatement()).thenReturn(statementMock);
+ when(statementMock.execute(anyString())).thenReturn(true).thenThrow(new SQLException());
+ when(statementMock.executeQuery(anyString())).thenThrow(new SQLException());
+
+ return dataSouceMock;
+ }
+
+ private BasicDataSource createDataSourceThatFailsWhenIteratingResults() throws SQLException {
+ BasicDataSource dataSouceMock = mock(BasicDataSource.class);
+ Connection connectionMock = mock(Connection.class);
+ Statement statementMock = mock(Statement.class);
+ ResultSet resultSetMock = mock(ResultSet.class);
+
+ when(dataSouceMock.getConnection()).thenReturn(connectionMock);
+ when(connectionMock.createStatement()).thenReturn(statementMock);
+ when(statementMock.executeQuery(anyString())).thenReturn(resultSetMock);
+ when(resultSetMock.next()).thenThrow(new SQLException());
+
+ return dataSouceMock;
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/TestUtil.java b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/TestUtil.java
new file mode 100644
index 0000000..023f9cd
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/gcconductor/postgresqueue/TestUtil.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2017 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.ericsson.gerrit.plugins.gcconductor.postgresqueue;
+
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.DRIVER;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.INITIAL_DATABASE;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.dropDatabase;
+import static com.ericsson.gerrit.plugins.gcconductor.postgresqueue.DatabaseConstants.executeStatement;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.ericsson.gerrit.plugins.gcconductor.evaluator.EvaluatorConfig;
+import java.sql.SQLException;
+import org.apache.commons.dbcp.BasicDataSource;
+import org.junit.Test;
+
+public class TestUtil {
+
+ private static final String DATABASE_SERVER_URL = "jdbc:postgresql://localhost:5432/";
+ private static final String DEFAULT_USER_AND_PASSWORD = "gc";
+
+ static EvaluatorConfig configMockFor(String databaseName) {
+ EvaluatorConfig configMock = mock(EvaluatorConfig.class);
+ when(configMock.getDatabaseUrl()).thenReturn(DATABASE_SERVER_URL);
+ when(configMock.getDatabaseUrlOptions()).thenReturn("");
+ when(configMock.getDatabaseName()).thenReturn(databaseName);
+ when(configMock.getUsername()).thenReturn(DEFAULT_USER_AND_PASSWORD);
+ when(configMock.getPassword()).thenReturn(DEFAULT_USER_AND_PASSWORD);
+ return configMock;
+ }
+
+ static void deleteDatabase(String databaseName) throws SQLException {
+ BasicDataSource ds = new BasicDataSource();
+ try {
+ ds.setDriverClassName(DRIVER);
+ ds.setUrl(DATABASE_SERVER_URL + INITIAL_DATABASE);
+ ds.setUsername(DEFAULT_USER_AND_PASSWORD);
+ ds.setPassword(DEFAULT_USER_AND_PASSWORD);
+ executeStatement(ds, dropDatabase(databaseName));
+ } finally {
+ ds.close();
+ }
+ }
+
+ @Test
+ public void fakeTest() {
+ // Hackish way of avoiding bazel test mark this class as failed
+ // because of the lack of executable methods
+ return;
+ }
+}
diff --git a/tools/bazel.rc b/tools/bazel.rc
new file mode 100644
index 0000000..4ed16cf
--- /dev/null
+++ b/tools/bazel.rc
@@ -0,0 +1,2 @@
+build --workspace_status_command=./tools/workspace-status.sh
+test --build_tests_only
diff --git a/tools/bzl/BUILD b/tools/bzl/BUILD
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/bzl/BUILD
diff --git a/tools/bzl/classpath.bzl b/tools/bzl/classpath.bzl
new file mode 100644
index 0000000..d5764f7
--- /dev/null
+++ b/tools/bzl/classpath.bzl
@@ -0,0 +1,4 @@
+load(
+ "@com_googlesource_gerrit_bazlets//tools:classpath.bzl",
+ "classpath_collector",
+)
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
new file mode 100644
index 0000000..3af7e58
--- /dev/null
+++ b/tools/bzl/junit.bzl
@@ -0,0 +1,4 @@
+load(
+ "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
+ "junit_tests",
+)
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
new file mode 100644
index 0000000..2eabedb
--- /dev/null
+++ b/tools/bzl/maven_jar.bzl
@@ -0,0 +1 @@
+load("@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", "maven_jar")
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
new file mode 100644
index 0000000..0b25d23
--- /dev/null
+++ b/tools/bzl/plugin.bzl
@@ -0,0 +1,6 @@
+load(
+ "@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
+ "PLUGIN_DEPS",
+ "PLUGIN_TEST_DEPS",
+ "gerrit_plugin",
+)
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
new file mode 100644
index 0000000..35a7598
--- /dev/null
+++ b/tools/eclipse/BUILD
@@ -0,0 +1,9 @@
+load("//tools/bzl:classpath.bzl", "classpath_collector")
+
+classpath_collector(
+ name = "main_classpath_collect",
+ testonly = 1,
+ deps = [
+ "//:gc-conductor__plugin_test_deps",
+ ],
+)
diff --git a/tools/eclipse/project.sh b/tools/eclipse/project.sh
new file mode 100755
index 0000000..2c64166
--- /dev/null
+++ b/tools/eclipse/project.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+# Copyright (C) 2017 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.
+
+`bazel query @com_googlesource_gerrit_bazlets//tools/eclipse:project --output location | sed s/BUILD:.*//`project.py -n gc-conductor -r .
diff --git a/tools/sonar/sonar.sh b/tools/sonar/sonar.sh
new file mode 100755
index 0000000..8df06d3
--- /dev/null
+++ b/tools/sonar/sonar.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+# Copyright (C) 2018 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.
+
+`bazel query @com_googlesource_gerrit_bazlets//tools/sonar:sonar --output location | sed s/BUILD:.*//`sonar.py
diff --git a/tools/workspace-status.sh b/tools/workspace-status.sh
new file mode 100755
index 0000000..00630d2
--- /dev/null
+++ b/tools/workspace-status.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# This script will be run by bazel when the build process starts to
+# generate key-value information that represents the status of the
+# workspace. The output should be like
+#
+# KEY1 VALUE1
+# KEY2 VALUE2
+#
+# If the script exits with non-zero code, it's considered as a failure
+# and the output will be discarded.
+
+function rev() {
+ cd $1 && git describe --always --match "v[0-9].*" --dirty
+}
+
+echo STABLE_BUILD_GC-CONDUCTOR_LABEL "$(rev .)"