Merge "Update JGit to acf21c0bc" into stable-3.5
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 9e36313..d942aa3 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1889,6 +1889,13 @@
 to run a new Gerrit daemon successfully.  If not set, defaults to
 90 seconds.
 
+[[container.shutdownTimeout]]container.shutdownTimeout::
++
+The maximum time (in seconds) to wait for a gerrit.sh stop command.
+This is added to the highest value between either 'sshd.gracefulStopTimeout'
+or 'httpd.gracefulStopTimeout'. If not set, defaults to
+30 seconds
+
 [[container.user]]container.user::
 +
 Login name (or UID) of the operating system user the Gerrit JVM
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index ab1d5f1..668a846 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -54,7 +54,7 @@
 To build Gerrit with Java 11 language level, run:
 
 ```
-  $ bazel build --java_toolchain=//tools:error_prone_warnings_toolchain_java11 :release
+  $ bazel build :release
 ```
 
 [[java-17]]
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 49ac84c..5470709 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -368,12 +368,27 @@
 * `git/upload-pack/request_count`: Total number of git-upload-pack requests.
 ** `operation`:
    The name of the operation (CLONE, FETCH).
+* `git/upload-pack/bitmap_index_misses_count`: Number of bitmap index misses per request.
+** `operation`:
+   The name of the operation (CLONE, FETCH).
+* `git/upload-pack/no_bitmap_index`: Total number of requests executed without a bitmap index.
+** `operation`:
+   The name of the operation (CLONE, FETCH).
 * `git/upload-pack/phase_counting`: Time spent in the 'Counting...' phase.
 ** `operation`:
    The name of the operation (CLONE, FETCH).
 * `git/upload-pack/phase_compressing`: Time spent in the 'Compressing...' phase.
 ** `operation`:
    The name of the operation (CLONE, FETCH).
+* `git/upload-pack/phase_negotiating`: Time spent in the negotiation phase.
+** `operation`:
+   The name of the operation (CLONE, FETCH).
+* `git/upload-pack/phase_searching_for_reuse`: Time spent in the 'Finding sources...' while searching for reuse phase.
+** `operation`:
+   The name of the operation (CLONE, FETCH).
+* `git/upload-pack/phase_searching_for_sizes`: Time spent in the 'Finding sources...' while searching for sizes phase.
+** `operation`:
+   The name of the operation (CLONE, FETCH).
 * `git/upload-pack/phase_writing`: Time spent transferring bytes to client.
 ** `operation`:
    The name of the operation (CLONE, FETCH).
diff --git a/WORKSPACE b/WORKSPACE
index c84b753..13dcddb 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -812,55 +812,55 @@
     sha1 = "7e060dd5b19431e6d198e91ff670644372f60fbd",
 )
 
-JETTY_VERS = "9.4.36.v20210114"
+JETTY_VERS = "9.4.53.v20231009"
 
 maven_jar(
     name = "jetty-servlet",
     artifact = "org.eclipse.jetty:jetty-servlet:" + JETTY_VERS,
-    sha1 = "b189e52a5ee55ae172e4e99e29c5c314f5daf4b9",
+    sha1 = "6670d6a54cdcaedd8090e8cf420fd5dd7d08e859",
 )
 
 maven_jar(
     name = "jetty-security",
     artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VERS,
-    sha1 = "42030d6ed7dfc0f75818cde0adcf738efc477574",
+    sha1 = "6fbc8ebe9046954dc2f51d4ba69c8f8344b05f7f",
 )
 
 maven_jar(
     name = "jetty-server",
     artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VERS,
-    sha1 = "88a7d342974aadca658e7386e8d0fcc5c0788f41",
+    sha1 = "8b0e761a0b359db59dae77c00b4213b0586cb994",
 )
 
 maven_jar(
     name = "jetty-jmx",
     artifact = "org.eclipse.jetty:jetty-jmx:" + JETTY_VERS,
-    sha1 = "bb3847eabe085832aeaedd30e872b40931632e54",
+    sha1 = "f0392f756b59f65ea7d6be41bf7a2f7b2c7c98d5",
 )
 
 maven_jar(
     name = "jetty-http",
     artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VERS,
-    sha1 = "1eee89a55e04ff94df0f85d95200fc48acb43d86",
+    sha1 = "87faf21eb322753f0527bcb88c43e67044786369",
 )
 
 maven_jar(
     name = "jetty-io",
     artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VERS,
-    sha1 = "84a8faf9031eb45a5a2ddb7681e22c483d81ab3a",
+    sha1 = "70cf7649b27c964ad29bfddf58f3bfe0d30346cf",
 )
 
 maven_jar(
     name = "jetty-util",
     artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VERS,
-    sha1 = "925257fbcca6b501a25252c7447dbedb021f7404",
+    sha1 = "f72bb4f687b4454052c6f06528ba9910714df947",
 )
 
 maven_jar(
     name = "jetty-util-ajax",
     artifact = "org.eclipse.jetty:jetty-util-ajax:" + JETTY_VERS,
-    sha1 = "2f478130c21787073facb64d7242e06f94980c60",
-    src_sha1 = "7153d7ca38878d971fd90992c303bb7719ba7a21",
+    sha1 = "4d20f6206eb7747293697c5f64c2dc5bf4bd54a4",
+    src_sha1 = "1aed8017c3c8a449323901639de6b4eb3b1f02ea",
 )
 
 maven_jar(
diff --git a/java/com/google/gerrit/index/Schema.java b/java/com/google/gerrit/index/Schema.java
index 91c3f70..a14e583 100644
--- a/java/com/google/gerrit/index/Schema.java
+++ b/java/com/google/gerrit/index/Schema.java
@@ -207,21 +207,19 @@
   /**
    * Build all fields in the schema from an input object.
    *
-   * <p>Null values are omitted, as are fields which cause errors, which are logged.
+   * <p>Null values are omitted, as are fields which cause errors, which are logged. If any of the
+   * fields cause a StorageException, the whole operation fails and the exception is propagated to
+   * the caller.
    *
    * @param obj input object.
    * @param skipFields set of field names to skip when indexing the document
    * @return all non-null field values from the object.
    */
   public final Iterable<Values<T>> buildFields(T obj, ImmutableSet<String> skipFields) {
-    try {
-      return fields.values().stream()
-          .map(f -> fieldValues(obj, f, skipFields))
-          .filter(Objects::nonNull)
-          .collect(toImmutableList());
-    } catch (StorageException e) {
-      return ImmutableList.of();
-    }
+    return fields.values().stream()
+        .map(f -> fieldValues(obj, f, skipFields))
+        .filter(Objects::nonNull)
+        .collect(toImmutableList());
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/account/GroupCacheImpl.java b/java/com/google/gerrit/server/account/GroupCacheImpl.java
index eaec9ba..36f725f 100644
--- a/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -283,7 +283,7 @@
       List<Cache.GroupKeyProto> keyList = new ArrayList<>();
       try (TraceTimer ignored =
               TraceContext.newTimer(
-                  "Loading group from serialized cache",
+                  "Building keys to load group(s) from serialized cache",
                   Metadata.builder().cacheName(BYUUID_NAME_PERSISTED).build());
           Repository allUsers = repoManager.openRepository(allUsersName)) {
         while (uuidIterator.hasNext()) {
@@ -302,8 +302,13 @@
           keyList.add(key);
         }
       }
-      persistedCache.getAll(keyList).entrySet().stream()
-          .forEach(g -> toReturn.put(g.getKey().getUuid(), Optional.of(g.getValue())));
+      try (TraceTimer ignored =
+          TraceContext.newTimer(
+              "Loading group(s) from serialized cache",
+              Metadata.builder().cacheName(BYUUID_NAME_PERSISTED).build())) {
+        persistedCache.getAll(keyList).entrySet().stream()
+            .forEach(g -> toReturn.put(g.getKey().getUuid(), Optional.of(g.getValue())));
+      }
       return toReturn;
     }
   }
diff --git a/java/com/google/gerrit/server/approval/RecursiveApprovalCopier.java b/java/com/google/gerrit/server/approval/RecursiveApprovalCopier.java
index 24595674..d5ee143 100644
--- a/java/com/google/gerrit/server/approval/RecursiveApprovalCopier.java
+++ b/java/com/google/gerrit/server/approval/RecursiveApprovalCopier.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.server.FanOutExecutor;
 import com.google.gerrit.server.InternalUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -68,6 +69,7 @@
   private final InternalUser.Factory internalUserFactory;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeNotes.Factory changeNotesFactory;
+  private final GitReferenceUpdated gitRefUpdated;
   private final ListeningExecutorService executor;
 
   private final ConcurrentHashMap<Project.NameKey, List<ReceiveCommand>> pendingRefUpdates =
@@ -88,12 +90,14 @@
       InternalUser.Factory internalUserFactory,
       ApprovalsUtil approvalsUtil,
       ChangeNotes.Factory changeNotesFactory,
+      GitReferenceUpdated gitRefUpdated,
       @FanOutExecutor ExecutorService executor) {
     this.batchUpdateFactory = batchUpdateFactory;
     this.repositoryManager = repositoryManager;
     this.internalUserFactory = internalUserFactory;
     this.approvalsUtil = approvalsUtil;
     this.changeNotesFactory = changeNotesFactory;
+    this.gitRefUpdated = gitRefUpdated;
     this.executor = MoreExecutors.listeningDecorator(executor);
   }
 
@@ -240,6 +244,7 @@
       }
       bu.addCommand(updates);
       RefUpdateUtil.executeChecked(bu, repository);
+      gitRefUpdated.fire(project, bu, null);
 
       finishedRefUpdates.addAndGet(updates.size());
       logProgress();
diff --git a/java/com/google/gerrit/server/cache/CacheInfo.java b/java/com/google/gerrit/server/cache/CacheInfo.java
index d6eb065..832ca04 100644
--- a/java/com/google/gerrit/server/cache/CacheInfo.java
+++ b/java/com/google/gerrit/server/cache/CacheInfo.java
@@ -90,7 +90,7 @@
       space = bytes(value);
     }
 
-    private static String bytes(double value) {
+    public static String bytes(double value) {
       value /= 1024;
       String suffix = "k";
 
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index d5e5c7f..5c6fd70 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -28,6 +28,7 @@
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.cache.CacheInfo;
 import com.google.gerrit.server.cache.PersistentCache;
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
 import com.google.gerrit.server.logging.Metadata;
@@ -660,12 +661,19 @@
           try (ResultSet r = s.executeQuery("SELECT SUM(space) FROM data")) {
             used = r.next() ? r.getLong(1) : 0;
           }
+          String formattedMaxSize = CacheInfo.EntriesInfo.bytes(maxSize);
           if (used <= maxSize) {
+            logger.atFine().log(
+                "Cache %s size (%s) is less than maxSize (%s), not pruning",
+                url, CacheInfo.EntriesInfo.bytes(used), formattedMaxSize);
             return;
           }
 
           try (ResultSet r =
               s.executeQuery("SELECT k, space, created FROM data ORDER BY accessed")) {
+            logger.atInfo().log(
+                "Cache %s size (%s) is greater than maxSize (%s), pruning",
+                url, CacheInfo.EntriesInfo.bytes(used), formattedMaxSize);
             while (maxSize < used && r.next()) {
               K key = keyType.get(r, 1);
               Timestamp created = r.getTimestamp(3);
@@ -676,6 +684,9 @@
                 used -= r.getLong(2);
               }
             }
+            logger.atInfo().log(
+                "Done pruning cache %s, size (%s) is now less than maxSize (%s)",
+                url, CacheInfo.EntriesInfo.bytes(used), formattedMaxSize);
           }
         }
       } catch (IOException | SQLException e) {
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index c1333cb..fac05d2 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -679,6 +679,10 @@
       return false;
     }
 
+    return canMerge(mergeTip, repo, toMerge);
+  }
+
+  private boolean canMerge(CodeReviewCommit mergeTip, Repository repo, CodeReviewCommit toMerge) {
     try (ObjectInserter ins = new InMemoryInserter(repo)) {
       return newThreeWayMerger(ins, repo.getConfig()).merge(mergeTip, toMerge);
     } catch (LargeObjectException e) {
@@ -700,6 +704,11 @@
       return false;
     }
 
+    return canFastForward(mergeTip, rw, toMerge);
+  }
+
+  private boolean canFastForward(
+      CodeReviewCommit mergeTip, CodeReviewRevWalk rw, CodeReviewCommit toMerge) {
     try {
       return mergeTip == null
           || rw.isMergedInto(mergeTip, toMerge)
@@ -709,6 +718,19 @@
     }
   }
 
+  public boolean canFastForwardOrMerge(
+      MergeSorter mergeSorter,
+      CodeReviewCommit mergeTip,
+      CodeReviewRevWalk rw,
+      Repository repo,
+      CodeReviewCommit toMerge) {
+    if (hasMissingDependencies(mergeSorter, toMerge)) {
+      return false;
+    }
+
+    return canFastForward(mergeTip, rw, toMerge) || canMerge(mergeTip, repo, toMerge);
+  }
+
   public boolean canCherryPick(
       MergeSorter mergeSorter,
       Repository repo,
@@ -751,8 +773,7 @@
     // by an equivalent merge with a different first parent. So
     // instead behave as though MERGE_IF_NECESSARY was configured.
     //
-    return canFastForward(mergeSorter, mergeTip, rw, toMerge)
-        || canMerge(mergeSorter, repo, mergeTip, toMerge);
+    return canFastForwardOrMerge(mergeSorter, mergeTip, rw, repo, toMerge);
   }
 
   public boolean hasMissingDependencies(MergeSorter mergeSorter, CodeReviewCommit toMerge) {
diff --git a/java/com/google/gerrit/server/git/UploadPackMetricsHook.java b/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
index 1619add..0e981f2 100644
--- a/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
+++ b/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
@@ -38,7 +38,12 @@
 
   private final Counter1<Operation> requestCount;
   private final Timer1<Operation> counting;
+  private final Histogram1<Operation> bitmapIndexMissesCount;
+  private final Counter1<Operation> noBitmapIndex;
   private final Timer1<Operation> compressing;
+  private final Timer1<Operation> negotiating;
+  private final Timer1<Operation> searchingForReuse;
+  private final Timer1<Operation> searchingForSizes;
   private final Timer1<Operation> writing;
   private final Histogram1<Operation> packBytes;
 
@@ -64,6 +69,22 @@
                 .setUnit(Units.MILLISECONDS),
             operationField);
 
+    bitmapIndexMissesCount =
+        metricMaker.newHistogram(
+            "git/upload-pack/bitmap_index_misses_count",
+            new Description("Number of bitmap index misses per request")
+                .setCumulative()
+                .setUnit("misses"),
+            operationField);
+
+    noBitmapIndex =
+        metricMaker.newCounter(
+            "git/upload-pack/no_bitmap_index",
+            new Description("Total number of requests executed without a bitmap index")
+                .setRate()
+                .setUnit("requests"),
+            operationField);
+
     compressing =
         metricMaker.newTimer(
             "git/upload-pack/phase_compressing",
@@ -72,6 +93,32 @@
                 .setUnit(Units.MILLISECONDS),
             operationField);
 
+    negotiating =
+        metricMaker.newTimer(
+            "git/upload-pack/phase_negotiating",
+            new Description("Time spent in the negotiation phase")
+                .setCumulative()
+                .setUnit(Units.MILLISECONDS),
+            operationField);
+
+    searchingForReuse =
+        metricMaker.newTimer(
+            "git/upload-pack/phase_searching_for_reuse",
+            new Description(
+                    "Time spent in the 'Finding sources...' while searching for reuse phase")
+                .setCumulative()
+                .setUnit(Units.MILLISECONDS),
+            operationField);
+
+    searchingForSizes =
+        metricMaker.newTimer(
+            "git/upload-pack/phase_searching_for_sizes",
+            new Description(
+                    "Time spent in the 'Finding sources...' while searching for sizes phase")
+                .setCumulative()
+                .setUnit(Units.MILLISECONDS),
+            operationField);
+
     writing =
         metricMaker.newTimer(
             "git/upload-pack/phase_writing",
@@ -98,7 +145,16 @@
 
     requestCount.increment(op);
     counting.record(op, stats.getTimeCounting(), MILLISECONDS);
+    long bitmapIndexMisses = stats.getBitmapIndexMisses();
+    if (bitmapIndexMisses < 0) {
+      noBitmapIndex.increment(op);
+    } else {
+      bitmapIndexMissesCount.record(op, bitmapIndexMisses);
+    }
     compressing.record(op, stats.getTimeCompressing(), MILLISECONDS);
+    negotiating.record(op, stats.getTimeNegotiating(), MILLISECONDS);
+    searchingForReuse.record(op, stats.getTimeSearchingForReuse(), MILLISECONDS);
+    searchingForSizes.record(op, stats.getTimeSearchingForSizes(), MILLISECONDS);
     writing.record(op, stats.getTimeWriting(), MILLISECONDS);
     packBytes.record(op, stats.getTotalBytes());
   }
diff --git a/java/com/google/gerrit/server/submit/MergeIfNecessary.java b/java/com/google/gerrit/server/submit/MergeIfNecessary.java
index 30f1661..c6877d2 100644
--- a/java/com/google/gerrit/server/submit/MergeIfNecessary.java
+++ b/java/com/google/gerrit/server/submit/MergeIfNecessary.java
@@ -48,7 +48,7 @@
 
   static boolean dryRun(
       SubmitDryRun.Arguments args, CodeReviewCommit mergeTip, CodeReviewCommit toMerge) {
-    return args.mergeUtil.canFastForward(args.mergeSorter, mergeTip, args.rw, toMerge)
-        || args.mergeUtil.canMerge(args.mergeSorter, args.repo, mergeTip, toMerge);
+    return args.mergeUtil.canFastForwardOrMerge(
+        args.mergeSorter, mergeTip, args.rw, args.repo, toMerge);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsIT.java
index c2f7771..4d1b032 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/CopyApprovalsIT.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.AtomicLongMap;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
@@ -30,9 +31,13 @@
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ApprovalInfo;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.approval.RecursiveApprovalCopier;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
+import com.google.inject.Module;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -46,6 +51,21 @@
   @Inject private ProjectOperations projectOperations;
   @Inject private RecursiveApprovalCopier recursiveApprovalCopier;
 
+  @Override
+  public Module createModule() {
+    return new AbstractModule() {
+      @Override
+      protected void configure() {
+        CopyApprovalsReferenceUpdateListener referenceUpdateListener =
+            new CopyApprovalsReferenceUpdateListener();
+
+        bind(CopyApprovalsReferenceUpdateListener.class).toInstance(referenceUpdateListener);
+        DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
+            .toInstance(referenceUpdateListener);
+      }
+    };
+  }
+
   @Test
   public void multipleProjects() throws Exception {
     Project.NameKey secondProject = projectOperations.newProject().name("secondProject").create();
@@ -222,6 +242,37 @@
   }
 
   @Test
+  public void refUpdateNotified() throws Exception {
+    PushOneCommit.Result change = createChange();
+    gApi.changes().id(change.getChangeId()).current().review(ReviewInput.recommend());
+
+    // this amend is a rework so votes will not be copied.
+    amendChange(change.getChangeId());
+
+    // votes don't exist on the new patch-set for all changes.
+    assertThat(gApi.changes().id(change.getChangeId()).current().votes()).isEmpty();
+
+    // change the project config to make the vote that was not copied to be copied once we do the
+    // schema upgrade.
+    try (ProjectConfigUpdate u = updateProject(allProjects)) {
+      u.getConfig().updateLabelType(LabelId.CODE_REVIEW, b -> b.setCopyAnyScore(true));
+      u.save();
+    }
+
+    ObjectId metaId = change.getChange().notes().getMetaId();
+    recursiveApprovalCopier.persist(project, null);
+
+    ApprovalInfo vote1 =
+        Iterables.getOnlyElement(
+            gApi.changes().id(change.getChangeId()).current().votes().values());
+    assertThat(vote1.value).isEqualTo(1);
+    assertThat(vote1._accountId).isEqualTo(admin.id().get());
+
+    CopyApprovalsReferenceUpdateListener testListener = testListener();
+    assertThat(testListener.refUpdateFor(metaId)).isTrue();
+  }
+
+  @Test
   public void oneCorruptChange_otherChangesProcessed() throws Exception {
     PushOneCommit.Result good = createChange();
     gApi.changes().id(good.getChangeId()).current().review(ReviewInput.recommend());
@@ -254,4 +305,22 @@
     assertThat(vote1.value).isEqualTo(1);
     assertThat(vote1._accountId).isEqualTo(admin.id().get());
   }
+
+  private CopyApprovalsReferenceUpdateListener testListener() {
+    return server.getTestInjector().getInstance(CopyApprovalsReferenceUpdateListener.class);
+  }
+
+  private static class CopyApprovalsReferenceUpdateListener implements GitReferenceUpdatedListener {
+    private final AtomicLongMap<String> countsByOldObjectId = AtomicLongMap.create();
+
+    @Override
+    public void onGitReferenceUpdated(Event event) {
+      String oldObjectId = event.getOldObjectId();
+      countsByOldObjectId.incrementAndGet(oldObjectId);
+    }
+
+    boolean refUpdateFor(ObjectId metaRef) {
+      return countsByOldObjectId.containsKey(metaRef.getName());
+    }
+  }
 }
diff --git a/resources/com/google/gerrit/pgm/init/gerrit.sh b/resources/com/google/gerrit/pgm/init/gerrit.sh
index e7fda5a..257dc61 100755
--- a/resources/com/google/gerrit/pgm/init/gerrit.sh
+++ b/resources/com/google/gerrit/pgm/init/gerrit.sh
@@ -353,7 +353,7 @@
 #####################################################
 # Configure the maximum wait time for shutdown
 #####################################################
-EXTRA_STOP_TIMEOUT=30
+EXTRA_STOP_TIMEOUT=$(get_time_unit_sec "$(get_config --get container.shutdownTimeout || echo 30)")
 HTTPD_STOP_TIMEOUT=$(get_time_unit_sec "$(get_config --get httpd.gracefulStopTimeout || echo 0)")
 SSHD_STOP_TIMEOUT=$(get_time_unit_sec "$(get_config --get sshd.gracefulStopTimeout || echo 0)")