This is an automated email from the ASF dual-hosted git repository.
apupier pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new dca2a14b07c4 CAMEL-26680: camel-disruptor - multipleConsumers: each
consumer copies a snapshot of the exchange
dca2a14b07c4 is described below
commit dca2a14b07c400f91e57e463d5ed7cdba58f0708
Author: Aurélien Pupier <[email protected]>
AuthorDate: Fri Nov 6 10:29:23 2026 +0100
CAMEL-26680: camel-disruptor - multipleConsumers: each consumer copies a
snapshot of the exchange
With multipleConsumers=true each consumer copied the shared original
exchange when it got to the event. The first consumer that failed copied its
result (the exception, body, headers and properties) into that exchange, so a
consumer that came later routed a copy that already had the exception and did
nothing with it, and a consumer that copied at the same moment read the maps
while they were written. The message was silently dropped for that consumer,
which also made DisruptorMultiple [...]
MultipleConsumerSynchronizedExchange now takes a snapshot of the exchange
when it is published, and the consumers copy that one. The test no longer needs
a barrier between the consumers, and a new test holds a consumer until the
other one has failed the exchange.
Co-authored-by: IBM Bob 2.2.1
Signed-off-by: Aurélien Pupier <[email protected]>
---
.../disruptor/AbstractSynchronizedExchange.java | 5 ++
.../component/disruptor/DisruptorConsumer.java | 3 +-
.../MultipleConsumerSynchronizedExchange.java | 11 +++
.../component/disruptor/SynchronizedExchange.java | 7 ++
...DisruptorMultipleConsumersLateConsumerTest.java | 87 ++++++++++++++++++++++
5 files changed, 112 insertions(+), 1 deletion(-)
diff --git
a/components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/AbstractSynchronizedExchange.java
b/components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/AbstractSynchronizedExchange.java
index ae138988a68a..ef100959a53c 100644
---
a/components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/AbstractSynchronizedExchange.java
+++
b/components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/AbstractSynchronizedExchange.java
@@ -36,6 +36,11 @@ public abstract class AbstractSynchronizedExchange
implements SynchronizedExchan
return exchange;
}
+ @Override
+ public Exchange getExchangeToCopy() {
+ return exchange;
+ }
+
@Override
public Exchange cancelAndGetOriginalExchange() {
if (synchronizations != null) {
diff --git
a/components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/DisruptorConsumer.java
b/components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/DisruptorConsumer.java
index f20145996c07..639994443bbd 100644
---
a/components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/DisruptorConsumer.java
+++
b/components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/DisruptorConsumer.java
@@ -135,6 +135,7 @@ public class DisruptorConsumer extends ServiceSupport
implements Consumer, Suspe
private Exchange prepareExchange(final Exchange exchange) {
// send a new copied exchange with new camel context
// don't copy handovers as they are handled by the Disruptor Event
Handlers
+ // (with several consumers the exchange is a snapshot, as another
consumer may fail and change the original)
final Exchange newExchange =
ExchangeHelper.copyExchangeWithProperties(exchange, endpoint.getCamelContext());
// the flag is only for this consumer, and must not be routed (or
copied back to the caller)
newExchange.removeProperty(DisruptorEndpoint.DISRUPTOR_IGNORE_EXCHANGE);
@@ -154,7 +155,7 @@ public class DisruptorConsumer extends ServiceSupport
implements Consumer, Suspe
}
// send a new copied exchange with new camel context
- final Exchange result = prepareExchange(exchange);
+ final Exchange result =
prepareExchange(synchronizedExchange.getExchangeToCopy());
// We need to be notified when the exchange processing is complete
to synchronize the original exchange
// This is however, the last part of the processing of this
exchange and as such can't be done
diff --git
a/components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/MultipleConsumerSynchronizedExchange.java
b/components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/MultipleConsumerSynchronizedExchange.java
index b2f2df6abd35..cc82c88247ad 100644
---
a/components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/MultipleConsumerSynchronizedExchange.java
+++
b/components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/MultipleConsumerSynchronizedExchange.java
@@ -30,6 +30,10 @@ public class MultipleConsumerSynchronizedExchange extends
AbstractSynchronizedEx
private final int expectedConsumers;
+ // the consumers copy this one: the first consumer that fails copies its
result into the exchange, which the other
+ // consumers must neither see nor read while it is written
+ private final Exchange exchangeToCopy;
+
private final AtomicInteger processedConsumers = new AtomicInteger();
private final AtomicBoolean resultHandled = new AtomicBoolean();
@@ -40,6 +44,13 @@ public class MultipleConsumerSynchronizedExchange extends
AbstractSynchronizedEx
super(exchange);
this.expectedConsumers = expectedConsumers;
processedConsumers.set(0);
+ // taken when the exchange is published, before any consumer can fail
+ this.exchangeToCopy =
ExchangeHelper.copyExchangeWithProperties(exchange, exchange.getContext());
+ }
+
+ @Override
+ public Exchange getExchangeToCopy() {
+ return exchangeToCopy;
}
@Override
diff --git
a/components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/SynchronizedExchange.java
b/components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/SynchronizedExchange.java
index ef51243ce9b0..d82344706f4c 100644
---
a/components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/SynchronizedExchange.java
+++
b/components/camel-disruptor/src/main/java/org/apache/camel/component/disruptor/SynchronizedExchange.java
@@ -25,6 +25,13 @@ import org.apache.camel.Exchange;
public interface SynchronizedExchange {
Exchange getExchange();
+ /**
+ * The exchange that a consumer copies to route. With several consumers
this is a snapshot taken when the exchange
+ * is published, as the first consumer that fails copies its result into
{@link #getExchange()}, which the other
+ * consumers must not see (the exception) or read while it is written.
+ */
+ Exchange getExchangeToCopy();
+
void consumed(Exchange result);
Exchange cancelAndGetOriginalExchange();
diff --git
a/components/camel-disruptor/src/test/java/org/apache/camel/component/disruptor/DisruptorMultipleConsumersLateConsumerTest.java
b/components/camel-disruptor/src/test/java/org/apache/camel/component/disruptor/DisruptorMultipleConsumersLateConsumerTest.java
new file mode 100644
index 000000000000..0162391adb04
--- /dev/null
+++
b/components/camel-disruptor/src/test/java/org/apache/camel/component/disruptor/DisruptorMultipleConsumersLateConsumerTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.camel.component.disruptor;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.builder.NotifyBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * With multipleConsumers every consumer routes its own copy of the exchange,
also a consumer that gets to the exchange
+ * after another consumer has failed it.
+ */
+public class DisruptorMultipleConsumersLateConsumerTest extends
CamelTestSupport {
+
+ private final CountDownLatch blockerStarted = new CountDownLatch(1);
+ private final CountDownLatch release = new CountDownLatch(1);
+
+ @AfterEach
+ void releaseConsumers() {
+ // do not leave a consumer waiting when a test fails
+ release.countDown();
+ }
+
+ @Test
+ void testLateConsumerRoutesTheExchangeThatAnotherConsumerFailed() throws
Exception {
+ MockEndpoint mock = getMockEndpoint("mock:late");
+ mock.expectedBodiesReceived("blocker", "fail");
+
+ NotifyBuilder failingDone = new
NotifyBuilder(context).from("disruptor:late*").whenDone(2).create();
+
+ // the late consumer is busy with this one, so it gets to the next
exchange only when it is released
+ template.sendBody("direct:start", "blocker");
+ assertTrue(blockerStarted.await(10, TimeUnit.SECONDS), "The late
consumer should be busy");
+
+ // the other consumer is done with both exchanges, and has failed the
second one
+ template.sendBody("direct:start", "fail");
+ assertTrue(failingDone.matches(10, TimeUnit.SECONDS), "The failing
consumer should be done");
+
+ release.countDown();
+ MockEndpoint.assertIsSatisfied(context);
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("direct:start").to("disruptor:late?multipleConsumers=true");
+
+
from("disruptor:late?multipleConsumers=true").errorHandler(noErrorHandler())
+ .filter(body().isEqualTo("fail"))
+ .throwException(new
IllegalArgumentException("Forced"));
+
+
from("disruptor:late?multipleConsumers=true").errorHandler(noErrorHandler())
+ .filter(body().isEqualTo("blocker"))
+ .process(e -> {
+ blockerStarted.countDown();
+ release.await(20, TimeUnit.SECONDS);
+ })
+ .end()
+ .to("mock:late");
+ }
+ };
+ }
+}