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();
+ }
+}