Merge branch 'stable-3.10'

* stable-3.10:
  Enforce draftComments when their streaming is enabled

Change-Id: I938fe8103bcd4aa37e5cefca375486355640a065
diff --git a/README.md b/README.md
index 81da677..e101a1a 100644
--- a/README.md
+++ b/README.md
@@ -31,11 +31,14 @@
 
 Example:
 
+```
 git clone https://gerrit.googlesource.com/gerrit
 git clone https://gerrit.googlesource.com/modules/global-refdb
 cd gerrit/plugins
 ln -s ../../global-refdb .
-From the Gerrit source tree issue the command bazelsk build plugins/global-refdb
+```
+
+From the Gerrit source tree issue the command `bazelisk build plugins/global-refdb`
 
 Example:
 
@@ -43,9 +46,9 @@
 bazelisk build plugins/global-refdb
 ```
 
-The libModule jar file is created under basel-bin/plugins/global-refdb/global-refdb.jar
+The libModule jar file is created under `bazel-bin/plugins/global-refdb/global-refdb.jar`
 
-To execute the tests run bazelisk test plugins/global-refdb/... from the Gerrit source tree.
+To execute the tests run `bazelisk test plugins/global-refdb/...` from the Gerrit source tree.
 
 Example:
 
diff --git a/bindings.md b/bindings.md
index 920b911..c936c5d 100644
--- a/bindings.md
+++ b/bindings.md
@@ -41,6 +41,7 @@
           .to(CustomSharedRefEnforcementByProject.class)
           .in(Scopes.SINGLETON);
     }
+    DynamicSet.bind(binder(), ExceptionHook.class).to(SharedRefDbExceptionHook.class);
   }
 }
 ```
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/GlobalRefDbLockException.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/GlobalRefDbLockException.java
index fa757db..65b9cf1 100644
--- a/src/main/java/com/gerritforge/gerrit/globalrefdb/GlobalRefDbLockException.java
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/GlobalRefDbLockException.java
@@ -14,11 +14,13 @@
 
 package com.gerritforge.gerrit.globalrefdb;
 
+import com.google.gerrit.git.LockFailureException;
+
 /**
  * {@code GlobalRefDbLockException} is an exception that can be thrown when interacting with the
  * global-refdb to represent the inability to lock or acquire a resource.
  */
-public class GlobalRefDbLockException extends RuntimeException {
+public class GlobalRefDbLockException extends LockFailureException {
   private static final long serialVersionUID = 1L;
 
   /**
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/BatchRefUpdateValidator.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/BatchRefUpdateValidator.java
index 1e45951..cbdb8cd 100644
--- a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/BatchRefUpdateValidator.java
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/BatchRefUpdateValidator.java
@@ -140,22 +140,24 @@
       return;
     }
 
-    List<RefPair> refsToUpdate = getRefPairs(commands).collect(Collectors.toList());
-    List<RefPair> refsFailures =
-        refsToUpdate.stream().filter(RefPair::hasFailed).collect(Collectors.toList());
+    List<RefUpdateSnapshot> refsToUpdate =
+        getRefUpdateSnapshots(commands).collect(Collectors.toList());
+    List<RefUpdateSnapshot> refsFailures =
+        refsToUpdate.stream().filter(RefUpdateSnapshot::hasFailed).collect(Collectors.toList());
     if (!refsFailures.isEmpty()) {
       String allFailuresMessage =
           refsFailures.stream()
-              .map(refPair -> String.format("Failed to fetch ref %s", refPair.compareRef.getName()))
+              .map(refSnapshot -> String.format("Failed to fetch ref %s", refSnapshot.getName()))
               .collect(Collectors.joining(", "));
-      Exception firstFailureException = refsFailures.get(0).exception;
+      Exception firstFailureException = refsFailures.get(0).getException();
 
       logger.atSevere().withCause(firstFailureException).log("%s", allFailuresMessage);
       throw new IOException(allFailuresMessage, firstFailureException);
     }
 
     try (CloseableSet<AutoCloseable> locks = new CloseableSet<>()) {
-      final List<RefPair> finalRefsToUpdate = compareAndGetLatestLocalRefs(refsToUpdate, locks);
+      final List<RefUpdateSnapshot> finalRefsToUpdate =
+          compareAndGetLatestLocalRefs(refsToUpdate, locks);
       delegateUpdate.invoke();
       boolean sharedDbUpdateSucceeded = false;
       try {
@@ -184,7 +186,7 @@
 
   private void rollback(
       OneParameterVoidFunction<List<ReceiveCommand>> delegateUpdateRollback,
-      List<RefPair> refsBeforeUpdate,
+      List<RefUpdateSnapshot> refsBeforeUpdate,
       List<ReceiveCommand> receiveCommands)
       throws IOException {
     List<ReceiveCommand> rollbackCommands =
@@ -192,61 +194,62 @@
             .map(
                 refBeforeUpdate ->
                     new ReceiveCommand(
-                        refBeforeUpdate.putValue,
-                        refBeforeUpdate.compareRef.getObjectId(),
+                        refBeforeUpdate.getNewValue(),
+                        refBeforeUpdate.getOldValue(),
                         refBeforeUpdate.getName()))
             .collect(Collectors.toList());
     delegateUpdateRollback.invoke(rollbackCommands);
     receiveCommands.forEach(command -> command.setResult(ReceiveCommand.Result.LOCK_FAILURE));
   }
 
-  private void updateSharedRefDb(Stream<ReceiveCommand> commandStream, List<RefPair> refsToUpdate)
+  private void updateSharedRefDb(
+      Stream<ReceiveCommand> commandStream, List<RefUpdateSnapshot> refsToUpdate)
       throws IOException {
     if (commandStream.anyMatch(cmd -> cmd.getResult() != ReceiveCommand.Result.OK)) {
       return;
     }
 
-    for (RefPair refPair : refsToUpdate) {
-      updateSharedDbOrThrowExceptionFor(refPair);
+    for (RefUpdateSnapshot refUpdateSnapshot : refsToUpdate) {
+      updateSharedDbOrThrowExceptionFor(refUpdateSnapshot);
     }
   }
 
-  private Stream<RefPair> getRefPairs(List<ReceiveCommand> receivedCommands) {
-    return receivedCommands.stream().map(this::getRefPairForCommand);
+  private Stream<RefUpdateSnapshot> getRefUpdateSnapshots(List<ReceiveCommand> receivedCommands) {
+    return receivedCommands.stream().map(this::getRefUpdateSnapshotForCommand);
   }
 
-  private RefPair getRefPairForCommand(ReceiveCommand command) {
+  private RefUpdateSnapshot getRefUpdateSnapshotForCommand(ReceiveCommand command) {
     try {
       switch (command.getType()) {
         case CREATE:
-          return new RefPair(nullRef(command.getRefName()), getNewRef(command));
+          return new RefUpdateSnapshot(nullRef(command.getRefName()), getNewValue(command));
 
         case UPDATE:
         case UPDATE_NONFASTFORWARD:
-          return new RefPair(getCurrentRef(command.getRefName()), getNewRef(command));
+          return new RefUpdateSnapshot(getCurrentRef(command.getRefName()), getNewValue(command));
 
         case DELETE:
-          return new RefPair(getCurrentRef(command.getRefName()), ObjectId.zeroId());
+          return new RefUpdateSnapshot(getCurrentRef(command.getRefName()), ObjectId.zeroId());
 
         default:
-          return new RefPair(
+          return new RefUpdateSnapshot(
               command.getRef(),
               new IllegalArgumentException("Unsupported command type " + command.getType()));
       }
     } catch (IOException e) {
-      return new RefPair(command.getRef(), e);
+      return new RefUpdateSnapshot(command.getRef(), e);
     }
   }
 
-  private ObjectId getNewRef(ReceiveCommand command) {
+  private ObjectId getNewValue(ReceiveCommand command) {
     return command.getNewId();
   }
 
-  private List<RefPair> compareAndGetLatestLocalRefs(
-      List<RefPair> refsToUpdate, CloseableSet<AutoCloseable> locks) throws IOException {
-    List<RefPair> latestRefsToUpdate = new ArrayList<>();
-    for (RefPair refPair : refsToUpdate) {
-      latestRefsToUpdate.add(compareAndGetLatestLocalRef(refPair, locks));
+  private List<RefUpdateSnapshot> compareAndGetLatestLocalRefs(
+      List<RefUpdateSnapshot> refsToUpdate, CloseableSet<AutoCloseable> locks) throws IOException {
+    List<RefUpdateSnapshot> latestRefsToUpdate = new ArrayList<>();
+    for (RefUpdateSnapshot refUpdateSnapshot : refsToUpdate) {
+      latestRefsToUpdate.add(compareAndGetLatestLocalRef(refUpdateSnapshot, locks));
     }
     return latestRefsToUpdate;
   }
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefPair.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefPair.java
deleted file mode 100644
index 4211c0c..0000000
--- a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefPair.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation;
-
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-
-/**
- * A convenience object encompassing the old (current) and the new (candidate) value of a {@link
- * Ref}. This is used to snapshot the current status of a ref update so that validations against the
- * global refdb are unaffected by changes on the {@link org.eclipse.jgit.lib.RefDatabase}.
- */
-public class RefPair {
-  public final Ref compareRef;
-  public final ObjectId putValue;
-  public final Exception exception;
-
-  /**
-   * Constructs a {@code RefPair} with the provided old and new ref values. The oldRef value is
-   * required not to be null, in which case an {@link IllegalArgumentException} is thrown.
-   *
-   * @param oldRef the old ref
-   * @param newRefValue the new (candidate) value for this ref.
-   */
-  RefPair(Ref oldRef, ObjectId newRefValue) {
-    if (oldRef == null) {
-      throw new IllegalArgumentException("Required not-null ref in RefPair");
-    }
-    this.compareRef = oldRef;
-    this.putValue = newRefValue;
-    this.exception = null;
-  }
-
-  /**
-   * Constructs a {@code RefPair} with the current ref and an Exception indicating why the new ref
-   * value failed being retrieved.
-   *
-   * @param newRef
-   * @param e
-   */
-  RefPair(Ref newRef, Exception e) {
-    this.compareRef = newRef;
-    this.exception = e;
-    this.putValue = ObjectId.zeroId();
-  }
-
-  /**
-   * Getter for the current ref
-   *
-   * @return the current ref value
-   */
-  public String getName() {
-    return compareRef.getName();
-  }
-
-  /**
-   * Whether the new value failed being retrieved
-   *
-   * @return true when this refPair has failed, false otherwise.
-   */
-  public boolean hasFailed() {
-    return exception != null;
-  }
-}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateSnapshot.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateSnapshot.java
new file mode 100644
index 0000000..2e83be3
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateSnapshot.java
@@ -0,0 +1,114 @@
+// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+
+/**
+ * A convenience object encompassing the old (current) and the new (candidate) value of a {@link
+ * Ref}. This is used to snapshot the current status of a ref update so that validations against the
+ * global refdb are unaffected by changes on the underlying {@link
+ * org.eclipse.jgit.lib.RefDatabase}.
+ */
+class RefUpdateSnapshot {
+  private final Ref ref;
+  private final ObjectId newValue;
+  private final Exception exception;
+
+  /**
+   * Constructs a {@code RefUpdateSnapshot} with the provided old and new values. The old value of
+   * this Ref must not be null, otherwise an {@link IllegalArgumentException} is thrown.
+   *
+   * @param ref the ref with its old value
+   * @param newRefValue the new (candidate) value for this ref.
+   */
+  RefUpdateSnapshot(Ref ref, ObjectId newRefValue) {
+    if (ref == null) {
+      throw new IllegalArgumentException("RefUpdateSnapshot cannot be created for null Ref");
+    }
+    this.ref = ref;
+    this.newValue = newRefValue;
+    this.exception = null;
+  }
+
+  /**
+   * Constructs a {@code RefUpdateSnapshot} with the current ref and an Exception indicating why the
+   * ref's new value could not be retrieved.
+   *
+   * @param ref the ref with its old value
+   * @param e exception caught when trying to retrieve the ref's new value
+   */
+  RefUpdateSnapshot(Ref ref, Exception e) {
+    this.ref = ref;
+    this.newValue = ObjectId.zeroId();
+    this.exception = e;
+  }
+
+  /**
+   * Get the ref name
+   *
+   * @return the ref name
+   */
+  public String getName() {
+    return ref.getName();
+  }
+
+  /**
+   * Get the ref's old value
+   *
+   * @return the ref's old value
+   */
+  public ObjectId getOldValue() {
+    return ref.getObjectId();
+  }
+
+  /**
+   * Get the ref's new (candidate) value
+   *
+   * @return the ref's new (candidate) value
+   */
+  public ObjectId getNewValue() {
+    return newValue;
+  }
+
+  /**
+   * Get the snapshotted ref with its old value
+   *
+   * @return the snapshotted ref with its old value
+   */
+  public Ref getRef() {
+    return ref;
+  }
+
+  /**
+   * Get the exception which occurred when retrieving the ref's new value
+   *
+   * @return the exception which occurred when retrieving the ref's new value
+   */
+  public Exception getException() {
+    return exception;
+  }
+
+  /**
+   * Whether retrieving the new (candidate) value failed
+   *
+   * @return {@code true} when retrieving the ref's new (candidate) value failed, {@code false}
+   *     otherwise.
+   */
+  public boolean hasFailed() {
+    return exception != null;
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidator.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidator.java
index d955599..31d3af1 100644
--- a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidator.java
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidator.java
@@ -14,12 +14,12 @@
 
 package com.gerritforge.gerrit.globalrefdb.validation;
 
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDbLockException;
 import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError;
 import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.CustomSharedRefEnforcementByProject;
 import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.DefaultSharedRefEnforcement;
 import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.OutOfSyncException;
 import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedDbSplitBrainException;
-import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedLockException;
 import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement;
 import com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb.SharedRefEnforcement.EnforcePolicy;
 import com.google.common.base.MoreObjects;
@@ -186,16 +186,16 @@
       OneParameterFunction<ObjectId, Result> rollbackFunction)
       throws IOException {
     try (CloseableSet<AutoCloseable> locks = new CloseableSet<>()) {
-      RefPair refPairForUpdate = newRefPairFrom(refUpdate);
-      compareAndGetLatestLocalRef(refPairForUpdate, locks);
+      RefUpdateSnapshot refUpdateSnapshot = newSnapshot(refUpdate);
+      compareAndGetLatestLocalRef(refUpdateSnapshot, locks);
       RefUpdate.Result result = refUpdateFunction.invoke();
       if (!isSuccessful(result)) {
         return result;
       }
       try {
-        updateSharedDbOrThrowExceptionFor(refPairForUpdate);
+        updateSharedDbOrThrowExceptionFor(refUpdateSnapshot);
       } catch (Exception e) {
-        result = rollbackFunction.invoke(refPairForUpdate.compareRef.getObjectId());
+        result = rollbackFunction.invoke(refUpdateSnapshot.getOldValue());
         if (isSuccessful(result)) {
           result = RefUpdate.Result.LOCK_FAILURE;
         }
@@ -207,26 +207,26 @@
     } catch (OutOfSyncException e) {
       logger.atWarning().withCause(e).log(
           "Local node is out of sync with ref-db: %s", e.getMessage());
-
       return RefUpdate.Result.LOCK_FAILURE;
     }
   }
 
-  protected void updateSharedDbOrThrowExceptionFor(RefPair refPair) throws IOException {
+  protected void updateSharedDbOrThrowExceptionFor(RefUpdateSnapshot refSnapshot)
+      throws IOException {
     // We are not checking refs that should be ignored
     final EnforcePolicy refEnforcementPolicy =
-        refEnforcement.getPolicy(projectName, refPair.getName());
+        refEnforcement.getPolicy(projectName, refSnapshot.getName());
     if (refEnforcementPolicy == EnforcePolicy.IGNORED) return;
 
     boolean succeeded;
     try {
       succeeded =
           sharedRefDb.compareAndPut(
-              Project.nameKey(projectName), refPair.compareRef, refPair.putValue);
+              Project.nameKey(projectName), refSnapshot.getRef(), refSnapshot.getNewValue());
     } catch (GlobalRefDbSystemError e) {
       logger.atWarning().withCause(e).log(
           "Not able to persist the data in global-refdb for project '%s', ref '%s' and value %s, message: %s",
-          projectName, refPair.getName(), refPair.putValue, e.getMessage());
+          projectName, refSnapshot.getName(), refSnapshot.getNewValue(), e.getMessage());
       throw e;
     }
 
@@ -236,17 +236,18 @@
               "Not able to persist the data in SharedRef for project '%s' and ref '%s',"
                   + "the cluster is now in Split Brain since the commit has been "
                   + "persisted locally but not in global-refdb the value %s",
-              projectName, refPair.getName(), refPair.putValue);
+              projectName, refSnapshot.getName(), refSnapshot.getNewValue());
       throw new SharedDbSplitBrainException(errorMessage);
     }
   }
 
-  protected RefPair compareAndGetLatestLocalRef(RefPair refPair, CloseableSet<AutoCloseable> locks)
-      throws SharedLockException, OutOfSyncException, IOException {
-    String refName = refPair.getName();
+  protected RefUpdateSnapshot compareAndGetLatestLocalRef(
+      RefUpdateSnapshot refUpdateSnapshot, CloseableSet<AutoCloseable> locks)
+      throws GlobalRefDbLockException, OutOfSyncException, IOException {
+    String refName = refUpdateSnapshot.getName();
     EnforcePolicy refEnforcementPolicy = refEnforcement.getPolicy(projectName, refName);
     if (refEnforcementPolicy == EnforcePolicy.IGNORED) {
-      return refPair;
+      return refUpdateSnapshot;
     }
 
     locks.addResourceIfNotExist(
@@ -255,30 +256,33 @@
             lockWrapperFactory.create(
                 projectName, refName, sharedRefDb.lockRef(Project.nameKey(projectName), refName)));
 
-    RefPair latestRefPair = getLatestLocalRef(refPair);
-    if (sharedRefDb.isUpToDate(Project.nameKey(projectName), latestRefPair.compareRef)) {
-      return latestRefPair;
+    RefUpdateSnapshot latestRefUpdateSnapshot = getLatestLocalRef(refUpdateSnapshot);
+    if (sharedRefDb.isUpToDate(Project.nameKey(projectName), latestRefUpdateSnapshot.getRef())) {
+      return latestRefUpdateSnapshot;
     }
 
-    if (isNullRef(latestRefPair.compareRef)
+    if (isNullRef(latestRefUpdateSnapshot.getRef())
         || sharedRefDb.exists(Project.nameKey(projectName), refName)) {
       validationMetrics.incrementSplitBrainPrevention();
 
       softFailBasedOnEnforcement(
-          new OutOfSyncException(projectName, latestRefPair.compareRef), refEnforcementPolicy);
+          new OutOfSyncException(projectName, latestRefUpdateSnapshot.getRef()),
+          refEnforcementPolicy);
     }
 
-    return latestRefPair;
+    return latestRefUpdateSnapshot;
   }
 
   private boolean isNullRef(Ref ref) {
     return ref.getObjectId().equals(ObjectId.zeroId());
   }
 
-  private RefPair getLatestLocalRef(RefPair refPair) throws IOException {
-    Ref latestRef = refDb.exactRef(refPair.getName());
-    return new RefPair(
-        latestRef == null ? nullRef(refPair.getName()) : latestRef, refPair.putValue);
+  private RefUpdateSnapshot getLatestLocalRef(RefUpdateSnapshot refUpdateSnapshot)
+      throws IOException {
+    Ref latestRef = refDb.exactRef(refUpdateSnapshot.getName());
+    return new RefUpdateSnapshot(
+        latestRef == null ? nullRef(refUpdateSnapshot.getName()) : latestRef,
+        refUpdateSnapshot.getNewValue());
   }
 
   private Ref nullRef(String name) {
@@ -306,8 +310,8 @@
     }
   }
 
-  protected RefPair newRefPairFrom(RefUpdate refUpdate) throws IOException {
-    return new RefPair(getCurrentRef(refUpdate.getName()), refUpdate.getNewObjectId());
+  protected RefUpdateSnapshot newSnapshot(RefUpdate refUpdate) throws IOException {
+    return new RefUpdateSnapshot(getCurrentRef(refUpdate.getName()), refUpdate.getNewObjectId());
   }
 
   protected Ref getCurrentRef(String refName) throws IOException {
@@ -326,8 +330,8 @@
     }
 
     public void addResourceIfNotExist(
-        String key, ExceptionThrowingSupplier<T, SharedLockException> resourceFactory)
-        throws SharedLockException {
+        String key, ExceptionThrowingSupplier<T, GlobalRefDbLockException> resourceFactory)
+        throws GlobalRefDbLockException {
       if (!elements.containsKey(key)) {
         elements.put(key, resourceFactory.create());
       }
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbExceptionHook.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbExceptionHook.java
new file mode 100644
index 0000000..a7f2740
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDbExceptionHook.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2024 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.gerritforge.gerrit.globalrefdb.validation;
+
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDbLockException;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.ExceptionHook;
+import java.util.Optional;
+
+public class SharedRefDbExceptionHook implements ExceptionHook {
+
+  @Override
+  public boolean shouldRetry(String actionType, String actionName, Throwable throwable) {
+    return throwable instanceof GlobalRefDbLockException;
+  }
+
+  @Override
+  public Optional<Status> getStatus(Throwable throwable) {
+    if (throwable instanceof GlobalRefDbLockException) {
+      return Optional.of(Status.create(503, "Lock failure"));
+    }
+    return Optional.empty();
+  }
+
+  @Override
+  public ImmutableList<String> getUserMessages(Throwable throwable, @Nullable String traceId) {
+    if (throwable instanceof GlobalRefDbLockException) {
+      ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
+      builder.add(throwable.getMessage());
+      if (traceId != null && !traceId.isBlank()) {
+        builder.add(String.format("Trace ID: %s", traceId));
+      }
+      return builder.build();
+    }
+    return ImmutableList.of();
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedLockException.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedLockException.java
deleted file mode 100644
index 5963da2..0000000
--- a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/SharedLockException.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (C) 2020 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.gerritforge.gerrit.globalrefdb.validation.dfsrefdb;
-
-import java.io.IOException;
-
-/** Unable to lock a project/ref resource. */
-public class SharedLockException extends IOException {
-  private static final long serialVersionUID = 1L;
-
-  /**
-   * Constructs a {@code SharedLockException} exception with the cause of failing to lock a
-   * project/ref resource
-   *
-   * @param project the project the lock is being acquired for
-   * @param refName the ref the project is being acquired for
-   * @param cause the cause of the failure
-   */
-  public SharedLockException(String project, String refName, Exception cause) {
-    super(String.format("Unable to lock project %s on ref %s", project, refName), cause);
-  }
-}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/GlobalRefDatabaseTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/GlobalRefDatabaseTest.java
index 8ed1f4c..6284f42 100644
--- a/src/test/java/com/gerritforge/gerrit/globalrefdb/GlobalRefDatabaseTest.java
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/GlobalRefDatabaseTest.java
@@ -95,26 +95,28 @@
   }
 
   @Test
-  public void shouldReturnIsUpToDateWhenProjectDoesNotExistsInTheGlobalRefDB() {
+  public void shouldReturnIsUpToDateWhenProjectDoesNotExistsInTheGlobalRefDB()
+      throws GlobalRefDbLockException {
     assertThat(objectUnderTest.isUpToDate(project, initialRef)).isTrue();
   }
 
   @Test
-  public void shouldReturnIsUpToDate() {
+  public void shouldReturnIsUpToDate() throws GlobalRefDbLockException {
     objectUnderTest.compareAndPut(project, nullRef, objectId1);
 
     assertThat(objectUnderTest.isUpToDate(project, ref1)).isTrue();
   }
 
   @Test
-  public void shouldReturnIsNotUpToDateWhenLocalRepoIsOutdated() {
+  public void shouldReturnIsNotUpToDateWhenLocalRepoIsOutdated() throws GlobalRefDbLockException {
     objectUnderTest.compareAndPut(project, nullRef, objectId1);
 
     assertThat(objectUnderTest.isUpToDate(project, nullRef)).isFalse();
   }
 
   @Test
-  public void shouldReturnIsNotUpToDateWhenLocalRepoIsAheadOfTheGlobalRefDB() {
+  public void shouldReturnIsNotUpToDateWhenLocalRepoIsAheadOfTheGlobalRefDB()
+      throws GlobalRefDbLockException {
     objectUnderTest.compareAndPut(project, nullRef, objectId1);
 
     assertThat(objectUnderTest.isUpToDate(project, ref2)).isFalse();
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapperTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapperTest.java
index 884090e..5240f2c 100644
--- a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapperTest.java
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapperTest.java
@@ -17,6 +17,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDbLockException;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.metrics.Timer0.Context;
 import org.eclipse.jgit.lib.ObjectId;
@@ -58,14 +59,16 @@
   }
 
   @Test
-  public void shouldUpdateLockRefExecutionTimeMetricWhenLockRefIsCalled() {
+  public void shouldUpdateLockRefExecutionTimeMetricWhenLockRefIsCalled()
+      throws GlobalRefDbLockException {
     objectUnderTest.lockRef(projectName, refName);
     verify(metrics).startLockRefExecutionTime();
     verify(context).close();
   }
 
   @Test
-  public void shouldUpdateIsUpToDateExecutionTimeMetricWhenIsUpToDate() {
+  public void shouldUpdateIsUpToDateExecutionTimeMetricWhenIsUpToDate()
+      throws GlobalRefDbLockException {
     objectUnderTest.isUpToDate(projectName, ref);
     verify(metrics).startIsUpToDateExecutionTime();
     verify(context).close();
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabaseTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabaseTest.java
index 8c749ea..eaeb6d4 100644
--- a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabaseTest.java
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabaseTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.gerritforge.gerrit.globalrefdb.GlobalRefDbLockException;
 import org.eclipse.jgit.lib.Ref;
 import org.junit.Test;
 
@@ -25,7 +26,7 @@
   private NoopSharedRefDatabase objectUnderTest = new NoopSharedRefDatabase();
 
   @Test
-  public void isUpToDateShouldAlwaysReturnTrue() {
+  public void isUpToDateShouldAlwaysReturnTrue() throws GlobalRefDbLockException {
     assertThat(objectUnderTest.isUpToDate(A_TEST_PROJECT_NAME_KEY, sampleRef)).isTrue();
   }