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>
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index f8a4159..bd2263c 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -910,6 +910,76 @@
 Default is "Reply and score". In the user interface it becomes "Reply
 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]]
 === Section changeMerge
 
@@ -1519,6 +1589,7 @@
 * `mon, month, months` (`1 month` is treated as `30 days`)
 * `y, year, years` (`1 year` is treated as `365 days`)
 
+[[schedule-examples]]
 Examples::
 +
 ----
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 084160a..1a2ed6b 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -21,6 +21,7 @@
 .. Changes
 ... link:user-changeid.html[Change-Id Lines]
 ... link:user-signedoffby.html[Signed-off-by Lines]
+... link:user-change-cleanup.html[Change Cleanup]
 
 == Project Management
 . link:project-configuration.html[Project Configuration]
diff --git a/Documentation/user-change-cleanup.txt b/Documentation/user-change-cleanup.txt
new file mode 100644
index 0000000..e569f38
--- /dev/null
+++ b/Documentation/user-change-cleanup.txt
@@ -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
+---------
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 1afa243..cb8e9ba 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -44,6 +44,7 @@
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 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.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
@@ -373,6 +374,7 @@
       }
     });
     modules.add(new GarbageCollectionModule());
+    modules.add(new ChangeCleanupRunner.Module());
     return cfgInjector.createChildInjector(modules);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index a73f8ba..5a6b274 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 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.ChangeMessage;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -88,14 +89,23 @@
     } else if (change.getStatus() == Change.Status.DRAFT) {
       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;
     ChangeUpdate update;
+    Change.Id changeId = control.getChange().getId();
     ReviewDb db = dbProvider.get();
-    db.changes().beginTransaction(change.getId());
+    db.changes().beginTransaction(changeId);
     try {
       change = db.changes().atomicUpdate(
-        change.getId(),
+        changeId,
         new AtomicUpdate<Change>() {
           @Override
           public Change update(Change change) {
@@ -109,12 +119,12 @@
         });
       if (change == null) {
         throw new ResourceConflictException("change is "
-            + status(db.changes().get(req.getChange().getId())));
+            + status(db.changes().get(changeId)));
       }
 
       //TODO(yyonas): atomic update was not propagated
       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);
       db.commit();
     } finally {
@@ -125,19 +135,20 @@
     indexer.index(db, change);
     try {
       ReplyToChangeSender cm = abandonedSenderFactory.create(change.getId());
-      cm.setFrom(caller.getAccountId());
+      if (acc != null) {
+        cm.setFrom(acc.getId());
+      }
       cm.setChangeMessage(message);
       cm.send();
     } catch (Exception e) {
       log.error("Cannot email update for change " + change.getChangeId(), e);
     }
     hooks.doChangeAbandonedHook(change,
-        caller.getAccount(),
+        acc,
         db.patchSets().get(change.currentPatchSetId()),
-        Strings.emptyToNull(input.message),
+        Strings.emptyToNull(msgTxt),
         db);
-    ChangeInfo result = json.format(change);
-    return result;
+    return change;
   }
 
   @Override
@@ -150,20 +161,20 @@
           && resource.getControl().canAbandon());
   }
 
-  private ChangeMessage newMessage(AbandonInput input, IdentifiedUser caller,
+  private ChangeMessage newMessage(String msgTxt, Account.Id accId,
       Change change) throws OrmException {
     StringBuilder msg = new StringBuilder();
     msg.append("Abandoned");
-    if (!Strings.nullToEmpty(input.message).trim().isEmpty()) {
+    if (!Strings.nullToEmpty(msgTxt).trim().isEmpty()) {
       msg.append("\n\n");
-      msg.append(input.message.trim());
+      msg.append(msgTxt.trim());
     }
 
     ChangeMessage message = new ChangeMessage(
         new ChangeMessage.Key(
             change.getId(),
             ChangeUtil.messageUUID(dbProvider.get())),
-        caller.getAccountId(),
+        accId,
         change.getLastUpdatedOn(),
         change.currentPatchSetId());
     message.setMessage(msg.toString());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java
new file mode 100644
index 0000000..c6e16be
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java
@@ -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()));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeCleanupRunner.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
new file mode 100644
index 0000000..310c8cb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
@@ -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";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ChangeCleanupConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
new file mode 100644
index 0000000..9d2ede8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
@@ -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;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index a6acb99..b4f744d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -219,6 +219,7 @@
     bind(GitWebConfig.class);
 
     bind(GcConfig.class);
+    bind(ChangeCleanupConfig.class);
 
     bind(ApprovalsUtil.class);
     bind(ChangeMergeQueue.class).in(SINGLETON);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java
index d19f063..91a20dc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ScheduleConfig.java
@@ -38,6 +38,11 @@
   private static final String KEY_INTERVAL = "interval";
   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 interval;
 
@@ -62,6 +67,11 @@
   @VisibleForTesting
   ScheduleConfig(Config rc, String section, String subsection,
       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);
     if (interval > 0) {
       this.initialDelay = initialDelay(rc, section, subsection, keyStartTime, now,
@@ -150,4 +160,31 @@
     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();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index 4a61e3e..098a0cc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -489,6 +489,10 @@
    * @return object suitable for serialization to JSON
    */
   public AccountAttribute asAccountAttribute(final Account account) {
+    if (account == null) {
+      return null;
+    }
+
     AccountAttribute who = new AccountAttribute();
     who.name = account.getFullName();
     who.email = account.getPreferredEmail();
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 3bc8b58..dde4923 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 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.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
@@ -323,6 +324,7 @@
       }
     });
     modules.add(new GarbageCollectionModule());
+    modules.add(new ChangeCleanupRunner.Module());
     return cfgInjector.createChildInjector(modules);
   }