diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt index 3266cb143e..8ab3d62bed 100644 --- a/Documentation/dev-plugins.txt +++ b/Documentation/dev-plugins.txt @@ -1281,6 +1281,39 @@ public class MyListener implements GitReferenceUpdatedListener { } ---- +== 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 cab6b58b37..563c2ef9ed 100644 --- a/java/com/google/gerrit/acceptance/EventRecorder.java +++ b/java/com/google/gerrit/acceptance/EventRecorder.java @@ -40,7 +40,7 @@ import org.eclipse.jgit.revwalk.RevCommit; public class EventRecorder { private final RegistrationHandle eventListenerRegistration; - private final ListMultimap recordedEvents; + private final ListMultimap recordedEvents; @Singleton public static class Factory { @@ -79,6 +79,8 @@ public class EventRecorder { refEventKey( event.getType(), event.getProjectNameKey().get(), event.getRefName()); recordedEvents.put(key, event); + } else { + recordedEvents.put(e.type, e); } } @@ -158,6 +160,17 @@ public class EventRecorder { return events; } + public ImmutableList getGenericEvents(String type, int expectedSize) { + if (expectedSize == 0) { + assertThat(recordedEvents).doesNotContainKey(type); + return ImmutableList.of(); + } + assertThat(recordedEvents).containsKey(type); + ImmutableList 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 c07987a29e..4cf4a5a366 100644 --- a/java/com/google/gerrit/server/events/Event.java +++ b/java/com/google/gerrit/server/events/Event.java @@ -19,6 +19,7 @@ import com.google.gerrit.server.util.time.TimeUtil; 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 32b20fbf3d..728dd01e21 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.registration.DynamicItem; 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 @@ public class EventBroker implements EventDispatcher { protected final ChangeNotes.Factory notesFactory; + protected final String gerritInstanceId; + @Inject public EventBroker( PluginSetContext listeners, PluginSetContext 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 @@ public class EventBroker implements EventDispatcher { } protected void fireEvent(Change change, ChangeEvent event) throws PermissionBackendException { + setInstanceId(event); for (PluginSetEntryContext c : listeners) { CurrentUser user = c.call(UserScopedEventListener::getUser); if (isVisibleTo(change, user)) { @@ -115,7 +122,9 @@ public class EventBroker implements EventDispatcher { } protected void fireEvent(Project.NameKey project, ProjectEvent event) { + setInstanceId(event); for (PluginSetEntryContext c : listeners) { + CurrentUser user = c.call(UserScopedEventListener::getUser); if (isVisibleTo(project, user)) { c.run(l -> l.onEvent(event)); @@ -126,6 +135,7 @@ public class EventBroker implements EventDispatcher { protected void fireEvent(BranchNameKey branchName, RefEvent event) throws PermissionBackendException { + setInstanceId(event); for (PluginSetEntryContext c : listeners) { CurrentUser user = c.call(UserScopedEventListener::getUser); if (isVisibleTo(branchName, user)) { @@ -136,6 +146,7 @@ public class EventBroker implements EventDispatcher { } protected void fireEvent(Event event) throws PermissionBackendException { + setInstanceId(event); for (PluginSetEntryContext c : listeners) { CurrentUser user = c.call(UserScopedEventListener::getUser); if (isVisibleTo(event, user)) { @@ -145,6 +156,10 @@ public class EventBroker implements EventDispatcher { fireEventForUnrestrictedListeners(event); } + protected void setInstanceId(Event event) { + event.instanceId = gerritInstanceId; + } + protected boolean isVisibleTo(Project.NameKey project, CurrentUser user) { try { Optional 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 0000000000..a333c3cef0 --- /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; + + @Inject + TestDispatcher(DynamicItem 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; + 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 events = eventRecorder.getGenericEvents(TestEvent.TYPE, 1); + assertThat(events.get(0).instanceId).isEqualTo("testInstanceId"); + } + + @Test + public void shouldNotSetInstanceIdWhenNotDefined() { + testDispatcher.postEvent(new TestEvent()); + + ImmutableList events = eventRecorder.getGenericEvents(TestEvent.TYPE, 1); + assertThat(events.get(0).instanceId).isNull(); + } +}