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

davsclaus 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 75b8c13b4e0d CAMEL-26604: camel-seda - multipleConsumers: a route that 
starts or stops consuming from the queue stops the other routes that consume 
from it (#29368)
75b8c13b4e0d is described below

commit 75b8c13b4e0d6eb087c5decfe473027158ff00f0
Author: allthingssecurity <[email protected]>
AuthorDate: Sat Nov 7 13:31:20 2026 +0530

    CAMEL-26604: camel-seda - multipleConsumers: a route that starts or stops 
consuming from the queue stops the other routes that consume from it (#29368)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../apache/camel/component/seda/SedaEndpoint.java  |  29 +++-
 .../seda/SedaMultipleConsumersOtherRoutesTest.java | 163 +++++++++++++++++++++
 2 files changed, 191 insertions(+), 1 deletion(-)

diff --git 
a/components/camel-seda/src/main/java/org/apache/camel/component/seda/SedaEndpoint.java
 
b/components/camel-seda/src/main/java/org/apache/camel/component/seda/SedaEndpoint.java
index 973bd1404bbd..652503eb4be3 100644
--- 
a/components/camel-seda/src/main/java/org/apache/camel/component/seda/SedaEndpoint.java
+++ 
b/components/camel-seda/src/main/java/org/apache/camel/component/seda/SedaEndpoint.java
@@ -26,6 +26,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.RejectedExecutionException;
 
+import org.apache.camel.AsyncCallback;
 import org.apache.camel.AsyncEndpoint;
 import org.apache.camel.AsyncProcessor;
 import org.apache.camel.Category;
@@ -48,6 +49,7 @@ import org.apache.camel.spi.Metadata;
 import org.apache.camel.spi.UriEndpoint;
 import org.apache.camel.spi.UriParam;
 import org.apache.camel.spi.UriPath;
+import org.apache.camel.support.AsyncProcessorSupport;
 import org.apache.camel.support.DefaultEndpoint;
 import org.apache.camel.support.PluginHelper;
 import org.apache.camel.support.UnitOfWorkHelper;
@@ -341,7 +343,9 @@ public class SedaEndpoint extends DefaultEndpoint 
implements AsyncEndpoint, Brow
                 Set<SedaConsumer> queueConsumers = getQueueConsumers();
                 List<Processor> processors = new 
ArrayList<>(queueConsumers.size());
                 for (SedaConsumer consumer : queueConsumers) {
-                    processors.add(consumer.getProcessor());
+                    // the consumer starts and stops its own processor (its 
route), so the multicast processor, which
+                    // is stopped and created again when a consumer starts or 
stops, must not stop it
+                    processors.add(new 
ConsumerProcessor(consumer.getAsyncProcessor()));
                 }
                 // create multicast processor
                 multicastStarted = false;
@@ -775,4 +779,27 @@ public class SedaEndpoint extends DefaultEndpoint 
implements AsyncEndpoint, Brow
         queue = null;
         ref = null;
     }
+
+    /**
+     * The processor of a consumer, as an output of the multicast processor of 
the consumers, which does not start or
+     * stop the processor of the consumer.
+     */
+    private static final class ConsumerProcessor extends AsyncProcessorSupport 
{
+
+        private final AsyncProcessor processor;
+
+        private ConsumerProcessor(AsyncProcessor processor) {
+            this.processor = processor;
+        }
+
+        @Override
+        public boolean process(Exchange exchange, AsyncCallback callback) {
+            return processor.process(exchange, callback);
+        }
+
+        @Override
+        public String toString() {
+            return processor.toString();
+        }
+    }
 }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/seda/SedaMultipleConsumersOtherRoutesTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/seda/SedaMultipleConsumersOtherRoutesTest.java
new file mode 100644
index 000000000000..6c87069120d3
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/seda/SedaMultipleConsumersOtherRoutesTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.seda;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.builder.AggregationStrategies;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.processor.aggregate.ClosedCorrelationKeyException;
+import org.apache.camel.support.service.ServiceSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * With multipleConsumers, a route that starts or stops consuming from the 
queue must not stop the other routes that
+ * consume from it.
+ */
+class SedaMultipleConsumersOtherRoutesTest extends ContextTestSupport {
+
+    private static final String URI = "seda:x?multipleConsumers=true";
+
+    private final CountDownLatch blocked = new CountDownLatch(1);
+    private final CountDownLatch release = new CountDownLatch(1);
+    private final CountingProcessor counting = new CountingProcessor();
+    private final CountDownLatch k2Done = new CountDownLatch(1);
+    private final AtomicReference<Exception> k2Failure = new 
AtomicReference<>();
+
+    @Test
+    void testInflightExchangeCompletesWhenAnotherRouteStarts() throws 
Exception {
+        MockEndpoint a = getMockEndpoint("mock:a");
+        a.expectedBodiesReceived("m1");
+
+        template.sendBody(URI, "m1");
+        assertTrue(blocked.await(10, TimeUnit.SECONDS));
+        context.addRoutes(routeB());
+        release.countDown();
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    void testOtherRoutesNotStoppedWhenAnotherRouteStartsAndStops() throws 
Exception {
+        MockEndpoint a = getMockEndpoint("mock:a");
+        // the multicast of the consumers is parallel, so the order is not kept
+        a.expectedBodiesReceivedInAnyOrder("m0", "m2", "m3");
+        release.countDown();
+
+        template.sendBody(URI, "m0");
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
a.getReceivedCounter() == 1);
+        context.addRoutes(routeB());
+        template.sendBody(URI, "m2");
+        context.getRouteController().stopRoute("B");
+        template.sendBody(URI, "m3");
+
+        MockEndpoint.assertIsSatisfied(context, 10, TimeUnit.SECONDS);
+        assertEquals(0, counting.stops.get(), "the processor of route A was 
stopped");
+    }
+
+    @Test
+    void testClosedCorrelationKeyKeptWhenAnotherRouteStarts() throws Exception 
{
+        release.countDown();
+        MockEndpoint agg = getMockEndpoint("mock:agg");
+        MockEndpoint b = getMockEndpoint("mock:b");
+
+        template.sendBodyAndHeader(URI, "k1", "k", "K");
+        agg.expectedMessageCount(1);
+        agg.assertIsSatisfied();
+
+        context.addRoutes(routeB());
+        b.expectedMessageCount(1);
+        // the key is closed, so the aggregator rejects this message
+        template.sendBodyAndHeader(URI, "k2", "k", "K");
+        b.assertIsSatisfied();
+        assertTrue(k2Done.await(10, TimeUnit.SECONDS));
+        // the aggregator hands a completed group to its own thread, so the 
rejection is what proves the key was kept
+        assertInstanceOf(ClosedCorrelationKeyException.class, k2Failure.get());
+
+        agg.expectedMessageCount(1);
+        agg.assertIsSatisfied();
+    }
+
+    private RouteBuilder routeB() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from(URI).routeId("B").to("mock:b");
+            }
+        };
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from(URI).routeId("A")
+                        .onCompletion()
+                        .process(e -> {
+                            if 
("k2".equals(e.getMessage().getBody(String.class))) {
+                                
k2Failure.set(e.getProperty(Exchange.EXCEPTION_CAUGHT, Exception.class));
+                                k2Done.countDown();
+                            }
+                        })
+                        .end()
+                        .process(e -> {
+                            if 
("m1".equals(e.getMessage().getBody(String.class))) {
+                                blocked.countDown();
+                                assertTrue(release.await(10, 
TimeUnit.SECONDS));
+                            }
+                        })
+                        .process(counting)
+                        .choice()
+                        .when(header("k").isNotNull())
+                        .aggregate(header("k"), 
AggregationStrategies.groupedBody()).completionSize(1)
+                        .closeCorrelationKeyOnCompletion(100)
+                        .to("mock:agg")
+                        .end()
+                        .endChoice()
+                        .otherwise()
+                        .to("mock:a");
+            }
+        };
+    }
+
+    private static final class CountingProcessor extends ServiceSupport 
implements Processor {
+        private final AtomicInteger stops = new AtomicInteger();
+
+        @Override
+        public void process(Exchange exchange) {
+            // noop
+        }
+
+        @Override
+        protected void doStop() {
+            stops.incrementAndGet();
+        }
+    }
+}

Reply via email to