Propagate instanceId in Events

Add instanceId in the Events payload when defined.
The value won't be present in the JSON payload
if the value is not set in the Gerrit config

Reference design: https://gerrit-review.googlesource.com/c/homepage/+/263710

Feature: Issue 12685
Change-Id: I507db3f0efba5649ad519361f88ee3f60ab27205
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 3266cb1..8ab3d62 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -1281,6 +1281,39 @@
 }
 ----
 
+== Trace Event origin
+
+When plugins are installed in a multi-master setups it can be useful to know
+the Gerrit `instanceId` of the server that has generated an Event.
+
+E.g. A plugin that sends an instance message for every comment on a change may
+want to react only if the event is generated on the local Gerrit master, for
+avoiding duplicating the notifications.
+
+If link:config-gerrit.html[instanceId] is set, each Event will contain its
+origin in the `instanceId` field.
+
+Here and example of ref-updated JSON event payload with `instanceId`:
+
+[source,json]
+---
+{
+  "submitter": {
+    "name": "Administrator",
+    "email": "admin@example.com",
+    "username": "admin"
+  },
+  "refUpdate": {
+    "oldRev": "a69fc95c7aad5ad41c618d31548b8af835d2959a",
+    "newRev": "31da6556d638a74e5370b62f83e8007f94abb7c6",
+    "refName": "refs/changes/01/1/meta",
+    "project": "test"
+  },
+  "type": "ref-updated",
+  "eventCreatedOn": 1588849085,
+  "instanceId": "instance1"
+}
+---
 
 [[capabilities]]
 == Plugin Owned Capabilities
diff --git a/java/com/google/gerrit/acceptance/EventRecorder.java b/java/com/google/gerrit/acceptance/EventRecorder.java
index cab6b58..563c2ef 100644
--- a/java/com/google/gerrit/acceptance/EventRecorder.java
+++ b/java/com/google/gerrit/acceptance/EventRecorder.java
@@ -40,7 +40,7 @@
 
 public class EventRecorder {
   private final RegistrationHandle eventListenerRegistration;
-  private final ListMultimap<String, RefEvent> recordedEvents;
+  private final ListMultimap<String, Event> recordedEvents;
 
   @Singleton
   public static class Factory {
@@ -79,6 +79,8 @@
                       refEventKey(
                           event.getType(), event.getProjectNameKey().get(), event.getRefName());
                   recordedEvents.put(key, event);
+                } else {
+                  recordedEvents.put(e.type, e);
                 }
               }
 
@@ -158,6 +160,17 @@
     return events;
   }
 
+  public ImmutableList<Event> getGenericEvents(String type, int expectedSize) {
+    if (expectedSize == 0) {
+      assertThat(recordedEvents).doesNotContainKey(type);
+      return ImmutableList.of();
+    }
+    assertThat(recordedEvents).containsKey(type);
+    ImmutableList<Event> events = FluentIterable.from(recordedEvents.get(type)).toList();
+    assertThat(events).hasSize(expectedSize);
+    return events;
+  }
+
   public void assertNoRefUpdatedEvents(String project, String branch) throws Exception {
     getRefUpdatedEvents(project, branch, 0);
   }
diff --git a/java/com/google/gerrit/server/events/Event.java b/java/com/google/gerrit/server/events/Event.java
index c07987a..4cf4a5a 100644
--- a/java/com/google/gerrit/server/events/Event.java
+++ b/java/com/google/gerrit/server/events/Event.java
@@ -19,6 +19,7 @@
 public abstract class Event {
   public final String type;
   public long eventCreatedOn = TimeUtil.nowMs() / 1000L;
+  public String instanceId;
 
   protected Event(String type) {
     this.type = type;
diff --git a/java/com/google/gerrit/server/events/EventBroker.java b/java/com/google/gerrit/server/events/EventBroker.java
index 32b20fb..728dd01 100644
--- a/java/com/google/gerrit/server/events/EventBroker.java
+++ b/java/com/google/gerrit/server/events/EventBroker.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
@@ -23,6 +24,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.GerritInstanceId;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -65,18 +67,22 @@
 
   protected final ChangeNotes.Factory notesFactory;
 
+  protected final String gerritInstanceId;
+
   @Inject
   public EventBroker(
       PluginSetContext<UserScopedEventListener> listeners,
       PluginSetContext<EventListener> unrestrictedListeners,
       PermissionBackend permissionBackend,
       ProjectCache projectCache,
-      ChangeNotes.Factory notesFactory) {
+      ChangeNotes.Factory notesFactory,
+      @Nullable @GerritInstanceId String gerritInstanceId) {
     this.listeners = listeners;
     this.unrestrictedListeners = unrestrictedListeners;
     this.permissionBackend = permissionBackend;
     this.projectCache = projectCache;
     this.notesFactory = notesFactory;
+    this.gerritInstanceId = gerritInstanceId;
   }
 
   @Override
@@ -105,6 +111,7 @@
   }
 
   protected void fireEvent(Change change, ChangeEvent event) throws PermissionBackendException {
+    setInstanceId(event);
     for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
       CurrentUser user = c.call(UserScopedEventListener::getUser);
       if (isVisibleTo(change, user)) {
@@ -115,7 +122,9 @@
   }
 
   protected void fireEvent(Project.NameKey project, ProjectEvent event) {
+    setInstanceId(event);
     for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
+
       CurrentUser user = c.call(UserScopedEventListener::getUser);
       if (isVisibleTo(project, user)) {
         c.run(l -> l.onEvent(event));
@@ -126,6 +135,7 @@
 
   protected void fireEvent(BranchNameKey branchName, RefEvent event)
       throws PermissionBackendException {
+    setInstanceId(event);
     for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
       CurrentUser user = c.call(UserScopedEventListener::getUser);
       if (isVisibleTo(branchName, user)) {
@@ -136,6 +146,7 @@
   }
 
   protected void fireEvent(Event event) throws PermissionBackendException {
+    setInstanceId(event);
     for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
       CurrentUser user = c.call(UserScopedEventListener::getUser);
       if (isVisibleTo(event, user)) {
@@ -145,6 +156,10 @@
     fireEventForUnrestrictedListeners(event);
   }
 
+  protected void setInstanceId(Event event) {
+    event.instanceId = gerritInstanceId;
+  }
+
   protected boolean isVisibleTo(Project.NameKey project, CurrentUser user) {
     try {
       Optional<ProjectState> state = projectCache.get(project);
diff --git a/javatests/com/google/gerrit/acceptance/server/event/InstanceIdInEventIT.java b/javatests/com/google/gerrit/acceptance/server/event/InstanceIdInEventIT.java
new file mode 100644
index 0000000..a333c3c
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/event/InstanceIdInEventIT.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2020 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.google.gerrit.acceptance.server.event;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventDispatcher;
+import com.google.gerrit.server.events.EventTypes;
+import com.google.inject.Inject;
+import org.junit.Before;
+import org.junit.Test;
+
+@NoHttpd
+public class InstanceIdInEventIT extends AbstractDaemonTest {
+
+  public static class TestDispatcher {
+    private final DynamicItem<EventDispatcher> eventDispatcher;
+
+    @Inject
+    TestDispatcher(DynamicItem<EventDispatcher> eventDispatcher) {
+      this.eventDispatcher = eventDispatcher;
+    }
+
+    public void postEvent(TestEvent event) {
+      try {
+        eventDispatcher.get().postEvent(event);
+      } catch (Exception e) {
+        fail("Exception raised when posting Event " + e.getCause());
+      }
+    }
+  }
+
+  public static class TestEvent extends Event {
+    private static final String TYPE = "test-event-instance-id";
+
+    public TestEvent() {
+      super(TYPE);
+    }
+  }
+
+  @Inject private DynamicItem<EventDispatcher> eventDispatcher;
+  TestDispatcher testDispatcher;
+
+  @Before
+  public void setUp() throws Exception {
+    testDispatcher = new TestDispatcher(eventDispatcher);
+    EventTypes.register(TestEvent.TYPE, TestEvent.class);
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = "testInstanceId")
+  public void shouldSetInstanceIdWhenDefined() {
+    testDispatcher.postEvent(new TestEvent());
+
+    ImmutableList<Event> events = eventRecorder.getGenericEvents(TestEvent.TYPE, 1);
+    assertThat(events.get(0).instanceId).isEqualTo("testInstanceId");
+  }
+
+  @Test
+  public void shouldNotSetInstanceIdWhenNotDefined() {
+    testDispatcher.postEvent(new TestEvent());
+
+    ImmutableList<Event> events = eventRecorder.getGenericEvents(TestEvent.TYPE, 1);
+    assertThat(events.get(0).instanceId).isNull();
+  }
+}