Retry on lock failure during change update

Gerrit did not retry if posting a review failed with a lock failure
thrown by the global refdb implementation. Thus, in case the lock in
the global refdb could not be acquired, the ref-update was aborted
after the first try. This could lead to a lot of failed updates like
reviews, affecting especially users on very active repositories.

An ExceptionHook has been added that will cause Gerrit to retry,
if a GlobalRefDbLockException is being thrown. This ExceptionHook
has to be bound by every plugin that wants to make use of the
Global RefDB.

Bug: Issue 334278785
Change-Id: I3518ba9187761f8475233867ed8a3212713c9079
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/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();
+  }
+}