Use a global version number instead of the commit timestamp

The commit timestamp is not a reliable source of repository
version numbering because of unreliability of the local
time of the committer and forced push use-case, where the
repository is brought back in time.

Define a new global version numbering
(current system time at the moment) and
use it as monotonic versioning for the repository.

Bug: Issue 12413
Change-Id: If1b642a56791bd3a4c1d6ef903ec2f7c61c19928
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdate.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdate.java
index d13ed79..3b82582 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdate.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdate.java
@@ -43,8 +43,6 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 
 @Singleton
 public class ProjectVersionRefUpdate implements EventListener {
@@ -101,18 +99,16 @@
     }
     try {
       Project.NameKey projectNameKey = refUpdatedEvent.getProjectNameKey();
+      long newVersion = getCurrentGlobalVersionNumber();
 
-      Optional<Long> lastRefUpdatedTimestamp = getLastRefUpdatedTimestamp(projectNameKey, refName);
       Optional<RefUpdate> newProjectVersionRefUpdate =
-          updateLocalProjectVersion(projectNameKey, lastRefUpdatedTimestamp);
+          updateLocalProjectVersion(projectNameKey, newVersion);
 
-      if (newProjectVersionRefUpdate.isPresent() && lastRefUpdatedTimestamp.isPresent()) {
-        verLogger.log(projectNameKey, lastRefUpdatedTimestamp.get(), 0L);
+      if (newProjectVersionRefUpdate.isPresent()) {
+        verLogger.log(projectNameKey, newVersion, 0L);
 
         if (updateSharedProjectVersion(
-            projectNameKey,
-            newProjectVersionRefUpdate.get().getNewObjectId(),
-            lastRefUpdatedTimestamp.get())) {
+            projectNameKey, newProjectVersionRefUpdate.get().getNewObjectId(), newVersion)) {
           gitReferenceUpdated.fire(projectNameKey, newProjectVersionRefUpdate.get(), null);
         }
       } else {
@@ -142,31 +138,6 @@
     return newId;
   }
 
-  private Optional<Long> getLastRefUpdatedTimestamp(Project.NameKey projectNameKey, String refName)
-      throws LocalProjectVersionUpdateException {
-    logger.atFine().log(
-        String.format(
-            "Getting last ref updated time for project %s, ref %s", projectNameKey.get(), refName));
-    try (Repository repository = gitRepositoryManager.openRepository(projectNameKey)) {
-      Ref ref = repository.findRef(refName);
-      if (ref == null) {
-        logger.atWarning().log("Unable to find ref " + refName + " in project " + projectNameKey);
-        return Optional.empty();
-      }
-      try (RevWalk walk = new RevWalk(repository)) {
-        RevCommit commit = walk.parseCommit(ref.getObjectId());
-        return Optional.of(Integer.toUnsignedLong(commit.getCommitTime()));
-      }
-    } catch (IOException ioe) {
-      String message =
-          String.format(
-              "Error while getting last ref updated time for project %s, ref %s",
-              projectNameKey.get(), refName);
-      logger.atSevere().withCause(ioe).log(message);
-      throw new LocalProjectVersionUpdateException(message);
-    }
-  }
-
   private boolean updateSharedProjectVersion(
       Project.NameKey projectNameKey, ObjectId newObjectId, Long newVersion)
       throws SharedProjectVersionUpdateException {
@@ -282,21 +253,19 @@
   }
 
   private Optional<RefUpdate> updateLocalProjectVersion(
-      Project.NameKey projectNameKey, Optional<Long> lastRefUpdatedTimestamp)
+      Project.NameKey projectNameKey, long newVersionNumber)
       throws LocalProjectVersionUpdateException {
-    if (!lastRefUpdatedTimestamp.isPresent()) {
-      return Optional.empty();
-    }
-
-    logger.atFine().log("Updating local version for project " + projectNameKey.get());
+    logger.atFine().log(
+        "Updating local version for project %s with version %d",
+        projectNameKey.get(), newVersionNumber);
     try (Repository repository = gitRepositoryManager.openRepository(projectNameKey)) {
-      RefUpdate refUpdate = getProjectVersionRefUpdate(repository, lastRefUpdatedTimestamp.get());
+      RefUpdate refUpdate = getProjectVersionRefUpdate(repository, newVersionNumber);
       RefUpdate.Result result = refUpdate.update();
       if (!isSuccessful(result)) {
         String message =
             String.format(
                 "RefUpdate failed with result %s for: project=%s, version=%d",
-                result.name(), projectNameKey.get(), lastRefUpdatedTimestamp.get());
+                result.name(), projectNameKey.get(), newVersionNumber);
         logger.atSevere().log(message);
         throw new LocalProjectVersionUpdateException(message);
       }
@@ -309,6 +278,10 @@
     }
   }
 
+  private long getCurrentGlobalVersionNumber() {
+    return System.currentTimeMillis() / 1000;
+  }
+
   private Boolean isSuccessful(RefUpdate.Result result) {
     return SUCCESSFUL_RESULTS.contains(result);
   }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateTest.java
index 1839fe1..6360227 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ProjectVersionRefUpdateTest.java
@@ -116,10 +116,56 @@
     assertThat(ref).isNotNull();
 
     ObjectLoader loader = repo.getRepository().open(ref.getObjectId());
-    String storedVersion = IOUtils.toString(loader.openStream(), StandardCharsets.UTF_8.name());
-    assertThat(Long.parseLong(storedVersion)).isEqualTo(masterCommit.getCommitTime());
+    long storedVersion =
+        Long.parseLong(IOUtils.toString(loader.openStream(), StandardCharsets.UTF_8.name()));
+    assertThat(storedVersion).isGreaterThan((long) masterCommit.getCommitTime());
 
-    verify(verLogger).log(A_TEST_PROJECT_NAME_KEY, masterCommit.getCommitTime(), 0);
+    verify(verLogger).log(A_TEST_PROJECT_NAME_KEY, storedVersion, 0);
+  }
+
+  @Test
+  public void producerShouldUpdateProjectVersionUponForcedPushRefUpdatedEvent() throws Exception {
+    Context.setForwardedEvent(false);
+
+    Thread.sleep(1000L);
+    RevCommit masterPlusOneCommit = repo.branch("master").commit().create();
+
+    Thread.sleep(1000L);
+    repo.branch("master").update(masterCommit);
+
+    when(sharedRefDb.get(
+            A_TEST_PROJECT_NAME_KEY,
+            ProjectVersionRefUpdate.MULTI_SITE_VERSIONING_REF,
+            String.class))
+        .thenReturn(Optional.of("26f7ee61bf0e470e8393c884526eec8a9b943a63"));
+    when(sharedRefDb.get(
+            A_TEST_PROJECT_NAME_KEY,
+            ProjectVersionRefUpdate.MULTI_SITE_VERSIONING_VALUE_REF,
+            String.class))
+        .thenReturn(Optional.of("" + (masterCommit.getCommitTime() - 1)));
+    when(sharedRefDb.compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class)))
+        .thenReturn(true);
+    when(sharedRefDb.compareAndPut(any(Project.NameKey.class), any(String.class), any(), any()))
+        .thenReturn(true);
+    when(refUpdatedEvent.getProjectNameKey()).thenReturn(A_TEST_PROJECT_NAME_KEY);
+    when(refUpdatedEvent.getRefName()).thenReturn(A_TEST_REF_NAME);
+
+    new ProjectVersionRefUpdate(repoManager, sharedRefDb, gitReferenceUpdated, verLogger)
+        .onEvent(refUpdatedEvent);
+
+    Ref ref = repo.getRepository().findRef(MULTI_SITE_VERSIONING_REF);
+
+    verify(sharedRefDb, atMost(1))
+        .compareAndPut(any(Project.NameKey.class), any(Ref.class), any(ObjectId.class));
+
+    assertThat(ref).isNotNull();
+
+    ObjectLoader loader = repo.getRepository().open(ref.getObjectId());
+    long storedVersion =
+        Long.parseLong(IOUtils.toString(loader.openStream(), StandardCharsets.UTF_8.name()));
+    assertThat(storedVersion).isGreaterThan((long) masterPlusOneCommit.getCommitTime());
+
+    verify(verLogger).log(A_TEST_PROJECT_NAME_KEY, storedVersion, 0);
   }
 
   @Test
@@ -155,10 +201,11 @@
     assertThat(ref).isNotNull();
 
     ObjectLoader loader = repo.getRepository().open(ref.getObjectId());
-    String storedVersion = IOUtils.toString(loader.openStream(), StandardCharsets.UTF_8.name());
-    assertThat(Long.parseLong(storedVersion)).isEqualTo(masterCommit.getCommitTime());
+    long storedVersion =
+        Long.parseLong(IOUtils.toString(loader.openStream(), StandardCharsets.UTF_8.name()));
+    assertThat(storedVersion).isGreaterThan((long) masterCommit.getCommitTime());
 
-    verify(verLogger).log(A_TEST_PROJECT_NAME_KEY, masterCommit.getCommitTime(), 0);
+    verify(verLogger).log(A_TEST_PROJECT_NAME_KEY, storedVersion, 0);
   }
 
   @Test