Add embrional support for split brain detection

Adds a RefOperationValidationListener to intercept and block operations
if they might cause a split brain, i.e. if the oldId of the refs
they are based on is not the currently most recent in the cluster.
The current code doesn't provide a real implementation of the
centralised DB but just an in memory implementation and a no-op one.
The in memory implementation is probably useless but shows the mechanics
of the centralised one.

The Integration Tests are currently just validating that the new code doesn't
introduce any regression.

Configuration for the in-memory module has not been exposed.

Feature: Issue 10554
Change-Id: I8351ddb386a9528721c1c9670e3304b40f88a67c
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java
index 5b900c9..0469bf1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java
@@ -28,6 +28,7 @@
 import com.googlesource.gerrit.plugins.multisite.index.IndexModule;
 import com.googlesource.gerrit.plugins.multisite.kafka.consumer.KafkaConsumerModule;
 import com.googlesource.gerrit.plugins.multisite.kafka.router.ForwardedEventRouterModule;
+import com.googlesource.gerrit.plugins.multisite.validation.ValidationModule;
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.FileReader;
@@ -73,6 +74,8 @@
       install(new BrokerForwarderModule(config.kafkaPublisher()));
     }
 
+    install(new ValidationModule());
+
     bind(Gson.class).toProvider(GsonProvider.class).in(Singleton.class);
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/InSyncChangeValidator.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/InSyncChangeValidator.java
new file mode 100644
index 0000000..09a1b56
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/InSyncChangeValidator.java
@@ -0,0 +1,198 @@
+// Copyright (C) 2019 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.
+// Copyright (C) 2018 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.googlesource.gerrit.plugins.multisite.validation;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.server.events.RefReceivedEvent;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.validators.RefOperationValidationListener;
+import com.google.gerrit.server.git.validators.ValidationMessage;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Validates if a change can be applied without bringing the system into a split brain situation by
+ * verifying that the local status is aligned with the central status as retrieved by the
+ * SharedRefDatabase. It also updates the DB to set the new current status for a ref as a
+ * consequence of ref updates, creation and deletions. The operation is done for mutable updates
+ * only. Operation on immutable ones are always considered valid.
+ */
+public class InSyncChangeValidator implements RefOperationValidationListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final SharedRefDatabase dfsRefDatabase;
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  public InSyncChangeValidator(SharedRefDatabase dfsRefDatabase, GitRepositoryManager repoManager) {
+    this.dfsRefDatabase = dfsRefDatabase;
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public List<ValidationMessage> onRefOperation(RefReceivedEvent refEvent)
+      throws ValidationException {
+    logger.atFine().log("Validating operation %s", refEvent);
+
+    if (isImmutableRef(refEvent.getRefName())) {
+      return Collections.emptyList();
+    }
+
+    try (Repository repo = repoManager.openRepository(refEvent.getProjectNameKey())) {
+
+      switch (refEvent.command.getType()) {
+        case CREATE:
+          return onCreateRef(refEvent);
+
+        case UPDATE:
+        case UPDATE_NONFASTFORWARD:
+          return onUpdateRef(repo, refEvent);
+
+        case DELETE:
+          return onDeleteRef(repo, refEvent);
+
+        default:
+          throw new IllegalArgumentException(
+              String.format(
+                  "Unsupported command type '%s', in event %s",
+                  refEvent.command.getType().name(), refEvent));
+      }
+    } catch (IOException e) {
+      throw new ValidationException(
+          "Unable to access repository " + refEvent.getProjectNameKey(), e);
+    }
+  }
+
+  private boolean isImmutableRef(String refName) {
+    return refName.startsWith("refs/changes") && !refName.endsWith("/meta");
+  }
+
+  private List<ValidationMessage> onDeleteRef(Repository repo, RefReceivedEvent refEvent)
+      throws ValidationException {
+    try {
+      Ref localRef = repo.findRef(refEvent.getRefName());
+      if (localRef == null) {
+        logger.atWarning().log(
+            "Local status inconsistent with shared ref database for ref %s. "
+                + "Trying to delete it but it is not in the local DB",
+            refEvent.getRefName());
+
+        throw new ValidationException(
+            String.format(
+                "Unable to delete ref '%s', cannot find it in the local ref database",
+                refEvent.getRefName()));
+      }
+
+      if (!dfsRefDatabase.compareAndRemove(refEvent.getProjectNameKey().get(), localRef)) {
+        throw new ValidationException(
+            String.format(
+                "Unable to delete ref '%s', the local ObjectId '%s' is not equal to the one "
+                    + "in the shared ref database",
+                refEvent.getRefName(), localRef.getObjectId().getName()));
+      }
+    } catch (IOException ioe) {
+      logger.atSevere().withCause(ioe).log(
+          "Local status inconsistent with shared ref database for ref %s. "
+              + "Trying to delete it but it is not in the DB",
+          refEvent.getRefName());
+
+      throw new ValidationException(
+          String.format(
+              "Unable to delete ref '%s', cannot find it in the shared ref database",
+              refEvent.getRefName()),
+          ioe);
+    }
+    return Collections.emptyList();
+  }
+
+  private List<ValidationMessage> onUpdateRef(Repository repo, RefReceivedEvent refEvent)
+      throws ValidationException {
+    try {
+      Ref localRef = repo.findRef(refEvent.getRefName());
+      if (localRef == null) {
+        logger.atWarning().log(
+            "Local status inconsistent with shared ref database for ref %s. "
+                + "Trying to update it but it is not in the local DB",
+            refEvent.getRefName());
+
+        throw new ValidationException(
+            String.format(
+                "Unable to update ref '%s', cannot find it in the local ref database",
+                refEvent.getRefName()));
+      }
+
+      Ref newRef = dfsRefDatabase.newRef(refEvent.getRefName(), refEvent.command.getNewId());
+      if (!dfsRefDatabase.compareAndPut(refEvent.getProjectNameKey().get(), localRef, newRef)) {
+        throw new ValidationException(
+            String.format(
+                "Unable to update ref '%s', the local objectId '%s' is not equal to the one "
+                    + "in the shared ref database",
+                refEvent.getRefName(), localRef.getObjectId().getName()));
+      }
+    } catch (IOException ioe) {
+      logger.atSevere().withCause(ioe).log(
+          "Local status inconsistent with shared ref database for ref %s. "
+              + "Trying to update it cannot extract the existing one on DB",
+          refEvent.getRefName());
+
+      throw new ValidationException(
+          String.format(
+              "Unable to update ref '%s', cannot open the local ref on the local DB",
+              refEvent.getRefName()),
+          ioe);
+    }
+
+    return Collections.emptyList();
+  }
+
+  private List<ValidationMessage> onCreateRef(RefReceivedEvent refEvent)
+      throws ValidationException {
+    try {
+      Ref newRef = dfsRefDatabase.newRef(refEvent.getRefName(), refEvent.command.getNewId());
+      dfsRefDatabase.compareAndCreate(refEvent.getProjectNameKey().get(), newRef);
+    } catch (IllegalArgumentException | IOException alreadyInDB) {
+      logger.atSevere().withCause(alreadyInDB).log(
+          "Local status inconsistent with shared ref database for ref %s. "
+              + "Trying to delete it but it is not in the DB",
+          refEvent.getRefName());
+
+      throw new ValidationException(
+          String.format(
+              "Unable to update ref '%s', cannot find it in the shared ref database",
+              refEvent.getRefName()),
+          alreadyInDB);
+    }
+    return Collections.emptyList();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java
new file mode 100644
index 0000000..f2a6b94
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationModule.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2019 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.googlesource.gerrit.plugins.multisite.validation;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.git.validators.RefOperationValidationListener;
+import com.google.inject.AbstractModule;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.NoOpDfsRefDatabase;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase;
+
+public class ValidationModule extends AbstractModule {
+
+  @Override
+  protected void configure() {
+    DynamicSet.bind(binder(), RefOperationValidationListener.class).to(InSyncChangeValidator.class);
+
+    bind(SharedRefDatabase.class).to(NoOpDfsRefDatabase.class);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/NoOpDfsRefDatabase.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/NoOpDfsRefDatabase.java
new file mode 100644
index 0000000..801fc3b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/NoOpDfsRefDatabase.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2019 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.googlesource.gerrit.plugins.multisite.validation.dfsrefdb;
+
+import java.io.IOException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+
+public class NoOpDfsRefDatabase implements SharedRefDatabase {
+
+  @Override
+  public Ref newRef(String refName, ObjectId objectId) {
+    return null;
+  }
+
+  @Override
+  public boolean compareAndPut(String project, Ref oldRef, Ref newRef) throws IOException {
+    return true;
+  }
+
+  @Override
+  public boolean compareAndRemove(String project, Ref oldRef) throws IOException {
+    return true;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/SharedRefDatabase.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/SharedRefDatabase.java
new file mode 100644
index 0000000..b995df9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/validation/dfsrefdb/SharedRefDatabase.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2019 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.googlesource.gerrit.plugins.multisite.validation.dfsrefdb;
+
+import java.io.IOException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+
+public interface SharedRefDatabase {
+  Ref NULL_REF =
+      new Ref() {
+
+        @Override
+        public String getName() {
+          return null;
+        }
+
+        @Override
+        public boolean isSymbolic() {
+          return false;
+        }
+
+        @Override
+        public Ref getLeaf() {
+          return null;
+        }
+
+        @Override
+        public Ref getTarget() {
+          return null;
+        }
+
+        @Override
+        public ObjectId getObjectId() {
+          return null;
+        }
+
+        @Override
+        public ObjectId getPeeledObjectId() {
+          return null;
+        }
+
+        @Override
+        public boolean isPeeled() {
+          return false;
+        }
+
+        @Override
+        public Storage getStorage() {
+          return Storage.NEW;
+        }
+      };
+
+  /**
+   * Create a new in-memory Ref name associated with an objectId.
+   *
+   * @param refName ref name
+   * @param objectId object id
+   */
+  Ref newRef(String refName, ObjectId objectId);
+
+  /**
+   * Utility method for new refs.
+   *
+   * @param project project name of the ref
+   * @param newRef new reference to store.
+   * @return
+   * @throws IOException
+   */
+  default boolean compareAndCreate(String project, Ref newRef) throws IOException {
+    return compareAndPut(project, NULL_REF, newRef);
+  }
+
+  /**
+   * Compare a reference, and put if it matches.
+   *
+   * <p>Two reference match if and only if they satisfy the following:
+   *
+   * <ul>
+   *   <li>If one reference is a symbolic ref, the other one should be a symbolic ref.
+   *   <li>If both are symbolic refs, the target names should be same.
+   *   <li>If both are object ID refs, the object IDs should be same.
+   * </ul>
+   *
+   * @param project project name of the ref
+   * @param oldRef old value to compare to. If the reference is expected to not exist the old value
+   *     has a storage of {@link org.eclipse.jgit.lib.Ref.Storage#NEW} and an ObjectId value of
+   *     {@code null}.
+   * @param newRef new reference to store.
+   * @return true if the put was successful; false otherwise.
+   * @throws java.io.IOException the reference cannot be put due to a system error.
+   */
+  boolean compareAndPut(String project, Ref oldRef, Ref newRef) throws IOException;
+
+  /**
+   * Compare a reference, and delete if it matches.
+   *
+   * @param project project name of the ref
+   * @param oldRef the old reference information that was previously read.
+   * @return true if the remove was successful; false otherwise.
+   * @throws java.io.IOException the reference could not be removed due to a system error.
+   */
+  boolean compareAndRemove(String project, Ref oldRef) throws IOException;
+}
diff --git a/src/main/resources/Documentation/git-replication-healthy.txt b/src/main/resources/Documentation/git-replication-healthy.txt
new file mode 100644
index 0000000..8367e7c
--- /dev/null
+++ b/src/main/resources/Documentation/git-replication-healthy.txt
@@ -0,0 +1,33 @@
+title Healthy Replication 
+
+participant Client1 
+participant Instance1
+participant Instance2
+participant Client2
+
+state over Client1, Client2, Instance1, Instance2: W0
+state over Client1 : W0 -> W1
+Client1 -> +Instance1: Push W1
+Instance1 -> Client1: Ack W1
+state over Instance1 : W0 -> W1
+Instance1->-Instance2: Replicate W1
+state over Instance2, Client1, Instance1: W0 -> W1
+
+state over Instance1 : Crash
+
+state over Client2 : W0 -> W2
+Client2 -> +Instance2: Push W2
+Instance2 -> Client2: Missing W1
+Client2 -> Instance2: Pull W1
+state over Client2 : W0 -> W1 -> W2
+Client2 -> Instance2: Push W2
+
+state over Instance2 : W0 -> W1 -> W2
+
+state over Instance1:  Restart
+
+Instance2->Instance1: Replicate W2
+
+state over Instance2, Client2, Instance1: W0 -> W1 -> W2
+
+
diff --git a/src/main/resources/Documentation/git-replication-split-brain-detected.txt b/src/main/resources/Documentation/git-replication-split-brain-detected.txt
new file mode 100644
index 0000000..b249b9f
--- /dev/null
+++ b/src/main/resources/Documentation/git-replication-split-brain-detected.txt
@@ -0,0 +1,45 @@
+title Replication - Split Brain Detected
+
+participant Client1 
+participant Instance1
+participant Ref-DB Coordinator
+participant Instance2
+participant Client2
+
+state over Client1, Client2, Instance1, Instance2: W0
+state over Client1 : W0 -> W1
+Client1 -> +Instance1: Push W1
+Instance1 -> +Ref-DB Coordinator: CAS if state == W0 set state W0 -> W1
+state over Ref-DB Coordinator : W0 -> W1
+Ref-DB Coordinator -> -Instance1 : ACK
+state over Instance1 : W0 -> W1
+Instance1 -> -Client1: Ack W1
+
+state over Instance1 : Crash
+
+state over Client2 : W0 -> W2
+Client2 -> +Instance2: Push W2
+
+Instance2 -> +Ref-DB Coordinator: CAS if state == W0 set state W0 -> W2
+Ref-DB Coordinator -> -Instance2 : NACK
+
+Instance2 -> -Client2 : Push failed -- RO Mode
+
+state over Instance1:  Restart
+
+Instance1->Instance2: Replicate W1 
+
+state over Instance2 : W0 -> W1
+
+Client2 -> +Instance2: Pull W1
+Instance2 -> Client2 : Missing W1
+Client2 -> Instance2: Pull W1
+state over Client2 : W0 -> W1 -> W2
+Client2 -> Instance2: Push W2
+Instance2 -> +Ref-DB Coordinator: CAS if state == W1 set state W1 -> W2
+state over Ref-DB Coordinator: W0 -> W1 -> W2
+Ref-DB Coordinator -> -Instance2 : ACK
+state over Instance2: W0 -> W1 -> W2 
+Instance2 -> -Client2 : ACK 
+
+
diff --git a/src/main/resources/Documentation/git-replication-split-brain.txt b/src/main/resources/Documentation/git-replication-split-brain.txt
new file mode 100644
index 0000000..1a1fc73
--- /dev/null
+++ b/src/main/resources/Documentation/git-replication-split-brain.txt
@@ -0,0 +1,38 @@
+title Replication - Split Brain 
+
+participant Client1 
+participant Instance1
+participant Instance2
+participant Client2
+
+state over Client1, Client2, Instance1, Instance2: W0
+state over Client1 : W0 -> W1
+Client1 -> +Instance1: Push W1
+Instance1 -> -Client1: Ack W1
+state over Instance1 : W0 -> W1
+state over Instance1 : Crash
+
+state over Client2 : W0 -> W2
+Client2 -> +Instance2: Push W2
+Instance2 -> -Client2 : Ack W2
+state over Instance2 : W0 -> W2
+
+state over Instance1:  Restart
+
+par 
+    Instance2->Instance1: Replicate W2
+    Instance1->Instance2: Replicate W1 
+end
+
+parallel {
+    state over Instance2: W0 -> W1 
+    state over Instance1: W0 -> W2
+    state over Client1: W0 -> W1
+    state over Client2: W0 -> W2
+}
+
+note over Instance1, Instance2
+    Instances status diverged 
+    and is even swapped from 
+    original 
+end note
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/InSyncChangeValidatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/InSyncChangeValidatorTest.java
new file mode 100644
index 0000000..6668529
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/InSyncChangeValidatorTest.java
@@ -0,0 +1,331 @@
+// Copyright (C) 2019 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.googlesource.gerrit.plugins.multisite.validation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.CoreMatchers.sameInstance;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.events.RefReceivedEvent;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.validators.ValidationMessage;
+import com.google.gerrit.server.validators.ValidationException;
+import com.googlesource.gerrit.plugins.multisite.validation.dfsrefdb.SharedRefDatabase;
+import java.io.IOException;
+import java.util.List;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceiveCommand.Type;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class InSyncChangeValidatorTest {
+  static final String PROJECT_NAME = "AProject";
+  static final Project.NameKey PROJECT_NAMEKEY = new Project.NameKey(PROJECT_NAME);
+  static final String REF_NAME = "refs/heads/master";
+  static final String REF_PATCHSET_NAME = "refs/changes/45/1245/1";
+  static final String REF_PATCHSET_META_NAME = "refs/changes/45/1245/1/meta";
+  static final ObjectId REF_OBJID = ObjectId.fromString("f2ffe80abb77223f3f8921f3f068b0e32d40f798");
+  static final ObjectId REF_OBJID_OLD =
+      ObjectId.fromString("a9a7a6fd1e9ad39a13fef5e897dc6d932a3282e1");
+  static final ReceiveCommand RECEIVE_COMMAND_CREATE_REF =
+      new ReceiveCommand(ObjectId.zeroId(), REF_OBJID, REF_NAME, Type.CREATE);
+  static final ReceiveCommand RECEIVE_COMMAND_UPDATE_REF =
+      new ReceiveCommand(REF_OBJID_OLD, REF_OBJID, REF_NAME, Type.UPDATE);
+  static final ReceiveCommand RECEIVE_COMMAND_DELETE_REF =
+      new ReceiveCommand(REF_OBJID_OLD, ObjectId.zeroId(), REF_NAME, Type.DELETE);
+  static final ReceiveCommand RECEIVE_COMMAND_CREATE_PATCHSET_REF =
+      new ReceiveCommand(ObjectId.zeroId(), REF_OBJID, REF_PATCHSET_NAME, Type.CREATE);
+  static final ReceiveCommand RECEIVE_COMMAND_CREATE_PATCHSET_META_REF =
+      new ReceiveCommand(ObjectId.zeroId(), REF_OBJID, REF_PATCHSET_META_NAME, Type.CREATE);
+
+  @Rule public ExpectedException expectedException = ExpectedException.none();
+
+  @Mock SharedRefDatabase dfsRefDatabase;
+
+  @Mock Repository repo;
+
+  @Mock RefDatabase localRefDatabase;
+
+  @Mock GitRepositoryManager repoManager;
+
+  private InSyncChangeValidator validator;
+
+  static class TestRef implements Ref {
+    private final String name;
+    private final ObjectId objectId;
+
+    public TestRef(String name, ObjectId objectId) {
+      super();
+      this.name = name;
+      this.objectId = objectId;
+    }
+
+    @Override
+    public String getName() {
+      return name;
+    }
+
+    @Override
+    public boolean isSymbolic() {
+      return false;
+    }
+
+    @Override
+    public Ref getLeaf() {
+      return null;
+    }
+
+    @Override
+    public Ref getTarget() {
+      return null;
+    }
+
+    @Override
+    public ObjectId getObjectId() {
+      return objectId;
+    }
+
+    @Override
+    public ObjectId getPeeledObjectId() {
+      return null;
+    }
+
+    @Override
+    public boolean isPeeled() {
+      return false;
+    }
+
+    @Override
+    public Storage getStorage() {
+      return Storage.LOOSE;
+    }
+  }
+
+  static class RefMatcher implements ArgumentMatcher<Ref> {
+    private final String name;
+    private final ObjectId objectId;
+
+    public RefMatcher(String name, ObjectId objectId) {
+      super();
+      this.name = name;
+      this.objectId = objectId;
+    }
+
+    @Override
+    public boolean matches(Ref that) {
+      if (that == null) {
+        return false;
+      }
+
+      return name.equals(that.getName()) && objectId.equals(that.getObjectId());
+    }
+  }
+
+  public static Ref eqRef(String name, ObjectId objectId) {
+    return argThat(new RefMatcher(name, objectId));
+  }
+
+  Ref testRef = new TestRef(REF_NAME, REF_OBJID);
+  RefReceivedEvent testRefReceivedEvent =
+      new RefReceivedEvent() {
+
+        @Override
+        public String getRefName() {
+          return command.getRefName();
+        }
+
+        @Override
+        public com.google.gerrit.reviewdb.client.Project.NameKey getProjectNameKey() {
+          return PROJECT_NAMEKEY;
+        }
+      };
+
+  @Before
+  public void setUp() throws IOException {
+    doReturn(testRef).when(dfsRefDatabase).newRef(REF_NAME, REF_OBJID);
+    doReturn(repo).when(repoManager).openRepository(PROJECT_NAMEKEY);
+    doReturn(localRefDatabase).when(repo).getRefDatabase();
+    lenient()
+        .doThrow(new NullPointerException("oldRef is null"))
+        .when(dfsRefDatabase)
+        .compareAndPut(any(), eq(null), any());
+    lenient()
+        .doThrow(new NullPointerException("newRef is null"))
+        .when(dfsRefDatabase)
+        .compareAndPut(any(), any(), eq(null));
+    lenient()
+        .doThrow(new NullPointerException("project name is null"))
+        .when(dfsRefDatabase)
+        .compareAndPut(eq(null), any(), any());
+
+    validator = new InSyncChangeValidator(dfsRefDatabase, repoManager);
+    repoManager.createRepository(PROJECT_NAMEKEY);
+  }
+
+  @Test
+  public void shouldNotVerifyStatusOfImmutablePatchSetRefs() throws Exception {
+    testRefReceivedEvent.command = RECEIVE_COMMAND_CREATE_PATCHSET_REF;
+    final List<ValidationMessage> validationMessages =
+        validator.onRefOperation(testRefReceivedEvent);
+
+    assertThat(validationMessages).isEmpty();
+
+    verifyZeroInteractions(dfsRefDatabase);
+  }
+
+  @Test
+  public void shouldVerifyStatusOfPatchSetMetaRefs() throws Exception {
+    testRefReceivedEvent.command = RECEIVE_COMMAND_CREATE_PATCHSET_META_REF;
+
+    Ref testRefMeta = new TestRef(REF_PATCHSET_META_NAME, REF_OBJID);
+    doReturn(testRefMeta).when(dfsRefDatabase).newRef(REF_PATCHSET_META_NAME, REF_OBJID);
+
+    validator.onRefOperation(testRefReceivedEvent);
+
+    verify(dfsRefDatabase)
+        .compareAndCreate(eq(PROJECT_NAME), eqRef(REF_PATCHSET_META_NAME, REF_OBJID));
+  }
+
+  @Test
+  public void shouldInsertNewRefInDfsDatabaseWhenHandlingRefCreationEvents() throws Exception {
+    testRefReceivedEvent.command = RECEIVE_COMMAND_CREATE_REF;
+
+    final List<ValidationMessage> validationMessages =
+        validator.onRefOperation(testRefReceivedEvent);
+
+    assertThat(validationMessages).isEmpty();
+    verify(dfsRefDatabase).compareAndCreate(eq(PROJECT_NAME), eqRef(REF_NAME, REF_OBJID));
+  }
+
+  @Test
+  public void shouldFailRefCreationIfInsertANewRefInDfsDatabaseFails() throws Exception {
+    testRefReceivedEvent.command = RECEIVE_COMMAND_CREATE_REF;
+
+    IllegalArgumentException alreadyInDb = new IllegalArgumentException("obj is already in db");
+
+    doThrow(alreadyInDb)
+        .when(dfsRefDatabase)
+        .compareAndCreate(eq(PROJECT_NAME), eqRef(REF_NAME, REF_OBJID));
+
+    expectedException.expect(ValidationException.class);
+    expectedException.expectCause(sameInstance(alreadyInDb));
+
+    validator.onRefOperation(testRefReceivedEvent);
+  }
+
+  @Test
+  public void shouldUpdateRefInDfsDatabaseWhenHandlingRefUpdateEvents() throws Exception {
+    testRefReceivedEvent.command = RECEIVE_COMMAND_UPDATE_REF;
+    doReturn(new TestRef(REF_NAME, REF_OBJID_OLD)).when(localRefDatabase).getRef(REF_NAME);
+    doReturn(true)
+        .when(dfsRefDatabase)
+        .compareAndPut(
+            eq(PROJECT_NAME), eqRef(REF_NAME, REF_OBJID_OLD), eqRef(REF_NAME, REF_OBJID));
+
+    final List<ValidationMessage> validationMessages =
+        validator.onRefOperation(testRefReceivedEvent);
+
+    assertThat(validationMessages).isEmpty();
+    verify(dfsRefDatabase)
+        .compareAndPut(
+            eq(PROJECT_NAME), eqRef(REF_NAME, REF_OBJID_OLD), eqRef(REF_NAME, REF_OBJID));
+  }
+
+  @Test
+  public void shouldFailRefUpdateIfRefUpdateInDfsRefDatabaseReturnsFalse() throws Exception {
+    testRefReceivedEvent.command = RECEIVE_COMMAND_UPDATE_REF;
+    doReturn(new TestRef(REF_NAME, REF_OBJID_OLD)).when(localRefDatabase).getRef(REF_NAME);
+    doReturn(false)
+        .when(dfsRefDatabase)
+        .compareAndPut(
+            eq(PROJECT_NAME), eqRef(REF_NAME, REF_OBJID_OLD), eqRef(REF_NAME, REF_OBJID));
+    expectedException.expect(ValidationException.class);
+    expectedException.expectCause(nullValue(Exception.class));
+
+    validator.onRefOperation(testRefReceivedEvent);
+  }
+
+  @Test
+  public void shouldFailRefUpdateIfRefIsNotInDfsRefDatabase() throws Exception {
+    testRefReceivedEvent.command = RECEIVE_COMMAND_UPDATE_REF;
+    doReturn(null).when(localRefDatabase).getRef(REF_NAME);
+
+    expectedException.expect(ValidationException.class);
+    expectedException.expectCause(nullValue(Exception.class));
+
+    validator.onRefOperation(testRefReceivedEvent);
+  }
+
+  @Test
+  public void shouldDeleteRefInDfsDatabaseWhenHandlingRefDeleteEvents() throws Exception {
+    testRefReceivedEvent.command = RECEIVE_COMMAND_DELETE_REF;
+    doReturn(new TestRef(REF_NAME, REF_OBJID_OLD)).when(localRefDatabase).getRef(REF_NAME);
+    doReturn(true)
+        .when(dfsRefDatabase)
+        .compareAndRemove(eq(PROJECT_NAME), eqRef(REF_NAME, REF_OBJID_OLD));
+
+    final List<ValidationMessage> validationMessages =
+        validator.onRefOperation(testRefReceivedEvent);
+
+    assertThat(validationMessages).isEmpty();
+
+    verify(dfsRefDatabase).compareAndRemove(eq(PROJECT_NAME), eqRef(REF_NAME, REF_OBJID_OLD));
+  }
+
+  @Test
+  public void shouldFailRefDeletionIfRefDeletionInDfsRefDatabaseReturnsFalse() throws Exception {
+    testRefReceivedEvent.command = RECEIVE_COMMAND_DELETE_REF;
+    doReturn(new TestRef(REF_NAME, REF_OBJID_OLD)).when(localRefDatabase).getRef(REF_NAME);
+    doReturn(false)
+        .when(dfsRefDatabase)
+        .compareAndRemove(eq(PROJECT_NAME), eqRef(REF_NAME, REF_OBJID_OLD));
+
+    expectedException.expect(ValidationException.class);
+    expectedException.expectCause(nullValue(Exception.class));
+
+    validator.onRefOperation(testRefReceivedEvent);
+  }
+
+  @Test
+  public void shouldFailRefDeletionIfRefIsNotInDfsDatabase() throws Exception {
+    testRefReceivedEvent.command = RECEIVE_COMMAND_DELETE_REF;
+    doReturn(null).when(localRefDatabase).getRef(REF_NAME);
+
+    expectedException.expect(ValidationException.class);
+    expectedException.expectCause(nullValue(Exception.class));
+
+    validator.onRefOperation(testRefReceivedEvent);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationIT.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationIT.java
new file mode 100644
index 0000000..0c6833d
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/validation/ValidationIT.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2019 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.googlesource.gerrit.plugins.multisite.validation;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.LogThreshold;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.inject.AbstractModule;
+import org.junit.Test;
+
+@NoHttpd
+@LogThreshold(level = "INFO")
+@TestPlugin(
+    name = "multi-site",
+    sysModule = "com.googlesource.gerrit.plugins.multisite.validation.ValidationIT$Module")
+public class ValidationIT extends LightweightPluginDaemonTest {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public static class Module extends AbstractModule {
+    @Override
+    protected void configure() {
+      install(new ValidationModule());
+    }
+  }
+
+  @Test
+  public void inSyncChangeValidatorShouldAcceptNewChange() throws Exception {
+    final PushOneCommit.Result change = createChange("refs/for/master");
+
+    change.assertOkStatus();
+  }
+}