Return better response when repo is out of sync

Returning LOCK_FAILURE as Result when the node is out of sync
with the shared ref-db so the client can retry if needed.

Feature: Issue 12129
Change-Id: If1a6f5e035f17c0876d0bb6f4069201099682bb5
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/BatchRefUpdateValidator.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/BatchRefUpdateValidator.java
index 932f0c4..cfe585d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/BatchRefUpdateValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/BatchRefUpdateValidator.java
@@ -19,6 +19,7 @@
 import com.google.inject.assistedinject.Assisted;
 import com.googlesource.gerrit.plugins.multisite.LockWrapper;
 import com.googlesource.gerrit.plugins.multisite.SharedRefDatabaseWrapper;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.OutOfSyncException;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefEnforcement;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefEnforcement.EnforcePolicy;
 import java.io.IOException;
@@ -100,6 +101,13 @@
       refsToUpdate = compareAndGetLatestLocalRefs(refsToUpdate, locks);
       delegateUpdate.invoke();
       updateSharedRefDb(batchRefUpdate.getCommands().stream(), refsToUpdate);
+    } catch (OutOfSyncException e) {
+      List<ReceiveCommand> receiveCommands = batchRefUpdate.getCommands();
+      logger.atWarning().withCause(e).log(
+          String.format(
+              "Batch ref-update failing because node is out of sync with the shared ref-db. Set all commands Result to LOCK_FAILURE [%d]",
+              receiveCommands.size()));
+      receiveCommands.forEach((command) -> command.setResult(ReceiveCommand.Result.LOCK_FAILURE));
     }
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/RefUpdateValidator.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/RefUpdateValidator.java
index a63c00b..1f87b77 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/RefUpdateValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/RefUpdateValidator.java
@@ -127,6 +127,11 @@
         updateSharedDbOrThrowExceptionFor(refPairForUpdate);
       }
       return result;
+    } catch (OutOfSyncException e) {
+      logger.atWarning().withCause(e).log(
+          String.format("Local node is out of sync with ref-db: %s", e.getMessage()));
+
+      return RefUpdate.Result.LOCK_FAILURE;
     }
   }
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/BatchRefUpdateValidatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/BatchRefUpdateValidatorTest.java
index 133c35d..0960cf1 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/BatchRefUpdateValidatorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/BatchRefUpdateValidatorTest.java
@@ -14,8 +14,11 @@
 
 package com.googlesource.gerrit.plugins.multisite.validation;
 
+import static com.google.common.truth.Truth.assertThat;
 import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
@@ -27,6 +30,7 @@
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefEnforcement;
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import org.eclipse.jgit.internal.storage.file.RefDirectory;
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
@@ -59,6 +63,8 @@
 
   @Mock SharedRefDatabaseWrapper sharedRefDatabase;
 
+  @Mock SharedRefEnforcement tmpRefEnforcement;
+
   @Before
   public void setup() throws Exception {
     super.setUp();
@@ -105,6 +111,32 @@
         .compareAndPut(A_TEST_PROJECT_NAME_KEY, newRef(DRAFT_COMMENT, A.getId()), B.getId());
   }
 
+  @Test
+  public void validationShouldFailWhenLocalRefDbIsOutOfSync() throws Exception {
+    String AN_OUT_OF_SYNC_REF = "refs/changes/01/1/1";
+    BatchRefUpdate batchRefUpdate =
+        newBatchUpdate(
+            Collections.singletonList(new ReceiveCommand(A, B, AN_OUT_OF_SYNC_REF, UPDATE)));
+    BatchRefUpdateValidator batchRefUpdateValidator =
+        getRefValidatorForEnforcement(A_TEST_PROJECT_NAME, tmpRefEnforcement);
+
+    doReturn(SharedRefEnforcement.EnforcePolicy.REQUIRED)
+        .when(batchRefUpdateValidator.refEnforcement)
+        .getPolicy(A_TEST_PROJECT_NAME, AN_OUT_OF_SYNC_REF);
+    lenient()
+        .doReturn(false)
+        .when(sharedRefDatabase)
+        .isUpToDate(A_TEST_PROJECT_NAME_KEY, newRef(AN_OUT_OF_SYNC_REF, AN_OBJECT_ID_1));
+
+    batchRefUpdateValidator.executeBatchUpdateWithValidation(
+        batchRefUpdate, () -> execute(batchRefUpdate));
+
+    final List<ReceiveCommand> commands = batchRefUpdate.getCommands();
+    assertThat(commands.size()).isEqualTo(1);
+    commands.forEach(
+        (command) -> assertThat(command.getResult()).isEqualTo(ReceiveCommand.Result.LOCK_FAILURE));
+  }
+
   private BatchRefUpdateValidator newDefaultValidator(String projectName) {
     return getRefValidatorForEnforcement(projectName, new DefaultSharedRefEnforcement());
   }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteBatchRefUpdateTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteBatchRefUpdateTest.java
index f6eadcc..187f686 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteBatchRefUpdateTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/MultiSiteBatchRefUpdateTest.java
@@ -15,10 +15,11 @@
 package com.googlesource.gerrit.plugins.multisite.validation;
 
 import static java.util.Arrays.asList;
-import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
@@ -50,6 +51,7 @@
 
   @Mock SharedRefDatabaseWrapper sharedRefDb;
   @Mock BatchRefUpdate batchRefUpdate;
+  @Mock BatchRefUpdateValidator batchRefUpdateValidator;
   @Mock RefDatabase refDatabase;
   @Mock RevWalk revWalk;
   @Mock ProgressMonitor progressMonitor;
@@ -123,17 +125,25 @@
     return argThat(new RefMatcher(oldRef));
   }
 
-  @Test
+  @Test(expected = IOException.class)
   public void executeAndFailsWithExceptions() throws IOException {
+    multiSiteRefUpdate = getMultiSiteBatchRefUpdateWithMockedValidator();
+    doThrow(new IOException("IO Test Exception"))
+        .when(batchRefUpdateValidator)
+        .executeBatchUpdateWithValidation(any(), any());
+
+    multiSiteRefUpdate.execute(revWalk, progressMonitor, Collections.emptyList());
+  }
+
+  @Test
+  public void executeSuccessfullyWithNoExceptionsWhenOutOfSync() throws IOException {
     setMockRequiredReturnValues();
     doReturn(true).when(sharedRefDb).exists(A_TEST_PROJECT_NAME_KEY, A_TEST_REF_NAME);
     doReturn(false).when(sharedRefDb).isUpToDate(A_TEST_PROJECT_NAME_KEY, oldRef);
-    try {
-      multiSiteRefUpdate.execute(revWalk, progressMonitor, Collections.emptyList());
-      fail("Expecting an IOException to be thrown");
-    } catch (IOException e) {
-      verify(validationMetrics).incrementSplitBrainPrevention();
-    }
+
+    multiSiteRefUpdate.execute(revWalk, progressMonitor, Collections.emptyList());
+
+    verify(validationMetrics).incrementSplitBrainPrevention();
   }
 
   @Test
@@ -163,6 +173,17 @@
     return new MultiSiteBatchRefUpdate(batchRefValidatorFactory, A_TEST_PROJECT_NAME, refDatabase);
   }
 
+  private MultiSiteBatchRefUpdate getMultiSiteBatchRefUpdateWithMockedValidator() {
+    BatchRefUpdateValidator.Factory batchRefValidatorFactory =
+        new BatchRefUpdateValidator.Factory() {
+          @Override
+          public BatchRefUpdateValidator create(String projectName, RefDatabase refDb) {
+            return batchRefUpdateValidator;
+          }
+        };
+    return new MultiSiteBatchRefUpdate(batchRefValidatorFactory, A_TEST_PROJECT_NAME, refDatabase);
+  }
+
   protected static class RefMatcher implements ArgumentMatcher<Ref> {
     private Ref left;
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/RefUpdateValidatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/RefUpdateValidatorTest.java
index 3887611..a9968c4 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/RefUpdateValidatorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/RefUpdateValidatorTest.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.googlesource.gerrit.plugins.multisite.SharedRefDatabaseWrapper;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.DefaultSharedRefEnforcement;
-import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.OutOfSyncException;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.RefFixture;
 import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedDbSplitBrainException;
 import org.eclipse.jgit.lib.ObjectId;
@@ -139,8 +138,8 @@
     assertThat(result).isEqualTo(RefUpdate.Result.NEW);
   }
 
-  @Test(expected = OutOfSyncException.class)
-  public void validationShouldFailWhenLocalRefDbIsNotUpToDate() throws Exception {
+  @Test
+  public void validationShouldFailWhenLocalRefDbIsOutOfSync() throws Exception {
     lenient()
         .doReturn(true)
         .when(sharedRefDb)
@@ -148,7 +147,9 @@
     doReturn(true).when(sharedRefDb).exists(A_TEST_PROJECT_NAME_KEY, refName);
     doReturn(false).when(sharedRefDb).isUpToDate(A_TEST_PROJECT_NAME_KEY, localRef);
 
-    refUpdateValidator.executeRefUpdate(refUpdate, () -> RefUpdate.Result.NEW);
+    Result result = refUpdateValidator.executeRefUpdate(refUpdate, () -> RefUpdate.Result.NEW);
+
+    assertThat(result).isEqualTo(Result.LOCK_FAILURE);
   }
 
   @Test(expected = SharedDbSplitBrainException.class)