This is an automated email from the ASF dual-hosted git repository.

chibenwa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit cd70ea729d4cafea9751443232d538b03610bb51
Author: Benoit TELLIER <[email protected]>
AuthorDate: Wed Sep 2 15:34:50 2026 +0200

    [PERF] eventbus.publishOnNoGroups: skip group publishing when no group is 
registered
---
 .../backends/rabbitmq/RabbitMQConfiguration.java   | 27 ++++++--
 .../rabbitmq/RabbitMQConfigurationTest.java        | 26 ++++++++
 .../servers/partials/configure/rabbitmq.adoc       |  6 +-
 .../org/apache/james/events/EventDispatcher.java   | 16 ++++-
 .../org/apache/james/events/RabbitMQEventBus.java  |  4 +-
 .../apache/james/events/RabbitMQEventBusTest.java  | 75 ++++++++++++++++++++++
 6 files changed, 146 insertions(+), 8 deletions(-)

diff --git 
a/backends-common/rabbitmq/src/main/java/org/apache/james/backends/rabbitmq/RabbitMQConfiguration.java
 
b/backends-common/rabbitmq/src/main/java/org/apache/james/backends/rabbitmq/RabbitMQConfiguration.java
index 645ea32648..42715eeb63 100644
--- 
a/backends-common/rabbitmq/src/main/java/org/apache/james/backends/rabbitmq/RabbitMQConfiguration.java
+++ 
b/backends-common/rabbitmq/src/main/java/org/apache/james/backends/rabbitmq/RabbitMQConfiguration.java
@@ -313,6 +313,7 @@ public class RabbitMQConfiguration {
     private static final String EVENT_BUS_NOTIFICATION_QUEUE_AUTO_DELETE = 
"notification.queue.autoDelete";
     private static final String EVENT_BUS_PUBLISH_CONFIRM_ENABLED = 
"event.bus.publish.confirm.enabled";
     private static final String EVENT_BUS_PROPAGATE_DISPATCH_ERROR = 
"event.bus.propagate.dispatch.error";
+    private static final String EVENT_BUS_PUBLISH_ON_NO_GROUPS = 
"eventbus.publishOnNoGroups";
     private static final String TASK_QUEUE_CONSUMER_TIMEOUT = 
"task.queue.consumer.timeout";
     private static final String VHOST = "vhost";
 
@@ -415,6 +416,7 @@ public class RabbitMQConfiguration {
         private Optional<Boolean> eventBusNotificationDurabilityEnabled;
         private Optional<Boolean> eventBusNotificationQueueAutoDelete;
         private Optional<Boolean> eventBusPropagateDispatchError;
+        private Optional<Boolean> eventBusPublishOnNoGroups;
         private Optional<String> vhost;
         private Optional<Duration> taskQueueConsumerTimeout;
 
@@ -443,6 +445,7 @@ public class RabbitMQConfiguration {
             this.vhost = Optional.empty();
             this.taskQueueConsumerTimeout = Optional.empty();
             this.eventBusPropagateDispatchError = Optional.empty();
+            this.eventBusPublishOnNoGroups = Optional.empty();
         }
 
         public Builder maxRetries(int maxRetries) {
@@ -535,6 +538,11 @@ public class RabbitMQConfiguration {
             return this;
         }
 
+        public Builder eventBusPublishOnNoGroups(Boolean 
eventBusPublishOnNoGroups) {
+            this.eventBusPublishOnNoGroups = 
Optional.ofNullable(eventBusPublishOnNoGroups);
+            return this;
+        }
+
         public Builder eventBusPropagateDispatchError(Boolean 
eventBusPropagateDispatchError) {
             this.eventBusPropagateDispatchError = 
Optional.ofNullable(eventBusPropagateDispatchError);
             return this;
@@ -598,7 +606,8 @@ public class RabbitMQConfiguration {
                     
eventBusNotificationQueueAutoDelete.orElse(Constants.AUTO_DELETE),
                     vhost,
                     
taskQueueConsumerTimeout.orElse(DEFAULT_TASK_QUEUE_CONSUMER_TIMEOUT),
-                    eventBusPropagateDispatchError.orElse(true));
+                    eventBusPropagateDispatchError.orElse(true),
+                    eventBusPublishOnNoGroups.orElse(true));
         }
 
         private List<Host> hostsDefaultingToUri() {
@@ -675,6 +684,7 @@ public class RabbitMQConfiguration {
             
.eventBusNotificationQueueAutoDelete(configuration.getBoolean(EVENT_BUS_NOTIFICATION_QUEUE_AUTO_DELETE,
 null))
             
.eventBusPublishConfirmEnabled(configuration.getBoolean(EVENT_BUS_PUBLISH_CONFIRM_ENABLED,
 null))
             
.eventBusPropagateDispatchError(configuration.getBoolean(EVENT_BUS_PROPAGATE_DISPATCH_ERROR,
 null))
+            
.eventBusPublishOnNoGroups(configuration.getBoolean(EVENT_BUS_PUBLISH_ON_NO_GROUPS,
 null))
             .vhost(vhost)
             .taskQueueConsumerTimeout(taskQueueConsumerTimeout)
             .build();
@@ -752,6 +762,7 @@ public class RabbitMQConfiguration {
     private final Optional<String> vhost;
     private final Duration taskQueueConsumerTimeout;
     private final boolean eventBusPropagateDispatchError;
+    private final boolean eventBusPublishOnNoGroups;
 
     private RabbitMQConfiguration(URI uri, URI managementUri, 
ManagementCredentials managementCredentials, int maxRetries, int minDelayInMs,
                                   int connectionTimeoutInMs, int 
channelRpcTimeoutInMs, int handshakeTimeoutInMs, int shutdownTimeoutInMs,
@@ -759,7 +770,8 @@ public class RabbitMQConfiguration {
                                   boolean useQuorumQueues, Optional<Integer> 
quorumQueueDeliveryLimit, int quorumQueueReplicationFactor, List<Host> hosts, 
Optional<Long> queueTTL,
                                   boolean eventBusPublishConfirmEnabled, 
boolean eventBusNotificationDurabilityEnabled,
                                   boolean eventBusNotificationQueueAutoDelete,
-                                  Optional<String> vhost, Duration 
taskQueueConsumerTimeout, boolean eventBusPropagateDispatchError) {
+                                  Optional<String> vhost, Duration 
taskQueueConsumerTimeout, boolean eventBusPropagateDispatchError,
+                                  boolean eventBusPublishOnNoGroups) {
         this.uri = uri;
         this.managementUri = managementUri;
         this.managementCredentials = managementCredentials;
@@ -784,6 +796,7 @@ public class RabbitMQConfiguration {
         this.vhost = vhost;
         this.taskQueueConsumerTimeout = taskQueueConsumerTimeout;
         this.eventBusPropagateDispatchError = eventBusPropagateDispatchError;
+        this.eventBusPublishOnNoGroups = eventBusPublishOnNoGroups;
     }
 
     public URI getUri() {
@@ -904,6 +917,10 @@ public class RabbitMQConfiguration {
         return quorumQueueReplicationFactor;
     }
 
+    public boolean eventBusPublishOnNoGroups() {
+        return eventBusPublishOnNoGroups;
+    }
+
     public boolean eventBusPropagateDispatchError() {
         return eventBusPropagateDispatchError;
     }
@@ -936,7 +953,8 @@ public class RabbitMQConfiguration {
                 && Objects.equals(this.eventBusNotificationQueueAutoDelete, 
that.eventBusNotificationQueueAutoDelete)
                 && Objects.equals(this.vhost, that.vhost)
                 && Objects.equals(this.taskQueueConsumerTimeout, 
that.taskQueueConsumerTimeout)
-                && Objects.equals(this.eventBusPropagateDispatchError, 
that.eventBusPropagateDispatchError);
+                && Objects.equals(this.eventBusPropagateDispatchError, 
that.eventBusPropagateDispatchError)
+                && Objects.equals(this.eventBusPublishOnNoGroups, 
that.eventBusPublishOnNoGroups);
         }
         return false;
     }
@@ -945,6 +963,7 @@ public class RabbitMQConfiguration {
     public final int hashCode() {
         return Objects.hash(uri, managementUri, maxRetries, minDelayInMs, 
connectionTimeoutInMs, quorumQueueReplicationFactor, quorumQueueDeliveryLimit, 
useQuorumQueues, hosts,
             channelRpcTimeoutInMs, handshakeTimeoutInMs, shutdownTimeoutInMs, 
networkRecoveryIntervalInMs, managementCredentials, useSsl, useSslManagement,
-            sslConfiguration, queueTTL, eventBusPublishConfirmEnabled, 
eventBusNotificationDurabilityEnabled, eventBusNotificationQueueAutoDelete, 
vhost, taskQueueConsumerTimeout, eventBusPropagateDispatchError);
+            sslConfiguration, queueTTL, eventBusPublishConfirmEnabled, 
eventBusNotificationDurabilityEnabled, eventBusNotificationQueueAutoDelete, 
vhost, taskQueueConsumerTimeout, eventBusPropagateDispatchError,
+            eventBusPublishOnNoGroups);
     }
 }
diff --git 
a/backends-common/rabbitmq/src/test/java/org/apache/james/backends/rabbitmq/RabbitMQConfigurationTest.java
 
b/backends-common/rabbitmq/src/test/java/org/apache/james/backends/rabbitmq/RabbitMQConfigurationTest.java
index df747723d1..2aebe342dc 100644
--- 
a/backends-common/rabbitmq/src/test/java/org/apache/james/backends/rabbitmq/RabbitMQConfigurationTest.java
+++ 
b/backends-common/rabbitmq/src/test/java/org/apache/james/backends/rabbitmq/RabbitMQConfigurationTest.java
@@ -598,6 +598,32 @@ class RabbitMQConfigurationTest {
             .isFalse();
     }
 
+    @Test
+    void eventBusPublishOnNoGroupsShouldBeTrueByDefault() {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        configuration.addProperty("uri", 
"amqp://james:james@rabbitmqhost:5672");
+        configuration.addProperty("management.uri", 
"http://james:james@rabbitmqhost:15672/api/";);
+        configuration.addProperty("management.user", DEFAULT_USER);
+        configuration.addProperty("management.password", 
DEFAULT_PASSWORD_STRING);
+
+        
assertThat(RabbitMQConfiguration.from(configuration).eventBusPublishOnNoGroups())
+            .isTrue();
+    }
+
+    @Test
+    void eventBusPublishOnNoGroupsShouldBeDisabledWhenConfiguredFalse() {
+        PropertiesConfiguration configuration = new PropertiesConfiguration();
+        configuration.addProperty("uri", 
"amqp://james:james@rabbitmqhost:5672");
+        configuration.addProperty("management.uri", 
"http://james:james@rabbitmqhost:15672/api/";);
+        configuration.addProperty("management.user", DEFAULT_USER);
+        configuration.addProperty("management.password", 
DEFAULT_PASSWORD_STRING);
+
+        configuration.addProperty("eventbus.publishOnNoGroups", "false");
+
+        
assertThat(RabbitMQConfiguration.from(configuration).eventBusPublishOnNoGroups())
+            .isFalse();
+    }
+
     @Nested
     class ManagementCredentialsTest {
         @Test
diff --git a/docs/modules/servers/partials/configure/rabbitmq.adoc 
b/docs/modules/servers/partials/configure/rabbitmq.adoc
index 3408aba3a2..7d105d1f53 100644
--- a/docs/modules/servers/partials/configure/rabbitmq.adoc
+++ b/docs/modules/servers/partials/configure/rabbitmq.adoc
@@ -117,6 +117,10 @@ collected once it has stayed unused for the TTL, which 
also cleans up the queues
 | Whether to propagate errors back to the callers when eventbus fails to 
dispatch group events to RabbitMQ (then store the failed events in the event 
dead letters).
 Optional boolean, defaults to true.
 
+| eventbus.publishOnNoGroups
+| Whether to publish group events to RabbitMQ even when this node has no group 
listener registered. Optional boolean,
+defaults to true.
+
 | vhost
 | Optional string. This parameter is only a workaround to support invalid URIs 
containing character like '_'.
 You still need to specify the vhost in the uri parameter.
@@ -167,4 +171,4 @@ Required at least RabbitMQ version 3.12 to have effect.
 This is used to avoid the task queue consumer (which could run very long 
tasks) being disconnected by RabbitMQ after the default acknowledgement timeout 
30 minutes.
 References: https://www.rabbitmq.com/consumers.html#acknowledgement-timeout.
 
-|===
\ No newline at end of file
+|===
diff --git 
a/event-bus/distributed/src/main/java/org/apache/james/events/EventDispatcher.java
 
b/event-bus/distributed/src/main/java/org/apache/james/events/EventDispatcher.java
index 92864f705a..75f232d958 100644
--- 
a/event-bus/distributed/src/main/java/org/apache/james/events/EventDispatcher.java
+++ 
b/event-bus/distributed/src/main/java/org/apache/james/events/EventDispatcher.java
@@ -69,13 +69,15 @@ public class EventDispatcher {
     private final ListenerExecutor listenerExecutor;
     private final EventDeadLetters deadLetters;
     private final RabbitMQConfiguration configuration;
+    private final GroupRegistrationHandler groupRegistrationHandler;
 
     private final DispatchingFailureGroup dispatchingFailureGroup;
 
     EventDispatcher(NamingStrategy namingStrategy, EventBusId eventBusId, 
EventSerializer eventSerializer, Sender sender,
                     LocalListenerRegistry localListenerRegistry,
                     ListenerExecutor listenerExecutor,
-                    EventDeadLetters deadLetters, RabbitMQConfiguration 
configuration) {
+                    EventDeadLetters deadLetters, RabbitMQConfiguration 
configuration,
+                    GroupRegistrationHandler groupRegistrationHandler) {
         this.namingStrategy = namingStrategy;
         this.eventSerializer = eventSerializer;
         this.sender = sender;
@@ -89,6 +91,7 @@ public class EventDispatcher {
         this.listenerExecutor = listenerExecutor;
         this.deadLetters = deadLetters;
         this.configuration = configuration;
+        this.groupRegistrationHandler = groupRegistrationHandler;
         this.dispatchingFailureGroup = new 
DispatchingFailureGroup(namingStrategy.getEventBusName());
     }
 
@@ -188,6 +191,9 @@ public class EventDispatcher {
     }
 
     private Mono<Void> remoteGroupsDispatch(byte[] serializedEvent, Event 
event) {
+        if (shouldSkipGroupsDispatch()) {
+            return Mono.empty();
+        }
         return remoteDispatchWithAcks(serializedEvent)
             .doOnError(ex -> LOGGER.error(
                 "cannot dispatch event of type '{}' belonging '{}' with id 
'{}' to remote groups, store it into dead letter",
@@ -200,6 +206,9 @@ public class EventDispatcher {
     }
 
     private Mono<Void> remoteGroupsDispatch(byte[] serializedEvent, 
List<Event> events) {
+        if (shouldSkipGroupsDispatch()) {
+            return Mono.empty();
+        }
         return remoteDispatchWithAcks(serializedEvent)
             .onErrorResume(ex -> Flux.fromIterable(events)
                 .map(event -> {
@@ -214,6 +223,11 @@ public class EventDispatcher {
                 .then(propagateErrorIfNeeded(ex)));
     }
 
+    private boolean shouldSkipGroupsDispatch() {
+        return !configuration.eventBusPublishOnNoGroups()
+            && groupRegistrationHandler.registeredGroups().isEmpty();
+    }
+
     private Mono<Void> propagateErrorIfNeeded(Throwable throwable) {
         if (configuration.eventBusPropagateDispatchError()) {
             return Mono.error(throwable);
diff --git 
a/event-bus/distributed/src/main/java/org/apache/james/events/RabbitMQEventBus.java
 
b/event-bus/distributed/src/main/java/org/apache/james/events/RabbitMQEventBus.java
index 558866651d..7ee1adb22f 100644
--- 
a/event-bus/distributed/src/main/java/org/apache/james/events/RabbitMQEventBus.java
+++ 
b/event-bus/distributed/src/main/java/org/apache/james/events/RabbitMQEventBus.java
@@ -116,7 +116,7 @@ public class RabbitMQEventBus implements EventBus, 
Startable {
             LocalListenerRegistry localListenerRegistry = new 
LocalListenerRegistry();
             keyRegistrationHandler = new 
KeyRegistrationHandler(namingStrategy, eventBusId, eventSerializer, sender, 
receiverProvider, routingKeyConverter, localListenerRegistry, listenerExecutor, 
configurations, metricFactory);
             groupRegistrationHandler = new 
GroupRegistrationHandler(namingStrategy, eventSerializer, channelPool, sender, 
receiverProvider, eventDeadLetters, listenerExecutor, configurations);
-            eventDispatcher = new EventDispatcher(namingStrategy, eventBusId, 
eventSerializer, sender, localListenerRegistry, listenerExecutor, 
eventDeadLetters, configurations.rabbitMQConfiguration());
+            eventDispatcher = new EventDispatcher(namingStrategy, eventBusId, 
eventSerializer, sender, localListenerRegistry, listenerExecutor, 
eventDeadLetters, configurations.rabbitMQConfiguration(), 
groupRegistrationHandler);
 
             eventDispatcher.start();
             keyRegistrationHandler.start();
@@ -137,7 +137,7 @@ public class RabbitMQEventBus implements EventBus, 
Startable {
             LocalListenerRegistry localListenerRegistry = new 
LocalListenerRegistry();
             keyRegistrationHandler = new 
KeyRegistrationHandler(namingStrategy, eventBusId, eventSerializer, sender, 
receiverProvider, routingKeyConverter, localListenerRegistry, listenerExecutor, 
configurations, metricFactory);
             groupRegistrationHandler = new 
GroupRegistrationHandler(namingStrategy, eventSerializer, channelPool, sender, 
receiverProvider, eventDeadLetters, listenerExecutor, configurations);
-            eventDispatcher = new EventDispatcher(namingStrategy, eventBusId, 
eventSerializer, sender, localListenerRegistry, listenerExecutor, 
eventDeadLetters, configurations.rabbitMQConfiguration());
+            eventDispatcher = new EventDispatcher(namingStrategy, eventBusId, 
eventSerializer, sender, localListenerRegistry, listenerExecutor, 
eventDeadLetters, configurations.rabbitMQConfiguration(), 
groupRegistrationHandler);
 
             keyRegistrationHandler.declareQueue();
 
diff --git 
a/event-bus/distributed/src/test/java/org/apache/james/events/RabbitMQEventBusTest.java
 
b/event-bus/distributed/src/test/java/org/apache/james/events/RabbitMQEventBusTest.java
index b1e531cd8a..ce12c5d628 100644
--- 
a/event-bus/distributed/src/test/java/org/apache/james/events/RabbitMQEventBusTest.java
+++ 
b/event-bus/distributed/src/test/java/org/apache/james/events/RabbitMQEventBusTest.java
@@ -53,6 +53,8 @@ import java.io.Closeable;
 import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.IntStream;
@@ -434,6 +436,79 @@ class RabbitMQEventBusTest implements 
GroupContract.SingleEventBusGroupContract,
                 return eventSerializer.asEvent(new String(eventInBytes, 
StandardCharsets.UTF_8)).event();
             }
         }
+
+    }
+
+    @Nested
+    class PublishOnNoGroupsTest {
+        private String probeQueueName;
+        private RabbitMQEventBus optimizedEventBus;
+
+        @BeforeEach
+        void setUp() throws Exception {
+            probeQueueName = "test-publishOnNoGroups-" + UUID.randomUUID();
+            Sender sender = rabbitMQExtension.getSender();
+            sender.declareQueue(QueueSpecification.queue(probeQueueName)
+                    .durable(!DURABLE)
+                    .exclusive(!EXCLUSIVE)
+                    .autoDelete(!AUTO_DELETE)
+                    .arguments(NO_ARGUMENTS))
+                .block();
+            // Group dispatches are the ones published with the empty routing 
key
+            sender.bind(BindingSpecification.binding()
+                    .exchange(TEST_NAMING_STRATEGY.exchange())
+                    .queue(probeQueueName)
+                    .routingKey(EMPTY_ROUTING_KEY))
+                .block();
+
+            optimizedEventBus = new RabbitMQEventBus(TEST_NAMING_STRATEGY, 
sender, rabbitMQExtension.getReceiverProvider(),
+                eventSerializer, routingKeyConverter, memoryEventDeadLetters, 
new RecordingMetricFactory(),
+                rabbitMQExtension.getRabbitChannelPool(), EventBusId.random(),
+                new 
RabbitMQEventBus.Configurations(rabbitMQExtension.getRabbitMQ().getConfigurationBuilder()
+                    .eventBusPublishOnNoGroups(false)
+                    .build(), 
EventBusTestFixture.RETRY_BACKOFF_CONFIGURATION));
+            optimizedEventBus.start();
+        }
+
+        @AfterEach
+        void tearDown() {
+            optimizedEventBus.stop();
+            
rabbitMQExtension.getSender().delete(QueueSpecification.queue(probeQueueName)).block();
+        }
+
+        @Test
+        void dispatchShouldNotPublishToGroupsWhenNoGroupIsRegistered() {
+            optimizedEventBus.dispatch(EVENT, NO_KEYS).block();
+
+            assertThat(dequeueEventWithin(Duration.ofSeconds(2))).isEmpty();
+        }
+
+        @Test
+        void dispatchShouldPublishToGroupsWhenAGroupIsRegistered() {
+            optimizedEventBus.register(newListener(), GROUP_A);
+
+            optimizedEventBus.dispatch(EVENT, NO_KEYS).block();
+
+            
assertThat(dequeueEventWithin(Duration.ofSeconds(10))).contains(EVENT);
+        }
+
+        @Test
+        void 
dispatchShouldPublishToGroupsWithoutRegistrationWhenOptionIsEnabled() {
+            // eventBus runs with the default configuration: 
`eventbus.publishOnNoGroups` is true
+            eventBus.dispatch(EVENT, NO_KEYS).block();
+
+            
assertThat(dequeueEventWithin(Duration.ofSeconds(10))).contains(EVENT);
+        }
+
+        private Optional<Event> dequeueEventWithin(Duration timeout) {
+            try (Receiver receiver = 
rabbitMQExtension.getReceiverProvider().createReceiver()) {
+                return 
Optional.ofNullable(receiver.consumeAutoAck(probeQueueName)
+                        .next()
+                        .timeout(timeout, Mono.empty())
+                        .block())
+                    .map(delivery -> eventSerializer.asEvent(new 
String(delivery.getBody(), StandardCharsets.UTF_8)).event());
+            }
+        }
     }
 
     @Nested


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to