Merge branch 'stable-2.16'

* stable-2.16:
  Create Dockerised test environment
  EventConsumerIT: Assert on event content
  Check for NoteDb migration when loading/testing multi-site
  Add embrional support for split brain detection
  Log published/consumed messages in message_log
  Fix healthcheck.config file

Change-Id: Ic17380436bde12d7a1af87ec6c0de8fb2dde45b9
diff --git a/README.md b/README.md
index 89f9e78..345fefb 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,9 @@
 - Connected to the same message broker
 - Accessible behind a load balancer (e.g., HAProxy)
 
+**NOTE**: The multi-site plugin will not start if Gerrit is not yet migrated
+to NoteDb.
+
 Currently, the only mode supported is one primary read/write master
 and multiple read-only masters but eventually the plan is to support N
 read/write masters. The read/write master is handling any traffic while the
diff --git a/dockerised_local_env/.gitignore b/dockerised_local_env/.gitignore
new file mode 100644
index 0000000..714b661
--- /dev/null
+++ b/dockerised_local_env/.gitignore
@@ -0,0 +1,16 @@
+gerrit-1/plugins
+gerrit-1/db
+gerrit-1/git
+gerrit-1/logs
+gerrit-1/index
+gerrit-1/data
+gerrit-1/bin
+gerrit-1/etc
+gerrit-2/plugins
+gerrit-2/db
+gerrit-2/git
+gerrit-2/logs
+gerrit-2/index
+gerrit-2/data
+gerrit-2/bin
+gerrit-2/etc
diff --git a/dockerised_local_env/Makefile b/dockerised_local_env/Makefile
new file mode 100644
index 0000000..c76ab65
--- /dev/null
+++ b/dockerised_local_env/Makefile
@@ -0,0 +1,59 @@
+GERRIT_JOB=Gerrit-bazel-stable-2.16
+BUILD_NUM=259
+GERRIT_1_PLUGINS_DIRECTORY=./gerrit-1/plugins
+GERRIT_2_PLUGINS_DIRECTORY=./gerrit-2/plugins
+GERRIT_1_BIN_DIRECTORY=./gerrit-1/bin
+GERRIT_2_BIN_DIRECTORY=./gerrit-2/bin
+GERRIT_1_PLUGINS_DIRECTORY=./gerrit-1/plugins
+GERRIT_2_PLUGINS_DIRECTORY=./gerrit-2/plugins
+CORE_PLUGINS=replication
+CI_URL=https://gerrit-ci.gerritforge.com/job
+MYDIR=$(shell basename $(shell pwd))
+WGET=wget -N -q
+
+all: download build
+
+download: gerrit plugin_websession_flatfile \
+	plugin_healthcheck \
+	plugin_multi_site
+
+
+gerrit:
+	-mkdir -p $(GERRIT_1_PLUGINS_DIRECTORY)
+	-mkdir -p $(GERRIT_2_PLUGINS_DIRECTORY)
+	$(WGET) $(CI_URL)/$(GERRIT_JOB)/lastSuccessfulBuild/artifact/gerrit/bazel-bin/release.war -P $(GERRIT_1_BIN_DIRECTORY)
+	cp $(GERRIT_1_BIN_DIRECTORY)/*.war $(GERRIT_2_BIN_DIRECTORY)
+	for plugin in $(CORE_PLUGINS); do $(WGET) $(CI_URL)/$(GERRIT_JOB)/$(BUILD_NUM)/artifact/gerrit/bazel-genfiles/plugins/$$plugin/$$plugin.jar -P $(GERRIT_1_PLUGINS_DIRECTORY); done
+	cp $(GERRIT_1_PLUGINS_DIRECTORY)/*.jar $(GERRIT_2_PLUGINS_DIRECTORY)
+
+plugin_websession_flatfile:
+	$(WGET) $(CI_URL)/plugin-websession-flatfile-bazel-master-stable-2.16/lastSuccessfulBuild/artifact/bazel-genfiles/plugins/websession-flatfile/websession-flatfile.jar -P $(GERRIT_1_PLUGINS_DIRECTORY)
+	cp $(GERRIT_1_PLUGINS_DIRECTORY)/websession-flatfile.jar $(GERRIT_2_PLUGINS_DIRECTORY)/websession-flatfile.jar
+
+plugin_multi_site:
+	$(WGET) $(CI_URL)/plugin-multi-site-bazel-stable-2.16/lastSuccessfulBuild/artifact/bazel-genfiles/plugins/multi-site/multi-site.jar -P $(GERRIT_1_PLUGINS_DIRECTORY)
+	cp $(GERRIT_1_PLUGINS_DIRECTORY)/multi-site.jar $(GERRIT_2_PLUGINS_DIRECTORY)/multi-site.jar
+
+plugin_healthcheck:
+	$(WGET) $(CI_URL)/plugin-healthcheck-bazel-stable-2.16/lastSuccessfulBuild/artifact/bazel-genfiles/plugins/healthcheck/healthcheck.jar -P $(GERRIT_1_PLUGINS_DIRECTORY)
+	cp $(GERRIT_1_PLUGINS_DIRECTORY)/healthcheck.jar $(GERRIT_2_PLUGINS_DIRECTORY)/healthcheck.jar
+
+build:
+	docker build -t $(MYDIR) ./gerrit-1
+	docker build -t $(MYDIR) ./gerrit-2
+clean_gerrit:
+	rm -fr gerrit-1/db/ gerrit-1/data/ gerrit-1/cache/ gerrit-1/db/ gerrit-1/git/ gerrit-1/indexes/ gerrit-1/etc/
+	rm -fr gerrit-2/db/ gerrit-2/data/ gerrit-2/cache/ gerrit-2/db/ gerrit-2/git/ gerrit-2/indexes/ gerrit-1/etc/
+	-mkdir -p gerrit-{1,2}/etc/
+	export GERRIT_REPLICATION_INSTANCE=gerrit-2; cat ./gerrit-common/replication.config.template | envsubst '$${GERRIT_REPLICATION_INSTANCE}'  > ./gerrit-1/etc/replication.config
+	export GERRIT_REPLICATION_INSTANCE=gerrit-1; cat ./gerrit-common/replication.config.template | envsubst '$${GERRIT_REPLICATION_INSTANCE}'  > ./gerrit-2/etc/replication.config
+	cp ./gerrit-common/gerrit.config ./gerrit-1/etc
+	cp ./gerrit-common/gerrit.config ./gerrit-2/etc
+	cp ./gerrit-common/git-daemon.sh ./gerrit-1/bin
+	cp ./gerrit-common/git-daemon.sh ./gerrit-2/bin
+
+init_gerrit:
+	docker-compose down && docker-compose build gerrit-1 && docker-compose build gerrit-2 && docker-compose up -d gerrit-1 && docker-compose up -d gerrit-2
+
+init_all: clean_gerrit download
+		docker-compose down && docker-compose build && docker-compose up -d
diff --git a/dockerised_local_env/README.md b/dockerised_local_env/README.md
new file mode 100644
index 0000000..bf7d8e0
--- /dev/null
+++ b/dockerised_local_env/README.md
@@ -0,0 +1,17 @@
+# Dockerised test environment
+
+The docker compose provided in this directory is meant to orchestrate the spin up
+of a dockerised test environment with the latest stable Gerrit version.
+Run it with:
+
+```bash
+make init_all
+```
+
+The spin up will take a while, check what is going on with:
+
+```bash
+docker-compose logs -f
+```
+
+*NOTE:* If you want to run any ssh command as admin you can use the ssh keys into the *gerrit-{1,2}/ssh* directory.
diff --git a/dockerised_local_env/docker-compose.yaml b/dockerised_local_env/docker-compose.yaml
new file mode 100644
index 0000000..4e66ec9
--- /dev/null
+++ b/dockerised_local_env/docker-compose.yaml
@@ -0,0 +1,44 @@
+version: '3'
+services:
+  gerrit-1:
+    build: ./gerrit-1
+    networks:
+      gerrit-net:
+    volumes:
+       - ./gerrit-1/git:/var/gerrit/git
+       - ./gerrit-1/logs:/var/gerrit/logs
+       - ./gerrit-1/ssh:/var/gerrit/.ssh
+       - ./gerrit-1/index:/var/gerrit/index
+       - ./gerrit-1/data:/var/gerrit/data
+    ports:
+       - "29418:29418"
+       - "8080:8080"
+    depends_on:
+      - gerrit-2
+      - sshd
+  gerrit-2:
+    build: ./gerrit-2
+    networks:
+      gerrit-net:
+    volumes:
+       - ./gerrit-2/git:/var/gerrit/git
+       - ./gerrit-2/logs:/var/gerrit/logs
+       - ./gerrit-2/ssh:/var/gerrit/.ssh
+       - ./gerrit-2/index:/var/gerrit/index
+       - ./gerrit-2/data:/var/gerrit/data
+    ports:
+       - "39418:29418"
+       - "8081:8080"
+    depends_on:
+      - sshd
+  sshd:
+    build: ./sshd
+    networks:
+      gerrit-net:
+    volumes:
+       - ./gerrit-2/git:/var/gerrit-2/git
+       - ./gerrit-2/ssh:/root/.ssh
+       - ./gerrit-1/git:/var/gerrit-1/git
+networks:
+  gerrit-net:
+    driver: bridge
diff --git a/dockerised_local_env/gerrit-1/Dockerfile b/dockerised_local_env/gerrit-1/Dockerfile
new file mode 100644
index 0000000..d71b6c2
--- /dev/null
+++ b/dockerised_local_env/gerrit-1/Dockerfile
@@ -0,0 +1,21 @@
+FROM openjdk:8-jdk-alpine
+
+RUN adduser -D -h /var/gerrit gerrit && \
+    apk update && apk upgrade && \
+    apk add --no-cache bash git openssh netcat-openbsd curl ttf-dejavu && \
+    apk add --no-cache git-daemon
+
+COPY --chown=gerrit:gerrit bin/release.war /var/gerrit/bin/gerrit.war
+COPY --chown=gerrit:gerrit plugins/* /var/gerrit/plugins/
+COPY --chown=gerrit:gerrit etc /var/gerrit/etc
+
+ADD bin/git-daemon.sh /usr/local/bin/git-daemon.sh
+RUN chmod +x /usr/local/bin/git-daemon.sh
+
+USER gerrit
+
+WORKDIR /var/gerrit
+
+COPY docker-entrypoint.sh /bin
+
+ENTRYPOINT /bin/docker-entrypoint.sh
diff --git a/dockerised_local_env/gerrit-1/docker-entrypoint.sh b/dockerised_local_env/gerrit-1/docker-entrypoint.sh
new file mode 100755
index 0000000..5429a52
--- /dev/null
+++ b/dockerised_local_env/gerrit-1/docker-entrypoint.sh
@@ -0,0 +1,16 @@
+#!/bin/bash -ex
+
+java -Xmx100g -jar /var/gerrit/bin/gerrit.war init -d /var/gerrit --batch --dev --install-all-plugins
+java -Xmx100g -jar /var/gerrit/bin/gerrit.war reindex -d /var/gerrit --index accounts
+java -Xmx100g -jar /var/gerrit/bin/gerrit.war reindex -d /var/gerrit --index groups
+
+echo "Gerrit configuration:"
+cat /var/gerrit/etc/gerrit.config
+echo "Replication plugin configuration:"
+cat /var/gerrit/etc/replication.config
+
+echo "Starting git daemon"
+/usr/local/bin/git-daemon.sh &
+
+sed -i -e 's/\-\-console-log//g' /var/gerrit/bin/gerrit.sh
+/var/gerrit/bin/gerrit.sh run
diff --git a/dockerised_local_env/gerrit-1/ssh/authorized_keys b/dockerised_local_env/gerrit-1/ssh/authorized_keys
new file mode 100644
index 0000000..951a480
--- /dev/null
+++ b/dockerised_local_env/gerrit-1/ssh/authorized_keys
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC4e8vBvNFfv/tkKbS2HnnmVpy+AL0pDRQCLoXy6dqjA+67wjBy8Bexn+iH5YDfvNq89Q//6gJ5vV+uGvhzBWrELPQyuim9hBhKooGf5STzr8qrO5SWyhuiE+L3gBQdqoxgC/Bzb5hAAQinGqCdyQAPaPuEP1nse4MEQTzNRhUhsjHdHAVz4gY12NvXoPIP+1ObHDp5rz1HkwWFAyiQpxhctB6r9SyJRkaaesN8X8q4wEVnK0+zhGawenhfPAzPETiLmg2k5IaHhWG+zsExfrLOoKRvc4EPTHbmyRNlefSciU9c00lrivSfLu4x2864uKaLRMQIEZV9EqgPur+At5nTutodBWz/kvyf19D+RnsG7+jJQVOWbAbQtmjxNbYH6IvnMBgLqQf8MEbTvXdnOF0Q0iCfoyhHrD4IueOmcdUOBkXEpqHn05FUX7/+UY8ZUG64+o4cz0A0g3BypigmI/ksoNAQA/AiehXNmhjI67J8clAsY/s3TJUZE/f8JFg5tO7SVCba65Ja7vlyyBDn6VTSuOYJ2ofzByvdaUxqtV9y8AvE1K/NOWDwNoQ/HGQWTVCBcu1+CM0RsObMuoFzZ/t7MY29tmB5R/nGl99Z/PWTvxrpsQx+TcUEKem3eS4ToYqUn/+5/5Wa3oUP1F4POYgRJh8x0DBJSkEuS44XeMsXHw== your_email@example.com
diff --git a/dockerised_local_env/gerrit-1/ssh/config b/dockerised_local_env/gerrit-1/ssh/config
new file mode 100644
index 0000000..e76bebf
--- /dev/null
+++ b/dockerised_local_env/gerrit-1/ssh/config
@@ -0,0 +1,6 @@
+Host *
+   User root
+   IdentityFile /var/gerrit/.ssh/id_rsa
+   PreferredAuthentications publickey
+   # Not ideal at all. Just a quick workaround to avoid updating the known_hosts at startup
+   StrictHostKeyChecking no
diff --git a/dockerised_local_env/gerrit-1/ssh/id_rsa b/dockerised_local_env/gerrit-1/ssh/id_rsa
new file mode 100644
index 0000000..6535ddf
--- /dev/null
+++ b/dockerised_local_env/gerrit-1/ssh/id_rsa
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/dockerised_local_env/gerrit-1/ssh/id_rsa.pub b/dockerised_local_env/gerrit-1/ssh/id_rsa.pub
new file mode 100644
index 0000000..951a480
--- /dev/null
+++ b/dockerised_local_env/gerrit-1/ssh/id_rsa.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC4e8vBvNFfv/tkKbS2HnnmVpy+AL0pDRQCLoXy6dqjA+67wjBy8Bexn+iH5YDfvNq89Q//6gJ5vV+uGvhzBWrELPQyuim9hBhKooGf5STzr8qrO5SWyhuiE+L3gBQdqoxgC/Bzb5hAAQinGqCdyQAPaPuEP1nse4MEQTzNRhUhsjHdHAVz4gY12NvXoPIP+1ObHDp5rz1HkwWFAyiQpxhctB6r9SyJRkaaesN8X8q4wEVnK0+zhGawenhfPAzPETiLmg2k5IaHhWG+zsExfrLOoKRvc4EPTHbmyRNlefSciU9c00lrivSfLu4x2864uKaLRMQIEZV9EqgPur+At5nTutodBWz/kvyf19D+RnsG7+jJQVOWbAbQtmjxNbYH6IvnMBgLqQf8MEbTvXdnOF0Q0iCfoyhHrD4IueOmcdUOBkXEpqHn05FUX7/+UY8ZUG64+o4cz0A0g3BypigmI/ksoNAQA/AiehXNmhjI67J8clAsY/s3TJUZE/f8JFg5tO7SVCba65Ja7vlyyBDn6VTSuOYJ2ofzByvdaUxqtV9y8AvE1K/NOWDwNoQ/HGQWTVCBcu1+CM0RsObMuoFzZ/t7MY29tmB5R/nGl99Z/PWTvxrpsQx+TcUEKem3eS4ToYqUn/+5/5Wa3oUP1F4POYgRJh8x0DBJSkEuS44XeMsXHw== your_email@example.com
diff --git a/dockerised_local_env/gerrit-1/ssh/known_hosts b/dockerised_local_env/gerrit-1/ssh/known_hosts
new file mode 100644
index 0000000..012fa68
--- /dev/null
+++ b/dockerised_local_env/gerrit-1/ssh/known_hosts
@@ -0,0 +1,2 @@
+
+sshd,* ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKZRTdCgJ0FFTpP3ZzRgMMbYgaNmm6PDlg0e9QtOXzCG63GE41EExz2RWw7K9e6eRz+hSVf4hC/KMPgH3Clgo6w=
diff --git a/dockerised_local_env/gerrit-2/Dockerfile b/dockerised_local_env/gerrit-2/Dockerfile
new file mode 100644
index 0000000..d71b6c2
--- /dev/null
+++ b/dockerised_local_env/gerrit-2/Dockerfile
@@ -0,0 +1,21 @@
+FROM openjdk:8-jdk-alpine
+
+RUN adduser -D -h /var/gerrit gerrit && \
+    apk update && apk upgrade && \
+    apk add --no-cache bash git openssh netcat-openbsd curl ttf-dejavu && \
+    apk add --no-cache git-daemon
+
+COPY --chown=gerrit:gerrit bin/release.war /var/gerrit/bin/gerrit.war
+COPY --chown=gerrit:gerrit plugins/* /var/gerrit/plugins/
+COPY --chown=gerrit:gerrit etc /var/gerrit/etc
+
+ADD bin/git-daemon.sh /usr/local/bin/git-daemon.sh
+RUN chmod +x /usr/local/bin/git-daemon.sh
+
+USER gerrit
+
+WORKDIR /var/gerrit
+
+COPY docker-entrypoint.sh /bin
+
+ENTRYPOINT /bin/docker-entrypoint.sh
diff --git a/dockerised_local_env/gerrit-2/docker-entrypoint.sh b/dockerised_local_env/gerrit-2/docker-entrypoint.sh
new file mode 100755
index 0000000..01f983b
--- /dev/null
+++ b/dockerised_local_env/gerrit-2/docker-entrypoint.sh
@@ -0,0 +1,19 @@
+#!/bin/bash -ex
+
+java -Xmx100g -jar /var/gerrit/bin/gerrit.war init -d /var/gerrit --batch --dev --no-auto-start --install-all-plugins
+#
+echo "Gerrit configuration:"
+cat /var/gerrit/etc/gerrit.config
+echo "Replication plugin configuration:"
+cat /var/gerrit/etc/replication.config
+
+echo "Remove git repos created during init phase"
+rm -fr /var/gerrit/git/*
+
+echo "Starting git daemon"
+/usr/local/bin/git-daemon.sh &
+
+echo "Waiting for initial replication"
+sleep 120
+
+java -Xmx100g -jar /var/gerrit/bin/gerrit.war daemon
diff --git a/dockerised_local_env/gerrit-2/ssh/authorized_keys b/dockerised_local_env/gerrit-2/ssh/authorized_keys
new file mode 100644
index 0000000..951a480
--- /dev/null
+++ b/dockerised_local_env/gerrit-2/ssh/authorized_keys
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC4e8vBvNFfv/tkKbS2HnnmVpy+AL0pDRQCLoXy6dqjA+67wjBy8Bexn+iH5YDfvNq89Q//6gJ5vV+uGvhzBWrELPQyuim9hBhKooGf5STzr8qrO5SWyhuiE+L3gBQdqoxgC/Bzb5hAAQinGqCdyQAPaPuEP1nse4MEQTzNRhUhsjHdHAVz4gY12NvXoPIP+1ObHDp5rz1HkwWFAyiQpxhctB6r9SyJRkaaesN8X8q4wEVnK0+zhGawenhfPAzPETiLmg2k5IaHhWG+zsExfrLOoKRvc4EPTHbmyRNlefSciU9c00lrivSfLu4x2864uKaLRMQIEZV9EqgPur+At5nTutodBWz/kvyf19D+RnsG7+jJQVOWbAbQtmjxNbYH6IvnMBgLqQf8MEbTvXdnOF0Q0iCfoyhHrD4IueOmcdUOBkXEpqHn05FUX7/+UY8ZUG64+o4cz0A0g3BypigmI/ksoNAQA/AiehXNmhjI67J8clAsY/s3TJUZE/f8JFg5tO7SVCba65Ja7vlyyBDn6VTSuOYJ2ofzByvdaUxqtV9y8AvE1K/NOWDwNoQ/HGQWTVCBcu1+CM0RsObMuoFzZ/t7MY29tmB5R/nGl99Z/PWTvxrpsQx+TcUEKem3eS4ToYqUn/+5/5Wa3oUP1F4POYgRJh8x0DBJSkEuS44XeMsXHw== your_email@example.com
diff --git a/dockerised_local_env/gerrit-2/ssh/config b/dockerised_local_env/gerrit-2/ssh/config
new file mode 100644
index 0000000..e76bebf
--- /dev/null
+++ b/dockerised_local_env/gerrit-2/ssh/config
@@ -0,0 +1,6 @@
+Host *
+   User root
+   IdentityFile /var/gerrit/.ssh/id_rsa
+   PreferredAuthentications publickey
+   # Not ideal at all. Just a quick workaround to avoid updating the known_hosts at startup
+   StrictHostKeyChecking no
diff --git a/dockerised_local_env/gerrit-2/ssh/id_rsa b/dockerised_local_env/gerrit-2/ssh/id_rsa
new file mode 100644
index 0000000..6535ddf
--- /dev/null
+++ b/dockerised_local_env/gerrit-2/ssh/id_rsa
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/dockerised_local_env/gerrit-2/ssh/id_rsa.pub b/dockerised_local_env/gerrit-2/ssh/id_rsa.pub
new file mode 100644
index 0000000..951a480
--- /dev/null
+++ b/dockerised_local_env/gerrit-2/ssh/id_rsa.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC4e8vBvNFfv/tkKbS2HnnmVpy+AL0pDRQCLoXy6dqjA+67wjBy8Bexn+iH5YDfvNq89Q//6gJ5vV+uGvhzBWrELPQyuim9hBhKooGf5STzr8qrO5SWyhuiE+L3gBQdqoxgC/Bzb5hAAQinGqCdyQAPaPuEP1nse4MEQTzNRhUhsjHdHAVz4gY12NvXoPIP+1ObHDp5rz1HkwWFAyiQpxhctB6r9SyJRkaaesN8X8q4wEVnK0+zhGawenhfPAzPETiLmg2k5IaHhWG+zsExfrLOoKRvc4EPTHbmyRNlefSciU9c00lrivSfLu4x2864uKaLRMQIEZV9EqgPur+At5nTutodBWz/kvyf19D+RnsG7+jJQVOWbAbQtmjxNbYH6IvnMBgLqQf8MEbTvXdnOF0Q0iCfoyhHrD4IueOmcdUOBkXEpqHn05FUX7/+UY8ZUG64+o4cz0A0g3BypigmI/ksoNAQA/AiehXNmhjI67J8clAsY/s3TJUZE/f8JFg5tO7SVCba65Ja7vlyyBDn6VTSuOYJ2ofzByvdaUxqtV9y8AvE1K/NOWDwNoQ/HGQWTVCBcu1+CM0RsObMuoFzZ/t7MY29tmB5R/nGl99Z/PWTvxrpsQx+TcUEKem3eS4ToYqUn/+5/5Wa3oUP1F4POYgRJh8x0DBJSkEuS44XeMsXHw== your_email@example.com
diff --git a/dockerised_local_env/gerrit-2/ssh/known_hosts b/dockerised_local_env/gerrit-2/ssh/known_hosts
new file mode 100644
index 0000000..c6f2bdd
--- /dev/null
+++ b/dockerised_local_env/gerrit-2/ssh/known_hosts
@@ -0,0 +1,2 @@
+[localhost]:39418 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHAfiYyOe3Df8h+nT1axmB5F4cQQg/qnzvBEJsfKHt3uCYjkOLjjadYjujCnkzb74LToaw4pToTfAnCJ42jw5Bk=
+[gerrit-1]:22 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHAfiYyOe3Df8h+nT1axmB5F4cQQg/qnzvBEJsfKHt3uCYjkOLjjadYjujCnkzb74LToaw4pToTfAnCJ42jw5Bk=
diff --git a/dockerised_local_env/gerrit-common/gerrit.config b/dockerised_local_env/gerrit-common/gerrit.config
new file mode 100644
index 0000000..c21242f
--- /dev/null
+++ b/dockerised_local_env/gerrit-common/gerrit.config
@@ -0,0 +1,34 @@
+[gerrit]
+	basePath = git
+	serverId = ff17821f-9571-42df-b690-30660f2d6e20
+	canonicalWebUrl = http://localhost:8080/
+[database]
+	type = h2
+	database = /var/gerrit/db-volume-1/db/ReviewDB
+[noteDb "changes"]
+	disableReviewDb = true
+	primaryStorage = note db
+	read = true
+	sequence = true
+	write = true
+[container]
+	javaOptions = "-Dflogger.backend_factory=com.google.common.flogger.backend.log4j.Log4jBackendFactory#getInstance"
+	javaOptions = "-Dflogger.logging_context=com.google.gerrit.server.logging.LoggingContext#getInstance"
+	javaHome = /usr/lib/jvm/java-1.8-openjdk/jre
+	user = gerrit
+[index]
+	type = LUCENE
+[auth]
+	type = DEVELOPMENT_BECOME_ANY_ACCOUNT
+[receive]
+	enableSignedPush = false
+[sendemail]
+	smtpServer = localhost
+[sshd]
+	listenAddress = *:29418
+[httpd]
+	listenUrl = http://*:8080/
+[cache]
+	directory = cache
+[plugins]
+	allowRemoteAdmin = true
diff --git a/dockerised_local_env/gerrit-common/git-daemon.sh b/dockerised_local_env/gerrit-common/git-daemon.sh
new file mode 100644
index 0000000..a54c8e0
--- /dev/null
+++ b/dockerised_local_env/gerrit-common/git-daemon.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+git daemon --verbose --enable=receive-pack --base-path=/var/gerrit/git --export-all
diff --git a/dockerised_local_env/gerrit-common/replication.config.template b/dockerised_local_env/gerrit-common/replication.config.template
new file mode 100644
index 0000000..537e8d8
--- /dev/null
+++ b/dockerised_local_env/gerrit-common/replication.config.template
@@ -0,0 +1,17 @@
+[remote "Replication"]
+    url = git://${GERRIT_REPLICATION_INSTANCE}:9418/${name}.git
+    adminUrl = ssh://root@sshd:22/var/${GERRIT_REPLICATION_INSTANCE}/git/${name}.git
+    push = +refs/*:refs/*
+    timeout = 600
+    rescheduleDelay = 15
+    replicationDelay = 5
+    mirror = true
+    createMissingRepositories = true
+    replicateProjectDeletions = true
+    replicateHiddenProjects = true
+[gerrit]
+    autoReload = true
+    replicateOnStartup = true
+[replication]
+    lockErrorMaxRetries = 5
+    maxRetries = 5
diff --git a/dockerised_local_env/sshd/Dockerfile b/dockerised_local_env/sshd/Dockerfile
new file mode 100644
index 0000000..d1065cd
--- /dev/null
+++ b/dockerised_local_env/sshd/Dockerfile
@@ -0,0 +1,6 @@
+FROM sickp/alpine-sshd:6.8
+
+RUN apk update && apk upgrade && \
+    apk add --no-cache bash git netcat-openbsd curl
+
+COPY sshd_config /etc/ssh/sshd_config
diff --git a/dockerised_local_env/sshd/sshd_config b/dockerised_local_env/sshd/sshd_config
new file mode 100644
index 0000000..c40561f
--- /dev/null
+++ b/dockerised_local_env/sshd/sshd_config
@@ -0,0 +1,16 @@
+Port 22
+HostKey /etc/ssh/ssh_host_rsa_key
+HostKey /etc/ssh/ssh_host_ecdsa_key
+HostKey /etc/ssh/ssh_host_ed25519_key
+SyslogFacility AUTHPRIV
+LogLevel INFO
+PermitRootLogin yes
+AuthorizedKeysFile	/root/.ssh/authorized_keys
+PasswordAuthentication no
+ChallengeResponseAuthentication no
+UsePAM yes
+AcceptEnv LANG LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY LC_MESSAGES
+AcceptEnv LC_PAPER LC_NAME LC_ADDRESS LC_TELEPHONE LC_MEASUREMENT
+AcceptEnv LC_IDENTIFICATION LC_ALL LANGUAGE
+AcceptEnv XMODIFIERS
+Subsystem	sftp	/usr/libexec/openssh/sftp-server
diff --git a/setup_local_env/configs/healthcheck.config b/setup_local_env/configs/healthcheck.config
index 69f87f7..1eef4b8 100644
--- a/setup_local_env/configs/healthcheck.config
+++ b/setup_local_env/configs/healthcheck.config
@@ -2,8 +2,8 @@
   timeout = 10s
 
 [healthcheck "querychanges"]
-  query = status:open OR status:merged OR status:abandoned
-  limit = 0 # there are no changes
+  # No changes available when Gerrit is installed from scratch
+  enabled = false
 
 [healthcheck "auth"]
-  userame = "admin"
\ No newline at end of file
+  username = "admin"
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/DisabledMessageLogger.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/DisabledMessageLogger.java
new file mode 100644
index 0000000..258314f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/DisabledMessageLogger.java
@@ -0,0 +1,23 @@
+// 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;
+
+import com.googlesource.gerrit.plugins.multisite.kafka.consumer.SourceAwareEventWrapper;
+
+public class DisabledMessageLogger implements MessageLogger {
+
+  @Override
+  public void log(Direction direction, SourceAwareEventWrapper event) {}
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/Log4jMessageLogger.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/Log4jMessageLogger.java
new file mode 100644
index 0000000..3070c2a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/Log4jMessageLogger.java
@@ -0,0 +1,42 @@
+// 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;
+
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.server.util.PluginLogFile;
+import com.google.gerrit.server.util.SystemLog;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.multisite.kafka.consumer.SourceAwareEventWrapper;
+import org.apache.log4j.PatternLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class Log4jMessageLogger extends PluginLogFile implements MessageLogger {
+  private static final String LOG_NAME = "message_log";
+  private final Logger msgLog;
+
+  @Inject
+  public Log4jMessageLogger(SystemLog systemLog, ServerInformation serverInfo) {
+    super(systemLog, serverInfo, LOG_NAME, new PatternLayout("[%d{ISO8601}] [%t] %-5p : %m%n"));
+    msgLog = LoggerFactory.getLogger(LOG_NAME);
+  }
+
+  @Override
+  public void log(Direction direction, SourceAwareEventWrapper event) {
+    msgLog.info("{} Header[{}] Body[{}]", direction, event.getHeader(), event.getBody());
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/MessageLogger.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/MessageLogger.java
new file mode 100644
index 0000000..8c8d949
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/MessageLogger.java
@@ -0,0 +1,27 @@
+// 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;
+
+import com.googlesource.gerrit.plugins.multisite.kafka.consumer.SourceAwareEventWrapper;
+
+public interface MessageLogger {
+
+  public enum Direction {
+    PUBLISH,
+    CONSUME;
+  }
+
+  public void log(Direction direction, SourceAwareEventWrapper event);
+}
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 0a0fe98..0cd3f8a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/Module.java
@@ -15,8 +15,8 @@
 package com.googlesource.gerrit.plugins.multisite;
 
 import com.google.gerrit.extensions.annotations.PluginData;
+import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gson.Gson;
-import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
@@ -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;
@@ -38,7 +39,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class Module extends AbstractModule {
+public class Module extends LifecycleModule {
   private static final Logger log = LoggerFactory.getLogger(Module.class);
   private final Configuration config;
 
@@ -49,6 +50,8 @@
 
   @Override
   protected void configure() {
+    listener().to(Log4jMessageLogger.class);
+    bind(MessageLogger.class).to(Log4jMessageLogger.class);
 
     install(new ForwarderModule());
 
@@ -70,6 +73,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/broker/BrokerPublisher.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/broker/BrokerPublisher.java
index 11310b5..cce9cc5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/broker/BrokerPublisher.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/broker/BrokerPublisher.java
@@ -22,6 +22,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.multisite.InstanceId;
+import com.googlesource.gerrit.plugins.multisite.MessageLogger;
+import com.googlesource.gerrit.plugins.multisite.MessageLogger.Direction;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.EventFamily;
 import com.googlesource.gerrit.plugins.multisite.kafka.consumer.SourceAwareEventWrapper;
 import java.util.UUID;
@@ -35,12 +37,15 @@
   private final BrokerSession session;
   private final Gson gson;
   private final UUID instanceId;
+  private final MessageLogger msgLog;
 
   @Inject
-  public BrokerPublisher(BrokerSession session, Gson gson, @InstanceId UUID instanceId) {
+  public BrokerPublisher(
+      BrokerSession session, Gson gson, @InstanceId UUID instanceId, MessageLogger msgLog) {
     this.session = session;
     this.gson = gson;
     this.instanceId = instanceId;
+    this.msgLog = msgLog;
   }
 
   @Override
@@ -58,11 +63,13 @@
   }
 
   public boolean publishEvent(EventFamily eventType, Event event) {
-    return session.publishEvent(eventType, getPayload(event));
+    SourceAwareEventWrapper brokerEvent = toBrokerEvent(event);
+    msgLog.log(Direction.PUBLISH, brokerEvent);
+    return session.publishEvent(eventType, getPayload(brokerEvent));
   }
 
-  private String getPayload(Event event) {
-    return gson.toJson(toBrokerEvent(event));
+  private String getPayload(SourceAwareEventWrapper event) {
+    return gson.toJson(event);
   }
 
   private SourceAwareEventWrapper toBrokerEvent(Event event) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/AbstractKafkaSubcriber.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/AbstractKafkaSubcriber.java
index ec9ee2c..fa73964 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/AbstractKafkaSubcriber.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/AbstractKafkaSubcriber.java
@@ -24,6 +24,8 @@
 import com.google.inject.Provider;
 import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.InstanceId;
+import com.googlesource.gerrit.plugins.multisite.MessageLogger;
+import com.googlesource.gerrit.plugins.multisite.MessageLogger.Direction;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.EventFamily;
 import com.googlesource.gerrit.plugins.multisite.kafka.router.ForwardedEventRouter;
 import java.io.IOException;
@@ -50,6 +52,7 @@
   private final Deserializer<SourceAwareEventWrapper> valueDeserializer;
   private final Configuration configuration;
   private final OneOffRequestContext oneOffCtx;
+  private final MessageLogger msgLog;
 
   public AbstractKafkaSubcriber(
       Configuration configuration,
@@ -59,13 +62,15 @@
       DynamicSet<DroppedEventListener> droppedEventListeners,
       Provider<Gson> gsonProvider,
       @InstanceId UUID instanceId,
-      OneOffRequestContext oneOffCtx) {
+      OneOffRequestContext oneOffCtx,
+      MessageLogger msgLog) {
     this.configuration = configuration;
     this.eventRouter = eventRouter;
     this.droppedEventListeners = droppedEventListeners;
     this.gsonProvider = gsonProvider;
     this.instanceId = instanceId;
     this.oneOffCtx = oneOffCtx;
+    this.msgLog = msgLog;
     final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader();
     try {
       Thread.currentThread().setContextClassLoader(AbstractKafkaSubcriber.class.getClassLoader());
@@ -116,7 +121,7 @@
         droppedEventListeners.forEach(l -> l.onEventDropped(event));
       } else {
         try {
-          logger.atInfo().log("Header[%s] Body[%s]", event.getHeader(), event.getBody());
+          msgLog.log(Direction.CONSUME, event);
           eventRouter.route(event.getEventBody(gsonProvider));
         } catch (IOException e) {
           logger.atSevere().withCause(e).log(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/CacheEvictionEventSubscriber.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/CacheEvictionEventSubscriber.java
index 01511be..0e33c00 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/CacheEvictionEventSubscriber.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/CacheEvictionEventSubscriber.java
@@ -22,6 +22,7 @@
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.InstanceId;
+import com.googlesource.gerrit.plugins.multisite.MessageLogger;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.EventFamily;
 import com.googlesource.gerrit.plugins.multisite.kafka.router.StreamEventRouter;
 import java.util.UUID;
@@ -38,7 +39,8 @@
       DynamicSet<DroppedEventListener> droppedEventListeners,
       Provider<Gson> gsonProvider,
       @InstanceId UUID instanceId,
-      OneOffRequestContext oneOffCtx) {
+      OneOffRequestContext oneOffCtx,
+      MessageLogger msgLog) {
     super(
         configuration,
         keyDeserializer,
@@ -47,7 +49,8 @@
         droppedEventListeners,
         gsonProvider,
         instanceId,
-        oneOffCtx);
+        oneOffCtx,
+        msgLog);
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/IndexEventSubscriber.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/IndexEventSubscriber.java
index abef7e6..b61d23d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/IndexEventSubscriber.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/IndexEventSubscriber.java
@@ -22,6 +22,7 @@
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.InstanceId;
+import com.googlesource.gerrit.plugins.multisite.MessageLogger;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.EventFamily;
 import com.googlesource.gerrit.plugins.multisite.kafka.router.IndexEventRouter;
 import java.util.UUID;
@@ -38,7 +39,8 @@
       DynamicSet<DroppedEventListener> droppedEventListeners,
       Provider<Gson> gsonProvider,
       @InstanceId UUID instanceId,
-      OneOffRequestContext oneOffCtx) {
+      OneOffRequestContext oneOffCtx,
+      MessageLogger msgLog) {
     super(
         configuration,
         keyDeserializer,
@@ -47,7 +49,8 @@
         droppedEventListeners,
         gsonProvider,
         instanceId,
-        oneOffCtx);
+        oneOffCtx,
+        msgLog);
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/ProjectUpdateEventSubscriber.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/ProjectUpdateEventSubscriber.java
index 648746e..207848e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/ProjectUpdateEventSubscriber.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/ProjectUpdateEventSubscriber.java
@@ -22,6 +22,7 @@
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.InstanceId;
+import com.googlesource.gerrit.plugins.multisite.MessageLogger;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.EventFamily;
 import com.googlesource.gerrit.plugins.multisite.kafka.router.ProjectListUpdateRouter;
 import java.util.UUID;
@@ -38,7 +39,8 @@
       DynamicSet<DroppedEventListener> droppedEventListeners,
       Provider<Gson> gsonProvider,
       @InstanceId UUID instanceId,
-      OneOffRequestContext oneOffCtx) {
+      OneOffRequestContext oneOffCtx,
+      MessageLogger msgLog) {
     super(
         configuration,
         keyDeserializer,
@@ -47,7 +49,8 @@
         droppedEventListeners,
         gsonProvider,
         instanceId,
-        oneOffCtx);
+        oneOffCtx,
+        msgLog);
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/StreamEventSubscriber.java b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/StreamEventSubscriber.java
index 756af54..c55be53 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/StreamEventSubscriber.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/StreamEventSubscriber.java
@@ -22,6 +22,7 @@
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.InstanceId;
+import com.googlesource.gerrit.plugins.multisite.MessageLogger;
 import com.googlesource.gerrit.plugins.multisite.forwarder.events.EventFamily;
 import com.googlesource.gerrit.plugins.multisite.kafka.router.StreamEventRouter;
 import java.util.UUID;
@@ -38,7 +39,8 @@
       DynamicSet<DroppedEventListener> droppedEventListeners,
       Provider<Gson> gsonProvider,
       @InstanceId UUID instanceId,
-      OneOffRequestContext oneOffCtx) {
+      OneOffRequestContext oneOffCtx,
+      MessageLogger msgLog) {
     super(
         configuration,
         keyDeserializer,
@@ -47,7 +49,8 @@
         droppedEventListeners,
         gsonProvider,
         instanceId,
-        oneOffCtx);
+        oneOffCtx,
+        msgLog);
   }
 
   @Override
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/about.md b/src/main/resources/Documentation/about.md
index 9e29e71..980aa06 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -8,7 +8,6 @@
 
 The masters must be:
 
-* migrated to NoteDb
 * connected to the same message broker
 * behind a load balancer (e.g., HAProxy)
 
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/broker/kafka/BrokerPublisherTest.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/broker/kafka/BrokerPublisherTest.java
index 329774e..fe73065 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/broker/kafka/BrokerPublisherTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/broker/kafka/BrokerPublisherTest.java
@@ -27,6 +27,8 @@
 import com.google.gson.Gson;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
+import com.googlesource.gerrit.plugins.multisite.DisabledMessageLogger;
+import com.googlesource.gerrit.plugins.multisite.MessageLogger;
 import com.googlesource.gerrit.plugins.multisite.broker.BrokerPublisher;
 import com.googlesource.gerrit.plugins.multisite.broker.BrokerSession;
 import com.googlesource.gerrit.plugins.multisite.broker.GsonProvider;
@@ -37,11 +39,12 @@
 
 public class BrokerPublisherTest {
   private BrokerPublisher publisher;
+  private MessageLogger NO_MSG_LOG = new DisabledMessageLogger();
   private Gson gson = new GsonProvider().get();
 
   @Before
   public void setUp() {
-    publisher = new BrokerPublisher(new TestBrokerSession(), gson, UUID.randomUUID());
+    publisher = new BrokerPublisher(new TestBrokerSession(), gson, UUID.randomUUID(), NO_MSG_LOG);
   }
 
   @Test
diff --git a/src/test/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/EventConsumerIT.java b/src/test/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/EventConsumerIT.java
index ea00251..74cb04b 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/EventConsumerIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/multisite/kafka/consumer/EventConsumerIT.java
@@ -15,25 +15,40 @@
 package com.googlesource.gerrit.plugins.multisite.kafka.consumer;
 
 import static com.google.common.truth.Truth.assertThat;
+import static java.util.stream.Collectors.toSet;
 
 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.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.data.PatchSetAttribute;
+import com.google.gerrit.server.events.CommentAddedEvent;
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Key;
 import com.google.inject.TypeLiteral;
 import com.googlesource.gerrit.plugins.multisite.Configuration;
 import com.googlesource.gerrit.plugins.multisite.Module;
+import com.googlesource.gerrit.plugins.multisite.broker.GsonProvider;
+import com.googlesource.gerrit.plugins.multisite.forwarder.events.ChangeIndexEvent;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Test;
 import org.testcontainers.containers.KafkaContainer;
 
@@ -44,7 +59,7 @@
     sysModule =
         "com.googlesource.gerrit.plugins.multisite.kafka.consumer.EventConsumerIT$KafkaTestContainerModule")
 public class EventConsumerIT extends LightweightPluginDaemonTest {
-  private static final int QUEUE_POLL_TIMEOUT_MSECS = 30000;
+  private static final int QUEUE_POLL_TIMEOUT_MSECS = 10000;
 
   public static class KafkaTestContainerModule extends LifecycleModule {
 
@@ -88,30 +103,87 @@
     LinkedBlockingQueue<SourceAwareEventWrapper> droppedEventsQueue = captureDroppedEvents();
     drainQueue(droppedEventsQueue);
 
-    createChange();
-    List<String> createdChangeEvents = receiveFromQueue(droppedEventsQueue, 3);
-    assertThat(createdChangeEvents).hasSize(3);
+    ChangeData change = createChange().getChange();
+    String project = change.project().get();
+    int changeNum = change.getId().get();
+    String changeNotesRef = change.notes().getRefName();
+    int patchsetNum = change.currentPatchSet().getPatchSetId();
+    String patchsetRevision = change.currentPatchSet().getRevision().get();
+    String patchsetRef = change.currentPatchSet().getRefName();
 
-    assertThat(createdChangeEvents).contains("change-index");
-    assertThat(createdChangeEvents).contains("ref-updated");
-    assertThat(createdChangeEvents).contains("patchset-created");
+    Map<String, List<Event>> eventsByType = receiveEventsByType(droppedEventsQueue);
+    assertThat(eventsByType.get("change-index"))
+        .containsExactly(createChangeIndexEvent(project, changeNum, getParentCommit(change)));
+
+    assertThat(
+            eventsByType
+                .get("ref-updated")
+                .stream()
+                .map(e -> ((RefUpdatedEvent) e).getRefName())
+                .collect(toSet()))
+        .containsAllOf(
+            changeNotesRef,
+            patchsetRef); // 'refs/sequences/changes' not always updated thus not checked
+
+    List<Event> patchSetCreatedEvents = eventsByType.get("patchset-created");
+    assertThat(patchSetCreatedEvents).hasSize(1);
+    assertPatchSetAttributes(
+        (PatchSetCreatedEvent) patchSetCreatedEvents.get(0),
+        patchsetNum,
+        patchsetRevision,
+        patchsetRef);
+  }
+
+  private void assertPatchSetAttributes(
+      PatchSetCreatedEvent patchSetCreated,
+      int patchsetNum,
+      String patchsetRevision,
+      String patchsetRef) {
+    PatchSetAttribute patchSetAttribute = patchSetCreated.patchSet.get();
+    assertThat(patchSetAttribute.number).isEqualTo(patchsetNum);
+    assertThat(patchSetAttribute.revision).isEqualTo(patchsetRevision);
+    assertThat(patchSetAttribute.ref).isEqualTo(patchsetRef);
   }
 
   @Test
   public void reviewChangeShouldPropagateChangeIndexAndCommentAdded() throws Exception {
     LinkedBlockingQueue<SourceAwareEventWrapper> droppedEventsQueue = captureDroppedEvents();
-    PushOneCommit.Result r = createChange();
+    ChangeData change = createChange().getChange();
+    String project = change.project().get();
+    int changeNum = change.getId().get();
     drainQueue(droppedEventsQueue);
 
     ReviewInput in = ReviewInput.recommend();
     in.message = "LGTM";
-    gApi.changes().id(r.getChangeId()).revision("current").review(in);
+    gApi.changes().id(changeNum).revision("current").review(in);
 
-    List<String> createdChangeEvents = receiveFromQueue(droppedEventsQueue, 2);
+    Map<String, List<Event>> eventsByType = receiveEventsByType(droppedEventsQueue);
 
-    assertThat(createdChangeEvents).hasSize(2);
-    assertThat(createdChangeEvents).contains("change-index");
-    assertThat(createdChangeEvents).contains("comment-added");
+    assertThat(eventsByType.get("change-index"))
+        .containsExactly(createChangeIndexEvent(project, changeNum, getParentCommit(change)));
+
+    List<Event> commentAddedEvents = eventsByType.get("comment-added");
+    assertThat(commentAddedEvents).hasSize(1);
+    assertThat(((CommentAddedEvent) commentAddedEvents.get(0)).comment)
+        .isEqualTo("Patch Set 1: Code-Review+1\n\n" + in.message);
+  }
+
+  private String getParentCommit(ChangeData change) throws Exception {
+    RevCommit parent;
+    try (Repository repo = repoManager.openRepository(change.project());
+        RevWalk walk = new RevWalk(repo)) {
+      RevCommit commit =
+          walk.parseCommit(ObjectId.fromString(change.currentPatchSet().getRevision().get()));
+      parent = commit.getParent(0);
+    }
+    return parent.getId().name();
+  }
+
+  private ChangeIndexEvent createChangeIndexEvent(
+      String projectName, int changeId, String targetSha1) {
+    ChangeIndexEvent event = new ChangeIndexEvent(projectName, changeId, false);
+    event.targetSha = targetSha1;
+    return event;
   }
 
   private LinkedBlockingQueue<SourceAwareEventWrapper> captureDroppedEvents() throws Exception {
@@ -133,23 +205,22 @@
     return droppedEvents;
   }
 
-  private List<String> receiveFromQueue(
-      LinkedBlockingQueue<SourceAwareEventWrapper> queue, int numEvents)
-      throws InterruptedException {
-    List<String> eventsList = new ArrayList<>();
-    for (int i = 0; i < numEvents; i++) {
-      SourceAwareEventWrapper event = queue.poll(QUEUE_POLL_TIMEOUT_MSECS, TimeUnit.MILLISECONDS);
-      if (event != null) {
-        eventsList.add(event.getHeader().getEventType());
-      }
-    }
-    return eventsList;
+  private Map<String, List<Event>> receiveEventsByType(
+      LinkedBlockingQueue<SourceAwareEventWrapper> queue) throws InterruptedException {
+    return drainQueue(queue)
+        .stream()
+        .sorted(Comparator.comparing(e -> e.type))
+        .collect(Collectors.groupingBy(e -> e.type));
   }
 
-  private void drainQueue(LinkedBlockingQueue<SourceAwareEventWrapper> queue)
+  private List<Event> drainQueue(LinkedBlockingQueue<SourceAwareEventWrapper> queue)
       throws InterruptedException {
-    while (queue.poll(QUEUE_POLL_TIMEOUT_MSECS, TimeUnit.MILLISECONDS) != null) {
-      // Just consume the event
+    GsonProvider gsonProvider = plugin.getSysInjector().getInstance(Key.get(GsonProvider.class));
+    SourceAwareEventWrapper event;
+    List<Event> eventsList = new ArrayList<>();
+    while ((event = queue.poll(QUEUE_POLL_TIMEOUT_MSECS, TimeUnit.MILLISECONDS)) != null) {
+      eventsList.add(event.getEventBody(gsonProvider));
     }
+    return eventsList;
   }
 }
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();
+  }
+}