Add Batch Cleaner to the Batch Plugin
Cleans expired batches daily, by default it removes all batches
that are older than 3 days.
Change-Id: Ic0f10e5cb9f68152360e068706ab8db5fdb9ceb4
diff --git a/src/main/java/com/googlesource/gerrit/plugins/batch/BatchCleaner.java b/src/main/java/com/googlesource/gerrit/plugins/batch/BatchCleaner.java
new file mode 100644
index 0000000..fe5c795
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/batch/BatchCleaner.java
@@ -0,0 +1,154 @@
+// 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.googlesource.gerrit.plugins.batch;
+
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.git.WorkQueue.CancelableRunnable;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.ArrayList;
+import java.util.concurrent.ScheduledFuture;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Clean up expired batches daily. */
+public class BatchCleaner implements CancelableRunnable {
+ private static final Logger log = LoggerFactory.getLogger(BatchCleaner.class);
+
+ public static class Lifecycle implements LifecycleListener {
+ public static final long DEFAULT_START_MINUTES = MINUTES.convert(1, MINUTES);
+ public static final long DEFAULT_MINUTES = MINUTES.convert(1, DAYS);
+
+ protected final long startMinutes;
+ protected final long intervalMinutes;
+
+ protected final PluginConfigFactory cfgFactory;
+ protected final WorkQueue workQueue;
+ protected final BatchCleaner cleaner;
+ protected final ProjectCache projectCache;
+ protected final String pluginName;
+
+ @Inject
+ protected Lifecycle(
+ PluginConfigFactory cfgFactory,
+ WorkQueue workQueue,
+ BatchCleaner cleaner,
+ ProjectCache projectCache,
+ @PluginName String pluginName)
+ throws NoSuchProjectException {
+ this.cfgFactory = cfgFactory;
+ this.workQueue = workQueue;
+ this.cleaner = cleaner;
+ this.projectCache = projectCache;
+ this.pluginName = pluginName;
+ startMinutes = startDelay();
+ intervalMinutes = interval();
+ }
+
+ @Override
+ public void start() {
+ cleaner.future =
+ workQueue
+ .getDefaultQueue()
+ .scheduleWithFixedDelay(cleaner, startMinutes, intervalMinutes, MINUTES);
+ }
+
+ @Override
+ public void stop() {
+ cleaner.cancel();
+ }
+
+ protected long startDelay() throws NoSuchProjectException {
+ Config config = cfgFactory.getProjectPluginConfig(projectCache.getAllProjects(), pluginName);
+ return ConfigUtil.getTimeUnit(
+ config, "cleaner", null, "startDelay", DEFAULT_START_MINUTES, MINUTES);
+ }
+
+ protected long interval() throws NoSuchProjectException {
+ Config config = cfgFactory.getProjectPluginConfig(projectCache.getAllProjects(), pluginName);
+ String freq = config.getString("cleaner", null, "interval");
+ if (freq != null && ("disabled".equalsIgnoreCase(freq) || "off".equalsIgnoreCase(freq))) {
+ return Long.MAX_VALUE;
+ }
+ return ConfigUtil.getTimeUnit(config, "cleaner", null, "interval", DEFAULT_MINUTES, MINUTES);
+ }
+ }
+
+ protected final ListBatches list;
+ protected final BatchRemover remover;
+ protected final Provider<CurrentUser> userProvider;
+
+ protected ScheduledFuture<?> future;
+ protected volatile boolean canceled = false;
+
+ @Inject
+ protected BatchCleaner(
+ ListBatches list, BatchRemover remover, Provider<CurrentUser> userProvider) {
+ this.remover = remover;
+ this.list = list;
+
+ this.userProvider = userProvider;
+ list.query = new ArrayList<String>();
+ list.query.add("is:expired");
+ }
+
+ @Override
+ public void run() {
+ Iterable<Batch> batches = null;
+ try {
+ batches = list.getBatches();
+ } catch (Exception e) {
+ log.error("getting list of batches to clean.", e);
+ // Ignore errors and hope someone notices the log file and fixes before the next run
+ return;
+ }
+
+ for (Batch batch : batches) {
+ if (canceled) {
+ return;
+ }
+ try {
+ remover.remove(batch.id);
+ } catch (Exception e) {
+ log.error("cleaning batch: " + batch.id, e);
+ // Ignore errors and hope someone notices the log file and fixes before the next run
+ }
+ }
+ }
+
+ @Override
+ public void cancel() {
+ if (future != null) {
+ future.cancel(true);
+ canceled = true;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Batch Cleaner";
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/batch/BatchRemover.java b/src/main/java/com/googlesource/gerrit/plugins/batch/BatchRemover.java
index ef37f86..ffb7b41 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/batch/BatchRemover.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/batch/BatchRemover.java
@@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+
package com.googlesource.gerrit.plugins.batch;
import com.google.gerrit.reviewdb.client.Branch;
@@ -20,6 +21,7 @@
import com.google.inject.Inject;
import com.googlesource.gerrit.plugins.batch.exception.NoSuchBatchException;
import java.io.IOException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
public class BatchRemover {
protected final RefUpdater refUpdater;
@@ -32,12 +34,14 @@
}
public Batch remove(String id)
- throws IllegalStateException, IOException, NoSuchBatchException, NoSuchProjectException {
+ throws IllegalStateException, NoSuchBatchException, IOException, RepositoryNotFoundException,
+ NoSuchProjectException {
return remove(store.read(id));
}
public Batch remove(Batch batch)
- throws IOException, IllegalStateException, NoSuchProjectException {
+ throws IOException, IllegalStateException, RepositoryNotFoundException,
+ NoSuchProjectException {
if (batch.state == Batch.State.OPEN) {
throw new IllegalStateException(
"Invalid Operation for Batch(" + batch.id + "): " + batch.state.toString());
@@ -48,7 +52,8 @@
return batch;
}
- protected void removeDownloadRefs(Batch batch) throws IOException, NoSuchProjectException {
+ protected void removeDownloadRefs(Batch batch)
+ throws IOException, RepositoryNotFoundException, NoSuchProjectException {
for (Batch.Destination dest : batch.listDestinations()) {
Project.NameKey project = new Project.NameKey(dest.project);
Branch.NameKey branch = new Branch.NameKey(project, dest.downloadRef);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/batch/BatchStore.java b/src/main/java/com/googlesource/gerrit/plugins/batch/BatchStore.java
index 1e168b9..e2f89cd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/batch/BatchStore.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/batch/BatchStore.java
@@ -59,11 +59,16 @@
}
/** Returns a list of batch objects */
- public List<Batch> find(boolean includeBatchInfo) throws IOException, NoSuchBatchException {
+ public List<Batch> find(boolean includeBatchInfo) throws IOException {
List<Batch> batches = new ArrayList<>();
try (Repository repo = repoManager.openRepository(project)) {
for (Map.Entry<String, Ref> entry : repo.getRefDatabase().getRefs(BATCHES_REF).entrySet()) {
- batches.add((includeBatchInfo == true) ? read(entry.getKey()) : new Batch(entry.getKey()));
+ try {
+ batches.add(
+ (includeBatchInfo == true) ? read(entry.getKey()) : new Batch(entry.getKey()));
+ } catch (NoSuchBatchException e) {
+ continue;
+ }
}
}
return batches;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/batch/ListBatches.java b/src/main/java/com/googlesource/gerrit/plugins/batch/ListBatches.java
index d183d98..778d72f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/batch/ListBatches.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/batch/ListBatches.java
@@ -15,34 +15,51 @@
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.base.Joiner;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.OutputFormat;
import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import com.googlesource.gerrit.plugins.batch.exception.NoSuchBatchException;
+import com.googlesource.gerrit.plugins.batch.query.BatchQueryBuilder;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
import java.util.List;
+import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
public class ListBatches {
+ @Argument(
+ index = 0,
+ required = false,
+ multiValued = true,
+ metaVar = "QUERY",
+ usage = "Query to execute")
+ public List<String> query;
+
@Option(name = "--include-batch-info", usage = "include additional information for every batch")
protected boolean includeBatchInfo;
+ protected final BatchQueryBuilder queryBuilder;
protected final BatchStore store;
@Inject
- ListBatches(BatchStore store) {
+ ListBatches(BatchQueryBuilder queryBuilder, BatchStore store) {
+ this.queryBuilder = queryBuilder;
this.store = store;
}
- public void display(OutputStream displayOutputStream) throws IOException, NoSuchBatchException {
+ public void display(OutputStream displayOutputStream)
+ throws IOException, OrmException, QueryParseException {
try {
PrintWriter stdout =
new PrintWriter(new BufferedWriter(new OutputStreamWriter(displayOutputStream, UTF_8)));
@@ -59,7 +76,18 @@
}
}
- public List<Batch> getBatches() throws IOException, NoSuchBatchException {
- return store.find(includeBatchInfo);
+ public List<Batch> getBatches() throws IOException, OrmException, QueryParseException {
+ Predicate<Batch> pred = null;
+ if (query != null) {
+ pred = queryBuilder.parse(Joiner.on(" ").join(query));
+ includeBatchInfo = true;
+ }
+ List<Batch> batches = new ArrayList<Batch>();
+ for (Batch batch : store.find(includeBatchInfo)) {
+ if (pred == null || pred.asMatchable().match(batch)) {
+ batches.add(batch);
+ }
+ }
+ return batches;
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/batch/Module.java b/src/main/java/com/googlesource/gerrit/plugins/batch/Module.java
index cfb2c15..1ac2911 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/batch/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/batch/Module.java
@@ -14,13 +14,21 @@
package com.googlesource.gerrit.plugins.batch;
import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.git.meta.GitFile;
+import com.google.inject.internal.UniqueAnnotations;
import com.googlesource.gerrit.plugins.batch.util.MergeBranch;
import com.googlesource.gerrit.plugins.batch.util.MergeBuilder;
public class Module extends FactoryModule {
@Override
protected void configure() {
+ factory(GitFile.Factory.class);
factory(MergeBranch.Factory.class);
factory(MergeBuilder.Factory.class);
+
+ bind(LifecycleListener.class)
+ .annotatedWith(UniqueAnnotations.create())
+ .to(BatchCleaner.Lifecycle.class);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/batch/query/BatchQueryBuilder.java b/src/main/java/com/googlesource/gerrit/plugins/batch/query/BatchQueryBuilder.java
new file mode 100644
index 0000000..53f1b6f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/batch/query/BatchQueryBuilder.java
@@ -0,0 +1,99 @@
+// 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.googlesource.gerrit.plugins.batch.query;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.index.query.Matchable;
+import com.google.gerrit.index.query.OperatorPredicate;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryBuilder;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.batch.Batch;
+import com.googlesource.gerrit.plugins.batch.BatchStore;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+
+/** Parses a query string meant to be applied to batch objects. */
+public class BatchQueryBuilder extends QueryBuilder<Batch> {
+ public abstract static class SimplePredicate extends OperatorPredicate<Batch>
+ implements Matchable<Batch> {
+ public SimplePredicate(String op, String val) {
+ super(op, val);
+ }
+
+ @Override
+ public boolean match(Batch b) {
+ return false;
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+ }
+
+ protected static final QueryBuilder.Definition<Batch, BatchQueryBuilder> mydef =
+ new QueryBuilder.Definition<Batch, BatchQueryBuilder>(BatchQueryBuilder.class);
+
+ public static final long DEFAULT_SECONDS = TimeUnit.SECONDS.convert(3, TimeUnit.DAYS);
+
+ protected final ProjectCache projectCache;
+ protected final PluginConfigFactory cfgFactory;
+ protected final BatchStore store;
+ protected final String pluginName;
+
+ @Inject
+ public BatchQueryBuilder(
+ ProjectCache projectCache,
+ PluginConfigFactory cfgFactory,
+ BatchStore store,
+ @PluginName String pluginName) {
+ super(mydef);
+ this.projectCache = projectCache;
+ this.cfgFactory = cfgFactory;
+ this.store = store;
+ this.pluginName = pluginName;
+ }
+
+ public Date getExpiry() throws NoSuchProjectException {
+ Config config = cfgFactory.getProjectPluginConfig(projectCache.getAllProjects(), pluginName);
+ long seconds =
+ ConfigUtil.getTimeUnit(
+ config, "cleaner", null, "maxAge", DEFAULT_SECONDS, TimeUnit.SECONDS);
+ long ms = TimeUnit.MILLISECONDS.convert(seconds, TimeUnit.SECONDS);
+ return new Date(new Date().getTime() - ms);
+ }
+
+ @Operator
+ public Predicate<Batch> is(String value) throws NoSuchProjectException, QueryParseException {
+ if ("expired".equalsIgnoreCase(value)) {
+ return new SimplePredicate("is", value) {
+ Date expiry = getExpiry();
+
+ @Override
+ public boolean match(Batch b) {
+ return b.lastModified.before(expiry);
+ }
+ };
+ }
+ throw error("Invalid query");
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/batch/ssh/SshModule.java b/src/main/java/com/googlesource/gerrit/plugins/batch/ssh/SshModule.java
index 2fd7b81..b80cf29 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/batch/ssh/SshModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/batch/ssh/SshModule.java
@@ -13,15 +13,11 @@
// limitations under the License.
package com.googlesource.gerrit.plugins.batch.ssh;
-import com.google.gerrit.server.git.meta.GitFile;
import com.google.gerrit.sshd.PluginCommandModule;
-import com.google.inject.assistedinject.FactoryModuleBuilder;
public class SshModule extends PluginCommandModule {
@Override
protected void configureCommands() {
- install(new FactoryModuleBuilder().build(GitFile.Factory.class));
-
command(MergeChangeCommand.class);
command(DeleteCommand.class);
command(ListCommand.class);
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 75bf414..8898d4c 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -90,3 +90,72 @@
the special refs/meta/batch/<batch_id> ref in the All-Projects project. This
is internal meta data to the batch plugin and these refs should not be
accessed or altered by users directly.
+
+Batch Cleanup
+-------------
+Batches are temporary proposed updates. They are meant to be
+created, tested, and then submitted to their destinations if
+they pass, or deleted if they fail. Since the output of good
+batches will likely persist on destination branches, and
+the results of bad batches are not typically desirable to keep
+around, the batch service has a background *cleaner* task
+which finds expired batches and deletes them automatically.
+This cleaning helps to ensure that resources are released when
+they are no longer needed.
+
+The cleaner task runs by default daily, and batches are expired
+by default after 3 days from their last modification. It is
+possible to configure expiration times, and the cleaner using
+the `All-Projects` `refs/meta/config` `@PLUGIN@.config` file.
+The `@PLUGIN@.config` file is a "git-config" style file
+and supports the following parameters:
+
+*`cleaner.maxAge`*
+
+: Age after which a batch is considered expired. Values should
+use common unit suffixes to express their setting:
+
+ * s, sec, second, seconds (default unit)
+ * m, min, minute, minutes
+ * h, hr, 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)
+ * y, year, years (1 year is treated as 365 days)
+
+: If a unit suffix is not specified, seconds is assumed. If 0 is
+supplied, the maximum age is infinite and items are never
+expired (they must be deleted manually). The default maxAge is
+3 days.
+
+*`cleaner.interval`*
+
+: Interval for periodic repetition of triggering the batch
+cleanups. The interval must be larger than zero. The following
+suffixes are supported to define the time unit for the interval:
+
+ * m, min, minute, minutes (default suffix)
+ * h, hr, 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)
+ * y, year, years (1 year is treated as 365 days)
+
+: If a unit suffix is not specified, minutes is assumed. The
+default interval is 1 day.
+
+*`cleaner.startDelay`*
+
+: One time delay to wait after plugin load before starting
+the periodic cleaner. The following suffixes are supported
+to define the time unit for the delay:
+
+ * m, min, minute, minutes (default suffix)
+ * h, hr, 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)
+ * y, year, years (1 year is treated as 365 days)
+
+: If a unit suffix is not specified, minutes is assumed. The
+default startDelay is 1 minute.
diff --git a/src/main/resources/Documentation/cmd-ls-batches.md b/src/main/resources/Documentation/cmd-ls-batches.md
index 76c0071..b70d0a7 100644
--- a/src/main/resources/Documentation/cmd-ls-batches.md
+++ b/src/main/resources/Documentation/cmd-ls-batches.md
@@ -10,6 +10,7 @@
```
ssh -p @SSH_PORT@ @SSH_HOST@ @PLUGIN@ ls-batches
[--include-batch-info]
+ <query>
```
OPTIONS
@@ -31,6 +32,21 @@
---------
This command is intended to be used in scripts.
+QUERIES
+-------
+Batches can be queried using a syntax that mimicks the change
+query syntax. Operators act as restrictions on the search. As
+more operators are added to the same query string, they further
+restrict the returned results.
+
+Operators
+---------
+<a name="is:expired"></a>
+*is:expired*
+
+: Batches which have expired ([see batch cleanup](about.md#cleanup))
+
+
EXAMPLES
--------
List visible batches: