Add background job to abandon inactive changes automatically
Add a change cleanup job that runs periodically in the background. This cleanup job can automatically abandon open changes that have been inactive for a defined time. Abandoning old inactive changes has a few advantages: - it reduces the load for recomputing the mergeability flag when a change is merged - it keeps dashboards clean - it signals change authors that changes are considered outdated Change-Id: Ia798667901fe8d734ca29bcf81c7b9b4a1eb4c50 Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
parent
64d33627b4
commit
e326a1dada
@ -910,6 +910,76 @@ keyboard shortcut is appended.
|
|||||||
Default is "Reply and score". In the user interface it becomes "Reply
|
Default is "Reply and score". In the user interface it becomes "Reply
|
||||||
and score (Shortcut: a)".
|
and score (Shortcut: a)".
|
||||||
|
|
||||||
|
[[changeCleanup]]
|
||||||
|
=== Section changeCleanup
|
||||||
|
|
||||||
|
This section allows to configure change cleanups and schedules them to
|
||||||
|
run periodically.
|
||||||
|
|
||||||
|
[[changeCleanup.abandonAfter]]changeCleanup.abandonAfter::
|
||||||
|
+
|
||||||
|
Period of inactivity after which open changes should be abandoned
|
||||||
|
automatically.
|
||||||
|
+
|
||||||
|
By default `0`, never abandon open changes.
|
||||||
|
+
|
||||||
|
[WARNING] Auto-Abandoning changes may confuse/annoy users. When
|
||||||
|
enabling this, make sure to choose a reasonably large grace period and
|
||||||
|
inform users in advance.
|
||||||
|
+
|
||||||
|
The following suffixes are supported to define the time unit:
|
||||||
|
+
|
||||||
|
* `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`)
|
||||||
|
|
||||||
|
[[changeCleanup.abandonMessage]]changeCleanup.abandonMessage::
|
||||||
|
+
|
||||||
|
Change message that should be posted when a change is abandoned.
|
||||||
|
+
|
||||||
|
'${URL}' can be used as a placeholder for the Gerrit web URL.
|
||||||
|
+
|
||||||
|
By default "Auto-Abandoned due to inactivity, see
|
||||||
|
${URL}Documentation/user-change-cleanup.html#auto-abandon\n\n
|
||||||
|
If this change is still wanted it should be restored.".
|
||||||
|
|
||||||
|
[[changeCleanup.startTime]]changeCleanup.startTime::
|
||||||
|
+
|
||||||
|
Start time to define the first execution of the change cleanups.
|
||||||
|
If the configured `'changeCleanup.interval'` is shorter than
|
||||||
|
`'changeCleanup.startTime - now'` the start time will be preponed by
|
||||||
|
the maximum integral multiple of `'changeCleanup.interval'` so that the
|
||||||
|
start time is still in the future.
|
||||||
|
+
|
||||||
|
----
|
||||||
|
<day of week> <hours>:<minutes>
|
||||||
|
or
|
||||||
|
<hours>:<minutes>
|
||||||
|
|
||||||
|
<day of week> : Mon, Tue, Wed, Thu, Fri, Sat, Sun
|
||||||
|
<hours> : 00-23
|
||||||
|
<minutes> : 0-59
|
||||||
|
----
|
||||||
|
|
||||||
|
|
||||||
|
[[changeCleanup.interval]]changeCleanup.interval::
|
||||||
|
+
|
||||||
|
Interval for periodic repetition of triggering the change cleanups.
|
||||||
|
The interval must be larger than zero. The following suffixes are supported
|
||||||
|
to define the time unit for the interval:
|
||||||
|
+
|
||||||
|
* `s, sec, second, seconds`
|
||||||
|
* `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`)
|
||||||
|
|
||||||
|
link:#schedule-examples[Schedule examples] can be found in the
|
||||||
|
link:#gc[gc] section.
|
||||||
|
|
||||||
[[changeMerge]]
|
[[changeMerge]]
|
||||||
=== Section changeMerge
|
=== Section changeMerge
|
||||||
|
|
||||||
@ -1519,6 +1589,7 @@ to define the time unit for the interval:
|
|||||||
* `mon, month, months` (`1 month` is treated as `30 days`)
|
* `mon, month, months` (`1 month` is treated as `30 days`)
|
||||||
* `y, year, years` (`1 year` is treated as `365 days`)
|
* `y, year, years` (`1 year` is treated as `365 days`)
|
||||||
|
|
||||||
|
[[schedule-examples]]
|
||||||
Examples::
|
Examples::
|
||||||
+
|
+
|
||||||
----
|
----
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
.. Changes
|
.. Changes
|
||||||
... link:user-changeid.html[Change-Id Lines]
|
... link:user-changeid.html[Change-Id Lines]
|
||||||
... link:user-signedoffby.html[Signed-off-by Lines]
|
... link:user-signedoffby.html[Signed-off-by Lines]
|
||||||
|
... link:user-change-cleanup.html[Change Cleanup]
|
||||||
|
|
||||||
== Project Management
|
== Project Management
|
||||||
. link:project-configuration.html[Project Configuration]
|
. link:project-configuration.html[Project Configuration]
|
||||||
|
28
Documentation/user-change-cleanup.txt
Normal file
28
Documentation/user-change-cleanup.txt
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
= Gerrit Code Review - Change Cleanup
|
||||||
|
|
||||||
|
Gerrit administrators may configure
|
||||||
|
link:config-gerrit.html#changeCleanup[change cleanups] that are
|
||||||
|
executed periodically.
|
||||||
|
|
||||||
|
[[auto-abandon]]
|
||||||
|
== Auto-Abandon
|
||||||
|
|
||||||
|
This cleanup job automatically abandons open changes that have been
|
||||||
|
inactive for a defined time.
|
||||||
|
|
||||||
|
Abandoning old inactive changes has the following advantages:
|
||||||
|
|
||||||
|
* it signals change authors that changes are considered outdated
|
||||||
|
* it keeps dashboards clean
|
||||||
|
* it reduces the load on the server (for open changes the mergeability
|
||||||
|
flag is recomputed whenever a change is merged)
|
||||||
|
|
||||||
|
If a change is still wanted it can be restored by clicking on the
|
||||||
|
`Restore` button.
|
||||||
|
|
||||||
|
GERRIT
|
||||||
|
------
|
||||||
|
Part of link:index.html[Gerrit Code Review]
|
||||||
|
|
||||||
|
SEARCHBOX
|
||||||
|
---------
|
@ -44,6 +44,7 @@ import com.google.gerrit.pgm.util.SiteProgram;
|
|||||||
import com.google.gerrit.reviewdb.client.AuthType;
|
import com.google.gerrit.reviewdb.client.AuthType;
|
||||||
import com.google.gerrit.server.account.InternalAccountDirectory;
|
import com.google.gerrit.server.account.InternalAccountDirectory;
|
||||||
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
|
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
|
||||||
|
import com.google.gerrit.server.change.ChangeCleanupRunner;
|
||||||
import com.google.gerrit.server.config.AuthConfig;
|
import com.google.gerrit.server.config.AuthConfig;
|
||||||
import com.google.gerrit.server.config.AuthConfigModule;
|
import com.google.gerrit.server.config.AuthConfigModule;
|
||||||
import com.google.gerrit.server.config.CanonicalWebUrlModule;
|
import com.google.gerrit.server.config.CanonicalWebUrlModule;
|
||||||
@ -373,6 +374,7 @@ public class Daemon extends SiteProgram {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
modules.add(new GarbageCollectionModule());
|
modules.add(new GarbageCollectionModule());
|
||||||
|
modules.add(new ChangeCleanupRunner.Module());
|
||||||
return cfgInjector.createChildInjector(modules);
|
return cfgInjector.createChildInjector(modules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import com.google.gerrit.extensions.restapi.AuthException;
|
|||||||
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||||
import com.google.gerrit.extensions.restapi.RestModifyView;
|
import com.google.gerrit.extensions.restapi.RestModifyView;
|
||||||
import com.google.gerrit.extensions.webui.UiAction;
|
import com.google.gerrit.extensions.webui.UiAction;
|
||||||
|
import com.google.gerrit.reviewdb.client.Account;
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
import com.google.gerrit.reviewdb.client.Change;
|
||||||
import com.google.gerrit.reviewdb.client.ChangeMessage;
|
import com.google.gerrit.reviewdb.client.ChangeMessage;
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
@ -88,14 +89,23 @@ public class Abandon implements RestModifyView<ChangeResource, AbandonInput>,
|
|||||||
} else if (change.getStatus() == Change.Status.DRAFT) {
|
} else if (change.getStatus() == Change.Status.DRAFT) {
|
||||||
throw new ResourceConflictException("draft changes cannot be abandoned");
|
throw new ResourceConflictException("draft changes cannot be abandoned");
|
||||||
}
|
}
|
||||||
|
change = abandon(control, input.message, caller.getAccount());
|
||||||
|
ChangeInfo result = json.format(change);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Change abandon(ChangeControl control,
|
||||||
|
String msgTxt, Account acc) throws ResourceConflictException,
|
||||||
|
OrmException, IOException {
|
||||||
|
Change change;
|
||||||
ChangeMessage message;
|
ChangeMessage message;
|
||||||
ChangeUpdate update;
|
ChangeUpdate update;
|
||||||
|
Change.Id changeId = control.getChange().getId();
|
||||||
ReviewDb db = dbProvider.get();
|
ReviewDb db = dbProvider.get();
|
||||||
db.changes().beginTransaction(change.getId());
|
db.changes().beginTransaction(changeId);
|
||||||
try {
|
try {
|
||||||
change = db.changes().atomicUpdate(
|
change = db.changes().atomicUpdate(
|
||||||
change.getId(),
|
changeId,
|
||||||
new AtomicUpdate<Change>() {
|
new AtomicUpdate<Change>() {
|
||||||
@Override
|
@Override
|
||||||
public Change update(Change change) {
|
public Change update(Change change) {
|
||||||
@ -109,12 +119,12 @@ public class Abandon implements RestModifyView<ChangeResource, AbandonInput>,
|
|||||||
});
|
});
|
||||||
if (change == null) {
|
if (change == null) {
|
||||||
throw new ResourceConflictException("change is "
|
throw new ResourceConflictException("change is "
|
||||||
+ status(db.changes().get(req.getChange().getId())));
|
+ status(db.changes().get(changeId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO(yyonas): atomic update was not propagated
|
//TODO(yyonas): atomic update was not propagated
|
||||||
update = updateFactory.create(control, change.getLastUpdatedOn());
|
update = updateFactory.create(control, change.getLastUpdatedOn());
|
||||||
message = newMessage(input, caller, change);
|
message = newMessage(msgTxt, acc != null ? acc.getId() : null, change);
|
||||||
cmUtil.addChangeMessage(db, update, message);
|
cmUtil.addChangeMessage(db, update, message);
|
||||||
db.commit();
|
db.commit();
|
||||||
} finally {
|
} finally {
|
||||||
@ -125,19 +135,20 @@ public class Abandon implements RestModifyView<ChangeResource, AbandonInput>,
|
|||||||
indexer.index(db, change);
|
indexer.index(db, change);
|
||||||
try {
|
try {
|
||||||
ReplyToChangeSender cm = abandonedSenderFactory.create(change.getId());
|
ReplyToChangeSender cm = abandonedSenderFactory.create(change.getId());
|
||||||
cm.setFrom(caller.getAccountId());
|
if (acc != null) {
|
||||||
|
cm.setFrom(acc.getId());
|
||||||
|
}
|
||||||
cm.setChangeMessage(message);
|
cm.setChangeMessage(message);
|
||||||
cm.send();
|
cm.send();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Cannot email update for change " + change.getChangeId(), e);
|
log.error("Cannot email update for change " + change.getChangeId(), e);
|
||||||
}
|
}
|
||||||
hooks.doChangeAbandonedHook(change,
|
hooks.doChangeAbandonedHook(change,
|
||||||
caller.getAccount(),
|
acc,
|
||||||
db.patchSets().get(change.currentPatchSetId()),
|
db.patchSets().get(change.currentPatchSetId()),
|
||||||
Strings.emptyToNull(input.message),
|
Strings.emptyToNull(msgTxt),
|
||||||
db);
|
db);
|
||||||
ChangeInfo result = json.format(change);
|
return change;
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -150,20 +161,20 @@ public class Abandon implements RestModifyView<ChangeResource, AbandonInput>,
|
|||||||
&& resource.getControl().canAbandon());
|
&& resource.getControl().canAbandon());
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChangeMessage newMessage(AbandonInput input, IdentifiedUser caller,
|
private ChangeMessage newMessage(String msgTxt, Account.Id accId,
|
||||||
Change change) throws OrmException {
|
Change change) throws OrmException {
|
||||||
StringBuilder msg = new StringBuilder();
|
StringBuilder msg = new StringBuilder();
|
||||||
msg.append("Abandoned");
|
msg.append("Abandoned");
|
||||||
if (!Strings.nullToEmpty(input.message).trim().isEmpty()) {
|
if (!Strings.nullToEmpty(msgTxt).trim().isEmpty()) {
|
||||||
msg.append("\n\n");
|
msg.append("\n\n");
|
||||||
msg.append(input.message.trim());
|
msg.append(msgTxt.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangeMessage message = new ChangeMessage(
|
ChangeMessage message = new ChangeMessage(
|
||||||
new ChangeMessage.Key(
|
new ChangeMessage.Key(
|
||||||
change.getId(),
|
change.getId(),
|
||||||
ChangeUtil.messageUUID(dbProvider.get())),
|
ChangeUtil.messageUUID(dbProvider.get())),
|
||||||
caller.getAccountId(),
|
accId,
|
||||||
change.getLastUpdatedOn(),
|
change.getLastUpdatedOn(),
|
||||||
change.currentPatchSetId());
|
change.currentPatchSetId());
|
||||||
message.setMessage(msg.toString());
|
message.setMessage(msg.toString());
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
// Copyright (C) 2015 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.google.gerrit.server.change;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.restapi.ResourceConflictException;
|
||||||
|
import com.google.gerrit.reviewdb.client.Change;
|
||||||
|
import com.google.gerrit.server.IdentifiedUser;
|
||||||
|
import com.google.gerrit.server.config.ChangeCleanupConfig;
|
||||||
|
import com.google.gerrit.server.project.ChangeControl;
|
||||||
|
import com.google.gerrit.server.project.NoSuchChangeException;
|
||||||
|
import com.google.gerrit.server.query.QueryParseException;
|
||||||
|
import com.google.gerrit.server.query.change.ChangeData;
|
||||||
|
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
|
||||||
|
import com.google.gerrit.server.query.change.QueryProcessor;
|
||||||
|
import com.google.gwtorm.server.OrmException;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class AbandonUtil {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AbandonUtil.class);
|
||||||
|
|
||||||
|
private final ChangeCleanupConfig cfg;
|
||||||
|
private final IdentifiedUser.GenericFactory identifiedUserFactory;
|
||||||
|
private final QueryProcessor queryProcessor;
|
||||||
|
private final ChangeQueryBuilder queryBuilder;
|
||||||
|
private final ChangeControl.GenericFactory changeControlFactory;
|
||||||
|
private final Abandon abandon;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AbandonUtil(
|
||||||
|
ChangeCleanupConfig cfg,
|
||||||
|
IdentifiedUser.GenericFactory identifiedUserFactory,
|
||||||
|
QueryProcessor queryProcessor,
|
||||||
|
ChangeQueryBuilder queryBuilder,
|
||||||
|
ChangeControl.GenericFactory changeControlFactory,
|
||||||
|
Abandon abandon) {
|
||||||
|
this.cfg = cfg;
|
||||||
|
this.identifiedUserFactory = identifiedUserFactory;
|
||||||
|
this.queryProcessor = queryProcessor;
|
||||||
|
this.queryBuilder = queryBuilder;
|
||||||
|
this.changeControlFactory = changeControlFactory;
|
||||||
|
this.abandon = abandon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void abandonInactiveOpenChanges() {
|
||||||
|
if (cfg.getAbandonAfter() <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String query = "status:new age:"
|
||||||
|
+ TimeUnit.MILLISECONDS.toMinutes(cfg.getAbandonAfter())
|
||||||
|
+ "m";
|
||||||
|
List<ChangeData> changesToAbandon = queryProcessor.enforceVisibility(false)
|
||||||
|
.queryChanges(queryBuilder.parse(query)).changes();
|
||||||
|
for (ChangeData cd : changesToAbandon) {
|
||||||
|
try {
|
||||||
|
abandon.abandon(changeControl(cd), cfg.getAbandonMessage(), null);
|
||||||
|
} catch (ResourceConflictException e) {
|
||||||
|
// Change was already merged or abandoned.
|
||||||
|
} catch (Throwable e) {
|
||||||
|
log.error(String.format(
|
||||||
|
"Failed to auto-abandon inactive open change %d.",
|
||||||
|
cd.getId().get()), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (QueryParseException | OrmException e) {
|
||||||
|
log.error("Failed to query inactive open changes for auto-abandoning.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChangeControl changeControl(ChangeData cd)
|
||||||
|
throws NoSuchChangeException, OrmException {
|
||||||
|
Change c = cd.change();
|
||||||
|
return changeControlFactory.controlFor(c,
|
||||||
|
identifiedUserFactory.create(c.getOwner()));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
// Copyright (C) 2015 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.google.gerrit.server.change;
|
||||||
|
|
||||||
|
import static com.google.gerrit.server.config.ScheduleConfig.MISSING_CONFIG;
|
||||||
|
|
||||||
|
import com.google.gerrit.extensions.events.LifecycleListener;
|
||||||
|
import com.google.gerrit.lifecycle.LifecycleModule;
|
||||||
|
import com.google.gerrit.server.config.ChangeCleanupConfig;
|
||||||
|
import com.google.gerrit.server.config.ScheduleConfig;
|
||||||
|
import com.google.gerrit.server.git.WorkQueue;
|
||||||
|
import com.google.gerrit.server.util.ManualRequestContext;
|
||||||
|
import com.google.gerrit.server.util.OneOffRequestContext;
|
||||||
|
import com.google.gwtorm.server.OrmException;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/** Runnable to enable scheduling change cleanups to run periodically */
|
||||||
|
public class ChangeCleanupRunner implements Runnable {
|
||||||
|
private static final Logger log = LoggerFactory
|
||||||
|
.getLogger(ChangeCleanupRunner.class);
|
||||||
|
|
||||||
|
public static class Module extends LifecycleModule {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
listener().to(Lifecycle.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Lifecycle implements LifecycleListener {
|
||||||
|
private final WorkQueue queue;
|
||||||
|
private final ChangeCleanupRunner runner;
|
||||||
|
private final ChangeCleanupConfig cfg;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Lifecycle(WorkQueue queue,
|
||||||
|
ChangeCleanupRunner runner,
|
||||||
|
ChangeCleanupConfig cfg) {
|
||||||
|
this.queue = queue;
|
||||||
|
this.runner = runner;
|
||||||
|
this.cfg = cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
ScheduleConfig scheduleConfig = cfg.getScheduleConfig();
|
||||||
|
long interval = scheduleConfig.getInterval();
|
||||||
|
long delay = scheduleConfig.getInitialDelay();
|
||||||
|
if (delay == MISSING_CONFIG && interval == MISSING_CONFIG) {
|
||||||
|
log.info("Ignoring missing changeCleanup schedule configuration");
|
||||||
|
} else if (delay < 0 || interval <= 0) {
|
||||||
|
log.warn(String.format(
|
||||||
|
"Ignoring invalid changeCleanup schedule configuration: %s",
|
||||||
|
scheduleConfig));
|
||||||
|
} else {
|
||||||
|
queue.getDefaultQueue().scheduleAtFixedRate(runner, delay,
|
||||||
|
interval, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
// handled by WorkQueue.stop() already
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final OneOffRequestContext oneOffRequestContext;
|
||||||
|
private final AbandonUtil abandonUtil;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ChangeCleanupRunner(
|
||||||
|
OneOffRequestContext oneOffRequestContext,
|
||||||
|
AbandonUtil abandonUtil) {
|
||||||
|
this.oneOffRequestContext = oneOffRequestContext;
|
||||||
|
this.abandonUtil = abandonUtil;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
log.info("Running change cleanups.");
|
||||||
|
try (ManualRequestContext ctx = oneOffRequestContext.open()) {
|
||||||
|
abandonUtil.abandonInactiveOpenChanges();
|
||||||
|
} catch (OrmException e) {
|
||||||
|
log.error("Failed to cleanup changes.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "change cleanup runner";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
// Copyright (C) 2015 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.google.gerrit.server.config;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lib.Config;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class ChangeCleanupConfig {
|
||||||
|
private static String SECTION = "changeCleanup";
|
||||||
|
private static String KEY_ABANDON_AFTER = "abandonAfter";
|
||||||
|
private static String KEY_ABANDON_MESSAGE = "abandonMessage";
|
||||||
|
private static String DEFAULT_ABANDON_MESSAGE =
|
||||||
|
"Auto-Abandoned due to inactivity, see "
|
||||||
|
+ "${URL}Documentation/user-change-cleanup.html#auto-abandon\n"
|
||||||
|
+ "\n"
|
||||||
|
+ "If this change is still wanted it should be restored.";
|
||||||
|
|
||||||
|
private final ScheduleConfig scheduleConfig;
|
||||||
|
private final long abandonAfter;
|
||||||
|
private final String abandonMessage;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ChangeCleanupConfig(@GerritServerConfig Config cfg,
|
||||||
|
@CanonicalWebUrl String canonicalWebUrl) {
|
||||||
|
scheduleConfig = new ScheduleConfig(cfg, SECTION);
|
||||||
|
abandonAfter = readAbandonAfter(cfg);
|
||||||
|
abandonMessage = readAbandonMessage(cfg, canonicalWebUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long readAbandonAfter(Config cfg) {
|
||||||
|
long abandonAfter =
|
||||||
|
ConfigUtil.getTimeUnit(cfg, SECTION, null, KEY_ABANDON_AFTER, 0,
|
||||||
|
TimeUnit.MILLISECONDS);
|
||||||
|
return abandonAfter >= 0 ? abandonAfter : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readAbandonMessage(Config cfg, String webUrl) {
|
||||||
|
String abandonMessage = cfg.getString(SECTION, null, KEY_ABANDON_MESSAGE);
|
||||||
|
if (Strings.isNullOrEmpty(abandonMessage)) {
|
||||||
|
abandonMessage = DEFAULT_ABANDON_MESSAGE;
|
||||||
|
}
|
||||||
|
abandonMessage = abandonMessage.replaceAll("\\$\\{URL\\}", webUrl);
|
||||||
|
return abandonMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScheduleConfig getScheduleConfig() {
|
||||||
|
return scheduleConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getAbandonAfter() {
|
||||||
|
return abandonAfter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAbandonMessage() {
|
||||||
|
return abandonMessage;
|
||||||
|
}
|
||||||
|
}
|
@ -219,6 +219,7 @@ public class GerritGlobalModule extends FactoryModule {
|
|||||||
bind(GitWebConfig.class);
|
bind(GitWebConfig.class);
|
||||||
|
|
||||||
bind(GcConfig.class);
|
bind(GcConfig.class);
|
||||||
|
bind(ChangeCleanupConfig.class);
|
||||||
|
|
||||||
bind(ApprovalsUtil.class);
|
bind(ApprovalsUtil.class);
|
||||||
bind(ChangeMergeQueue.class).in(SINGLETON);
|
bind(ChangeMergeQueue.class).in(SINGLETON);
|
||||||
|
@ -38,6 +38,11 @@ public class ScheduleConfig {
|
|||||||
private static final String KEY_INTERVAL = "interval";
|
private static final String KEY_INTERVAL = "interval";
|
||||||
private static final String KEY_STARTTIME = "startTime";
|
private static final String KEY_STARTTIME = "startTime";
|
||||||
|
|
||||||
|
private final Config rc;
|
||||||
|
private final String section;
|
||||||
|
private final String subsection;
|
||||||
|
private final String keyInterval;
|
||||||
|
private final String keyStartTime;
|
||||||
private final long initialDelay;
|
private final long initialDelay;
|
||||||
private final long interval;
|
private final long interval;
|
||||||
|
|
||||||
@ -62,6 +67,11 @@ public class ScheduleConfig {
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
ScheduleConfig(Config rc, String section, String subsection,
|
ScheduleConfig(Config rc, String section, String subsection,
|
||||||
String keyInterval, String keyStartTime, DateTime now) {
|
String keyInterval, String keyStartTime, DateTime now) {
|
||||||
|
this.rc = rc;
|
||||||
|
this.section = section;
|
||||||
|
this.subsection = subsection;
|
||||||
|
this.keyInterval = keyInterval;
|
||||||
|
this.keyStartTime = keyStartTime;
|
||||||
this.interval = interval(rc, section, subsection, keyInterval);
|
this.interval = interval(rc, section, subsection, keyInterval);
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
this.initialDelay = initialDelay(rc, section, subsection, keyStartTime, now,
|
this.initialDelay = initialDelay(rc, section, subsection, keyStartTime, now,
|
||||||
@ -150,4 +160,31 @@ public class ScheduleConfig {
|
|||||||
return delay;
|
return delay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder b = new StringBuilder();
|
||||||
|
b.append(formatValue(keyInterval));
|
||||||
|
b.append(", ");
|
||||||
|
b.append(formatValue(keyStartTime));
|
||||||
|
return b.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatValue(String key) {
|
||||||
|
StringBuilder b = new StringBuilder();
|
||||||
|
b.append(section);
|
||||||
|
if (subsection != null) {
|
||||||
|
b.append(".");
|
||||||
|
b.append(subsection);
|
||||||
|
}
|
||||||
|
b.append(".");
|
||||||
|
b.append(key);
|
||||||
|
String value = rc.getString(section, subsection, key);
|
||||||
|
if (value != null) {
|
||||||
|
b.append(" = ");
|
||||||
|
b.append(value);
|
||||||
|
} else {
|
||||||
|
b.append(": NA");
|
||||||
|
}
|
||||||
|
return b.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -489,6 +489,10 @@ public class EventFactory {
|
|||||||
* @return object suitable for serialization to JSON
|
* @return object suitable for serialization to JSON
|
||||||
*/
|
*/
|
||||||
public AccountAttribute asAccountAttribute(final Account account) {
|
public AccountAttribute asAccountAttribute(final Account account) {
|
||||||
|
if (account == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
AccountAttribute who = new AccountAttribute();
|
AccountAttribute who = new AccountAttribute();
|
||||||
who.name = account.getFullName();
|
who.name = account.getFullName();
|
||||||
who.email = account.getPreferredEmail();
|
who.email = account.getPreferredEmail();
|
||||||
|
@ -28,6 +28,7 @@ import com.google.gerrit.lucene.LuceneIndexModule;
|
|||||||
import com.google.gerrit.reviewdb.client.AuthType;
|
import com.google.gerrit.reviewdb.client.AuthType;
|
||||||
import com.google.gerrit.server.account.InternalAccountDirectory;
|
import com.google.gerrit.server.account.InternalAccountDirectory;
|
||||||
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
|
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
|
||||||
|
import com.google.gerrit.server.change.ChangeCleanupRunner;
|
||||||
import com.google.gerrit.server.config.AuthConfig;
|
import com.google.gerrit.server.config.AuthConfig;
|
||||||
import com.google.gerrit.server.config.AuthConfigModule;
|
import com.google.gerrit.server.config.AuthConfigModule;
|
||||||
import com.google.gerrit.server.config.CanonicalWebUrlModule;
|
import com.google.gerrit.server.config.CanonicalWebUrlModule;
|
||||||
@ -323,6 +324,7 @@ public class WebAppInitializer extends GuiceServletContextListener
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
modules.add(new GarbageCollectionModule());
|
modules.add(new GarbageCollectionModule());
|
||||||
|
modules.add(new ChangeCleanupRunner.Module());
|
||||||
return cfgInjector.createChildInjector(modules);
|
return cfgInjector.createChildInjector(modules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user