Merge branch 'stable-3.5' into stable-3.6

* stable-3.5:
  Bump version to v3.5.4.2
  Add global-refdb operations latency metrics

Change-Id: I19bfa21e21a2bfd532163749047ffd3432ed921b
diff --git a/README.md b/README.md
index 582bd2c..8249d8c 100644
--- a/README.md
+++ b/README.md
@@ -14,4 +14,9 @@
 ## Bindings
 
 In order to consume this library, some Guice bindings need to be registered
-appropriately. More information in the relevant [documentation](./bindings.md).
\ No newline at end of file
+appropriately. More information in the relevant [documentation](./bindings.md).
+
+## Metrics
+
+Global ref-database expose metrics to measure the global ref-database operation latency.
+List of the available metrics can be found [here](./metrics.md).
\ No newline at end of file
diff --git a/metrics.md b/metrics.md
new file mode 100644
index 0000000..4e53f30
--- /dev/null
+++ b/metrics.md
@@ -0,0 +1,20 @@
+Metrics
+=============
+
+* global_refdb/compare_and_put_latency
+  : the latency in milliseconds of the compareAndPut operation.
+
+* global_refdb/get_latency
+  : the latency in milliseconds of the get operation.
+
+* global_refdb/lock_ref_latency
+  : the latency in milliseconds of the lock ref operation.
+
+* global_refdb/exists_latency
+  : the latency in milliseconds of the exists operation.
+
+* global_refdb/is_up_to_date_latency
+  : the latency in milliseconds of the isUpToDate operation.
+
+* global_refdb/remove_latency
+  : the latency in milliseconds of the remove operation.
\ No newline at end of file
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDBMetrics.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDBMetrics.java
new file mode 100644
index 0000000..ff483ab
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDBMetrics.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2023 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.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.metrics.Timer0.Context;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class SharedRefDBMetrics {
+
+  private final Timer0 lockRefExecutionTime;
+  private final Timer0 getOperationExecutionTime;
+  private final Timer0 existsExecutionTime;
+  private Timer0 compareAndPutExecutionTime;
+  private Timer0 removeExecutionTime;
+  private Timer0 isUpToDateExecutionTime;
+
+  @Inject
+  public SharedRefDBMetrics(MetricMaker metricMaker) {
+    compareAndPutExecutionTime =
+        metricMaker.newTimer(
+            "global_refdb/compare_and_put_latency",
+            new Description("Time spent on compareAndPut.")
+                .setCumulative()
+                .setUnit(Description.Units.MILLISECONDS));
+    getOperationExecutionTime =
+        metricMaker.newTimer(
+            "global_refdb/get_latency",
+            new Description("Time spent on get operation.")
+                .setCumulative()
+                .setUnit(Description.Units.MILLISECONDS));
+
+    lockRefExecutionTime =
+        metricMaker.newTimer(
+            "global_refdb/lock_ref_latency",
+            new Description("Time spent on locking ref.")
+                .setCumulative()
+                .setUnit(Description.Units.MILLISECONDS));
+    existsExecutionTime =
+        metricMaker.newTimer(
+            "global_refdb/exists_latency",
+            new Description("Time spent on verifying if the global-refdb contains a value.")
+                .setCumulative()
+                .setUnit(Description.Units.MILLISECONDS));
+    removeExecutionTime =
+        metricMaker.newTimer(
+            "global_refdb/remove_latency",
+            new Description("Time spent on cleaning up the path from global-ref db.")
+                .setCumulative()
+                .setUnit(Description.Units.MILLISECONDS));
+    isUpToDateExecutionTime =
+        metricMaker.newTimer(
+            "global_refdb/is_up_to_date_latency",
+            new Description("Time spent on checking in global ref-db if ref is up-to-date.")
+                .setCumulative()
+                .setUnit(Description.Units.MILLISECONDS));
+  }
+
+  public Context startCompareAndPutExecutionTime() {
+    return compareAndPutExecutionTime.start();
+  }
+
+  public Context startGetExecutionTime() {
+    return getOperationExecutionTime.start();
+  }
+
+  public Context startLockRefExecutionTime() {
+    return lockRefExecutionTime.start();
+  }
+
+  public Context startExistsExecutionTime() {
+    return existsExecutionTime.start();
+  }
+
+  public Context startRemoveExecutionTime() {
+    return removeExecutionTime.start();
+  }
+
+  public Context startIsUpToDateExecutionTime() {
+    return isUpToDateExecutionTime.start();
+  }
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapper.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapper.java
index 4b5aeec..4349193 100644
--- a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapper.java
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapper.java
@@ -21,6 +21,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.metrics.Timer0.Context;
 import com.google.inject.Inject;
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
@@ -38,6 +39,7 @@
   private DynamicItem<GlobalRefDatabase> sharedRefDbDynamicItem;
 
   private final SharedRefLogger sharedRefLogger;
+  private final SharedRefDBMetrics metrics;
 
   /**
    * Constructs a {@code SharedRefDatabaseWrapper} wrapping an optional {@link GlobalRefDatabase},
@@ -46,69 +48,86 @@
    * @param sharedRefLogger logger of shared ref-db operations.
    */
   @Inject
-  public SharedRefDatabaseWrapper(SharedRefLogger sharedRefLogger) {
+  public SharedRefDatabaseWrapper(SharedRefLogger sharedRefLogger, SharedRefDBMetrics metrics) {
     this.sharedRefLogger = sharedRefLogger;
+    this.metrics = metrics;
   }
 
   @VisibleForTesting
   public SharedRefDatabaseWrapper(
-      DynamicItem<GlobalRefDatabase> sharedRefDbDynamicItem, SharedRefLogger sharedRefLogger) {
-    this.sharedRefLogger = sharedRefLogger;
+      DynamicItem<GlobalRefDatabase> sharedRefDbDynamicItem,
+      SharedRefLogger sharedRefLogger,
+      SharedRefDBMetrics metrics) {
+    this(sharedRefLogger, metrics);
     this.sharedRefDbDynamicItem = sharedRefDbDynamicItem;
   }
 
   @Override
   public boolean isUpToDate(Project.NameKey project, Ref ref) throws GlobalRefDbLockException {
-    return sharedRefDb().isUpToDate(project, ref);
+    try (Context context = metrics.startIsUpToDateExecutionTime()) {
+      return sharedRefDb().isUpToDate(project, ref);
+    }
   }
 
   /** {@inheritDoc}. The operation is logged upon success. */
   @Override
   public boolean compareAndPut(Project.NameKey project, Ref currRef, ObjectId newRefValue)
       throws GlobalRefDbSystemError {
-    boolean succeeded = sharedRefDb().compareAndPut(project, currRef, newRefValue);
-    if (succeeded) {
-      sharedRefLogger.logRefUpdate(project.get(), currRef, newRefValue);
+    try (Context context = metrics.startCompareAndPutExecutionTime()) {
+      boolean succeeded = sharedRefDb().compareAndPut(project, currRef, newRefValue);
+      if (succeeded) {
+        sharedRefLogger.logRefUpdate(project.get(), currRef, newRefValue);
+      }
+      return succeeded;
     }
-    return succeeded;
   }
 
   /** {@inheritDoc} the operation is logged upon success. */
   @Override
   public <T> boolean compareAndPut(Project.NameKey project, String refName, T currValue, T newValue)
       throws GlobalRefDbSystemError {
-    boolean succeeded = sharedRefDb().compareAndPut(project, refName, currValue, newValue);
-    if (succeeded) {
-      sharedRefLogger.logRefUpdate(project.get(), refName, currValue, newValue);
+    try (Context context = metrics.startCompareAndPutExecutionTime()) {
+      boolean succeeded = sharedRefDb().compareAndPut(project, refName, currValue, newValue);
+      if (succeeded) {
+        sharedRefLogger.logRefUpdate(project.get(), refName, currValue, newValue);
+      }
+      return succeeded;
     }
-    return succeeded;
   }
 
   /** {@inheritDoc}. The operation is logged. */
   @Override
   public AutoCloseable lockRef(Project.NameKey project, String refName)
       throws GlobalRefDbLockException {
-    AutoCloseable locker = sharedRefDb().lockRef(project, refName);
-    sharedRefLogger.logLockAcquisition(project.get(), refName);
-    return locker;
+    try (Context context = metrics.startLockRefExecutionTime()) {
+      AutoCloseable locker = sharedRefDb().lockRef(project, refName);
+      sharedRefLogger.logLockAcquisition(project.get(), refName);
+      return locker;
+    }
   }
 
   @Override
   public boolean exists(Project.NameKey project, String refName) {
-    return sharedRefDb().exists(project, refName);
+    try (Context context = metrics.startExistsExecutionTime()) {
+      return sharedRefDb().exists(project, refName);
+    }
   }
 
   /** {@inheritDoc}. The operation is logged. */
   @Override
   public void remove(Project.NameKey project) throws GlobalRefDbSystemError {
-    sharedRefDb().remove(project);
-    sharedRefLogger.logProjectDelete(project.get());
+    try (Context context = metrics.startRemoveExecutionTime()) {
+      sharedRefDb().remove(project);
+      sharedRefLogger.logProjectDelete(project.get());
+    }
   }
 
   @Override
   public <T> Optional<T> get(Project.NameKey nameKey, String s, Class<T> clazz)
       throws GlobalRefDbSystemError {
-    return sharedRefDb().get(nameKey, s, clazz);
+    try (Context context = metrics.startGetExecutionTime()) {
+      return sharedRefDb().get(nameKey, s, clazz);
+    }
   }
 
   private GlobalRefDatabase sharedRefDb() {
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidatorTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidatorTest.java
index f937286..3c63d13 100644
--- a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidatorTest.java
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/RefUpdateValidatorTest.java
@@ -50,6 +50,8 @@
 
   @Mock SharedRefLogger sharedRefLogger;
 
+  @Mock SharedRefDBMetrics sharedRefDBMetrics;
+
   @Mock RefDatabase localRefDb;
 
   @Mock ValidationMetrics validationMetrics;
@@ -90,7 +92,8 @@
 
   @Test
   public void validationShouldSucceedWhenSharedRefDbIsNoop() throws Exception {
-    SharedRefDatabaseWrapper noopSharedRefDbWrapper = new SharedRefDatabaseWrapper(sharedRefLogger);
+    SharedRefDatabaseWrapper noopSharedRefDbWrapper =
+        new SharedRefDatabaseWrapper(sharedRefLogger, sharedRefDBMetrics);
 
     Result result =
         newRefUpdateValidator(noopSharedRefDbWrapper)
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapperTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapperTest.java
new file mode 100644
index 0000000..884090e
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapperTest.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2023 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 static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.metrics.Timer0.Context;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SharedRefDatabaseWrapperTest {
+
+  @Mock private SharedRefDBMetrics metrics;
+  @Mock SharedRefLogger sharedRefLogger;
+  @Mock private Context context;
+  @Mock private Ref ref;
+
+  private SharedRefDatabaseWrapper objectUnderTest;
+  private String refName = "refs/heads/master";
+  private Project.NameKey projectName = Project.nameKey("test_project");
+
+  @Before
+  public void setup() {
+    when(metrics.startCompareAndPutExecutionTime()).thenReturn(context);
+    when(metrics.startLockRefExecutionTime()).thenReturn(context);
+    when(metrics.startGetExecutionTime()).thenReturn(context);
+    when(metrics.startExistsExecutionTime()).thenReturn(context);
+    when(metrics.startIsUpToDateExecutionTime()).thenReturn(context);
+    when(metrics.startRemoveExecutionTime()).thenReturn(context);
+    objectUnderTest = new SharedRefDatabaseWrapper(sharedRefLogger, metrics);
+  }
+
+  @Test
+  public void shouldUpdateCompareAndPutExecutionTimeMetricWhenCompareAndPut() {
+    objectUnderTest.compareAndPut(projectName, refName, ObjectId.zeroId(), ObjectId.zeroId());
+    verify(metrics).startCompareAndPutExecutionTime();
+    verify(context).close();
+  }
+
+  @Test
+  public void shouldUpdateLockRefExecutionTimeMetricWhenLockRefIsCalled() {
+    objectUnderTest.lockRef(projectName, refName);
+    verify(metrics).startLockRefExecutionTime();
+    verify(context).close();
+  }
+
+  @Test
+  public void shouldUpdateIsUpToDateExecutionTimeMetricWhenIsUpToDate() {
+    objectUnderTest.isUpToDate(projectName, ref);
+    verify(metrics).startIsUpToDateExecutionTime();
+    verify(context).close();
+  }
+
+  @Test
+  public void shouldUpdateExistsExecutionTimeMetricWhenExistsIsCalled() {
+    objectUnderTest.exists(projectName, refName);
+    verify(metrics).startExistsExecutionTime();
+    verify(context).close();
+  }
+
+  @Test
+  public void shouldUpdateGetExecutionTimeMetricWhenGetIsCalled() {
+    objectUnderTest.get(projectName, refName, String.class);
+    verify(metrics).startGetExecutionTime();
+    verify(context).close();
+  }
+
+  @Test
+  public void shouldUpdateRemoveExecutionTimeMetricWhenRemoveCalled() {
+    objectUnderTest.remove(projectName);
+    verify(metrics).startRemoveExecutionTime();
+    verify(context).close();
+  }
+}