Add put method to create/update global-refdb entry

This change extends the ExtendedBrokerApi interface with the
capability to put value in global-refdb. Opposite to compareAndPut
this method is not checking current global-refdb value.

Bug: Issue 297440085
Change-Id: Iba779d984e7888aeb9985e918241ebab4fca4c93
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/ExtendedGlobalRefDatabase.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/ExtendedGlobalRefDatabase.java
new file mode 100644
index 0000000..c6f8de7
--- /dev/null
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/ExtendedGlobalRefDatabase.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 GerritForge Ltd
+//
+// 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;
+
+import com.google.gerrit.entities.Project;
+
+public interface ExtendedGlobalRefDatabase extends GlobalRefDatabase {
+
+  /**
+   * Set a value of generic type T.
+   *
+   * <p>Set is executed as an atomic operation.
+   *
+   * @param project project name of the ref.
+   * @param refName to store the value for.
+   * @param newValue new value to store.
+   * @param <T> Type of the current and new value
+   * @throws GlobalRefDbSystemError the reference cannot be set due to a system error.
+   */
+  <T> void put(Project.NameKey project, String refName, T newValue) throws GlobalRefDbSystemError;
+}
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/Log4jSharedRefLogger.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/Log4jSharedRefLogger.java
index 3b4dbb2..04ffeed 100644
--- a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/Log4jSharedRefLogger.java
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/Log4jSharedRefLogger.java
@@ -140,6 +140,18 @@
     }
   }
 
+  @Override
+  public <T> void logRefUpdate(String project, String refName, T newRefValue) {
+    if (newRefValue != null) {
+      sharedRefDBLog.info(
+          gson.toJson(
+              new SharedRefLogEntry.UpdateRef(
+                  project, refName, null, safeToString(newRefValue), null, null)));
+    } else {
+      sharedRefDBLog.info(gson.toJson(new SharedRefLogEntry.DeleteRef(project, refName, null)));
+    }
+  }
+
   /**
    * {@inheritDoc}.
    *
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDBMetrics.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDBMetrics.java
index ff483ab..c907831 100644
--- a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDBMetrics.java
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDBMetrics.java
@@ -28,6 +28,7 @@
   private final Timer0 getOperationExecutionTime;
   private final Timer0 existsExecutionTime;
   private Timer0 compareAndPutExecutionTime;
+  private Timer0 setExecutionTime;
   private Timer0 removeExecutionTime;
   private Timer0 isUpToDateExecutionTime;
 
@@ -39,6 +40,12 @@
             new Description("Time spent on compareAndPut.")
                 .setCumulative()
                 .setUnit(Description.Units.MILLISECONDS));
+    setExecutionTime =
+        metricMaker.newTimer(
+            "global_refdb/set_latency",
+            new Description("Time spent on set.")
+                .setCumulative()
+                .setUnit(Description.Units.MILLISECONDS));
     getOperationExecutionTime =
         metricMaker.newTimer(
             "global_refdb/get_latency",
@@ -76,6 +83,10 @@
     return compareAndPutExecutionTime.start();
   }
 
+  public Context startSetExecutionTime() {
+    return setExecutionTime.start();
+  }
+
   public Context startGetExecutionTime() {
     return getOperationExecutionTime.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 c3b41e8..c9a6c7d 100644
--- a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapper.java
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefDatabaseWrapper.java
@@ -14,6 +14,7 @@
 
 package com.gerritforge.gerrit.globalrefdb.validation;
 
+import com.gerritforge.gerrit.globalrefdb.ExtendedGlobalRefDatabase;
 import com.gerritforge.gerrit.globalrefdb.GlobalRefDatabase;
 import com.gerritforge.gerrit.globalrefdb.GlobalRefDbLockException;
 import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError;
@@ -35,7 +36,7 @@
  * {@link NoopSharedRefDatabase} instance is wrapped instead.
  */
 @Singleton
-public class SharedRefDatabaseWrapper implements GlobalRefDatabase {
+public class SharedRefDatabaseWrapper implements ExtendedGlobalRefDatabase {
   private static final FluentLogger log = FluentLogger.forEnclosingClass();
   private static final GlobalRefDatabase NOOP_REFDB = new NoopSharedRefDatabase();
 
@@ -99,6 +100,23 @@
     }
   }
 
+  @Override
+  public <T> void put(Project.NameKey project, String refName, T newValue)
+      throws GlobalRefDbSystemError {
+    if (!isSetOperationSupported()) {
+      throw new UnsupportedOperationException(
+          "GlobalRefDb implementation doesn't support set operation");
+    }
+    try (Context context = metrics.startSetExecutionTime()) {
+      ((ExtendedGlobalRefDatabase) sharedRefDb()).put(project, refName, newValue);
+      sharedRefLogger.logRefUpdate(project.get(), refName, newValue);
+    }
+  }
+
+  public boolean isSetOperationSupported() {
+    return sharedRefDb() instanceof ExtendedGlobalRefDatabase;
+  }
+
   /** {@inheritDoc}. The operation is logged. */
   @Override
   public AutoCloseable lockRef(Project.NameKey project, String refName)
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogEntry.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogEntry.java
index c72ab7c..4b8132f 100644
--- a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogEntry.java
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogEntry.java
@@ -41,7 +41,7 @@
     UpdateRef(
         String projectName,
         String refName,
-        String oldId,
+        @Nullable String oldId,
         String newId,
         @Nullable GitPerson committer,
         @Nullable String comment) {
@@ -68,7 +68,7 @@
     public String refName;
     public String oldId;
 
-    DeleteRef(String projectName, String refName, String oldId) {
+    DeleteRef(String projectName, String refName, @Nullable String oldId) {
       this.type = Type.DELETE_REF;
       this.projectName = projectName;
       this.refName = refName;
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogger.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogger.java
index 858ba18..f61f253 100644
--- a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogger.java
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/SharedRefLogger.java
@@ -42,6 +42,16 @@
   <T> void logRefUpdate(String project, String refName, T currRef, T newRefValue);
 
   /**
+   * Log the update of ref pointed to by refName, in project 'project' to ref 'newRefValue'
+   *
+   * @param project the project the update is for
+   * @param refName the name of the ref being updatex
+   * @param newRefValue the new value of the ref being updated
+   * @param <T> Type of the 'currRef' and the 'newRefValue'
+   */
+  <T> void logRefUpdate(String project, String refName, T newRefValue);
+
+  /**
    * Log the deletion of 'project' from the global refdb
    *
    * @param project the project being deleted
diff --git a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabase.java b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabase.java
index 960b311..1ce480a 100644
--- a/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabase.java
+++ b/src/main/java/com/gerritforge/gerrit/globalrefdb/validation/dfsrefdb/NoopSharedRefDatabase.java
@@ -14,6 +14,7 @@
 
 package com.gerritforge.gerrit.globalrefdb.validation.dfsrefdb;
 
+import com.gerritforge.gerrit.globalrefdb.ExtendedGlobalRefDatabase;
 import com.gerritforge.gerrit.globalrefdb.GlobalRefDatabase;
 import com.gerritforge.gerrit.globalrefdb.GlobalRefDbLockException;
 import com.gerritforge.gerrit.globalrefdb.GlobalRefDbSystemError;
@@ -29,7 +30,7 @@
  * <p>This is useful for setting up a test environment and allows multi-site library to be installed
  * independently from any additional libModules or the existence of a specific Ref-DB installation.
  */
-public class NoopSharedRefDatabase implements GlobalRefDatabase {
+public class NoopSharedRefDatabase implements ExtendedGlobalRefDatabase {
 
   /**
    * Project/ref is always considered up-to-date
@@ -126,4 +127,10 @@
       throws GlobalRefDbSystemError {
     return Optional.empty();
   }
+
+  @Override
+  public <T> void put(Project.NameKey project, String refName, T newValue)
+      throws GlobalRefDbSystemError {
+    // do nothing
+  }
 }
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/ExtendedGlobalRefDatabaseTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/ExtendedGlobalRefDatabaseTest.java
new file mode 100644
index 0000000..d58e0f3
--- /dev/null
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/ExtendedGlobalRefDatabaseTest.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 GerritForge Ltd
+//
+// 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Optional;
+import org.junit.Test;
+
+public class ExtendedGlobalRefDatabaseTest extends GlobalRefDatabaseTest {
+
+  @Test
+  public void shouldSetLongValueInTheGlobalRefDB() {
+    objectUnderTest.put(project, refName, 1L);
+
+    Optional<Long> o = objectUnderTest.get(project, refName, Long.class);
+
+    assertThat(o.isPresent()).isTrue();
+    assertThat(o.get()).isEqualTo(1L);
+  }
+}
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/FakeGlobalRefDatabase.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/FakeGlobalRefDatabase.java
index 44c4fbb..5d4175f 100644
--- a/src/test/java/com/gerritforge/gerrit/globalrefdb/FakeGlobalRefDatabase.java
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/FakeGlobalRefDatabase.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.MapMaker;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
 import java.util.Optional;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.atomic.AtomicReference;
@@ -26,7 +27,7 @@
 import org.junit.Ignore;
 
 @Ignore
-public class FakeGlobalRefDatabase implements GlobalRefDatabase {
+public class FakeGlobalRefDatabase implements ExtendedGlobalRefDatabase {
 
   private ConcurrentMap<Project.NameKey, ConcurrentMap<String, AtomicReference<ObjectId>>>
       keyValueStore;
@@ -132,6 +133,12 @@
     return projectRefLock;
   }
 
+  @Override
+  public <T> void put(NameKey project, String refName, T newValue) throws GlobalRefDbSystemError {
+    String key = String.format("%s/%s", project.get(), refName);
+    genericKeyValueStore.put(key, new AtomicReference<>(newValue));
+  }
+
   private static class RefLock implements AutoCloseable {
     private Lock lock;
 
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/GlobalRefDatabaseTest.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/GlobalRefDatabaseTest.java
index bb5ad8d..8ed1f4c 100644
--- a/src/test/java/com/gerritforge/gerrit/globalrefdb/GlobalRefDatabaseTest.java
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/GlobalRefDatabaseTest.java
@@ -31,7 +31,8 @@
 
 public class GlobalRefDatabaseTest extends AbstractDaemonTest {
 
-  private String refName = RefNames.REFS_HEADS + "branch";
+  protected ExtendedGlobalRefDatabase objectUnderTest;
+  protected String refName = RefNames.REFS_HEADS + "branch";
 
   private ObjectId objectId1;
   private ObjectId objectId2;
@@ -45,8 +46,6 @@
 
   private Executor executor = Executors.newFixedThreadPool(1);
 
-  private GlobalRefDatabase objectUnderTest;
-
   @Before
   public void setup() throws Exception {
     this.objectUnderTest = new FakeGlobalRefDatabase();
diff --git a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/DisabledSharedRefLogger.java b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/DisabledSharedRefLogger.java
index f9a1e57..d6dd338 100644
--- a/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/DisabledSharedRefLogger.java
+++ b/src/test/java/com/gerritforge/gerrit/globalrefdb/validation/DisabledSharedRefLogger.java
@@ -35,4 +35,7 @@
 
   @Override
   public <T> void logRefUpdate(String project, String refName, T currRef, T newRefValue) {}
+
+  @Override
+  public <T> void logRefUpdate(String project, String refName, T newRefValue) {}
 }