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 96e78f96c127 CAMEL-23129: Fix CountDownLatch count mismatch in
ThreadPerTaskSedaConsumer
96e78f96c127 is described below
commit 96e78f96c1271dabd918feb0232b744d5168682d
Author: Guillaume Nodet <[email protected]>
AuthorDate: Tue Aug 4 10:41:16 2026 +0200
CAMEL-23129: Fix CountDownLatch count mismatch in ThreadPerTaskSedaConsumer
SedaConsumer.doStart() creates latch with concurrentConsumers count, but
ThreadPerTaskSedaConsumer uses a single coordinator thread. When
concurrentConsumers > 1 (used as concurrency limit), the latch count
exceeds the coordinator thread count, causing prepareShutdown() to wait
the full shutdown timeout. Fix by overriding doStart() to set latch
count to 1, matching the single coordinator thread.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
.../apache/camel/component/seda/SedaConsumer.java | 2 +-
.../component/seda/ThreadPerTaskSedaConsumer.java | 13 ++++++++
.../seda/ThreadPerTaskSedaConsumerTest.java | 38 +++++++++++++++++++---
3 files changed, 48 insertions(+), 5 deletions(-)
diff --git
a/components/camel-seda/src/main/java/org/apache/camel/component/seda/SedaConsumer.java
b/components/camel-seda/src/main/java/org/apache/camel/component/seda/SedaConsumer.java
index 9432ed7860bc..e4503f9f7ca8 100644
---
a/components/camel-seda/src/main/java/org/apache/camel/component/seda/SedaConsumer.java
+++
b/components/camel-seda/src/main/java/org/apache/camel/component/seda/SedaConsumer.java
@@ -49,7 +49,7 @@ public class SedaConsumer extends DefaultConsumer implements
Runnable, ShutdownA
private static final Logger LOG =
LoggerFactory.getLogger(SedaConsumer.class);
private final AtomicInteger taskCount = new AtomicInteger();
- private volatile CountDownLatch latch;
+ protected volatile CountDownLatch latch;
private volatile boolean shutdownPending;
private volatile boolean forceShutdown;
private ExecutorService executor;
diff --git
a/components/camel-seda/src/main/java/org/apache/camel/component/seda/ThreadPerTaskSedaConsumer.java
b/components/camel-seda/src/main/java/org/apache/camel/component/seda/ThreadPerTaskSedaConsumer.java
index d41c09e17f7d..a764c9e2fe39 100644
---
a/components/camel-seda/src/main/java/org/apache/camel/component/seda/ThreadPerTaskSedaConsumer.java
+++
b/components/camel-seda/src/main/java/org/apache/camel/component/seda/ThreadPerTaskSedaConsumer.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.component.seda;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
@@ -68,6 +69,18 @@ public class ThreadPerTaskSedaConsumer extends SedaConsumer {
.newSingleThreadExecutor(this, getEndpoint().getEndpointUri()
+ "-coordinator");
}
+ @Override
+ protected void doStart() throws Exception {
+ super.doStart();
+ // SedaConsumer.doStart() creates the latch with concurrentConsumers
count,
+ // but ThreadPerTaskSedaConsumer uses a single coordinator thread that
polls
+ // the queue and dispatches each exchange to the task executor.
+ // The concurrentConsumers value is used here as a concurrency limit
for the
+ // task executor (via a Semaphore), not as the number of polling
threads.
+ // Override the latch to match the actual coordinator thread count (1).
+ latch = new CountDownLatch(1);
+ }
+
@Override
protected void setupTasks() {
// Create task executor - uses virtual threads when enabled
diff --git
a/core/camel-core/src/test/java/org/apache/camel/component/seda/ThreadPerTaskSedaConsumerTest.java
b/core/camel-core/src/test/java/org/apache/camel/component/seda/ThreadPerTaskSedaConsumerTest.java
index b3cf9e5af948..8cc6e7b6bd3c 100644
---
a/core/camel-core/src/test/java/org/apache/camel/component/seda/ThreadPerTaskSedaConsumerTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/component/seda/ThreadPerTaskSedaConsumerTest.java
@@ -16,18 +16,22 @@
*/
package org.apache.camel.component.seda;
+import java.util.concurrent.TimeUnit;
+
import org.apache.camel.ContextTestSupport;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
/**
* Test for the virtualThreadPerTask mode of SEDA consumer
*/
-public class ThreadPerTaskSedaConsumerTest extends ContextTestSupport {
+class ThreadPerTaskSedaConsumerTest extends ContextTestSupport {
@Test
- public void testVirtualThreadPerTask() throws Exception {
+ void testVirtualThreadPerTask() throws Exception {
MockEndpoint mock = getMockEndpoint("mock:result");
mock.expectedMessageCount(10);
@@ -39,7 +43,7 @@ public class ThreadPerTaskSedaConsumerTest extends
ContextTestSupport {
}
@Test
- public void testVirtualThreadPerTaskWithConcurrencyLimit() throws
Exception {
+ void testVirtualThreadPerTaskWithConcurrencyLimit() throws Exception {
MockEndpoint mock = getMockEndpoint("mock:limited");
mock.expectedMessageCount(5);
@@ -51,7 +55,7 @@ public class ThreadPerTaskSedaConsumerTest extends
ContextTestSupport {
}
@Test
- public void testVirtualThreadPerTaskHighThroughput() throws Exception {
+ void testVirtualThreadPerTaskHighThroughput() throws Exception {
int messageCount = 100;
MockEndpoint mock = getMockEndpoint("mock:throughput");
mock.expectedMessageCount(messageCount);
@@ -63,6 +67,32 @@ public class ThreadPerTaskSedaConsumerTest extends
ContextTestSupport {
mock.assertIsSatisfied();
}
+ @Test
+ void testShutdownWithConcurrencyLimitCompletesQuickly() throws Exception {
+ // Send messages so the route is actively used
+ for (int i = 0; i < 5; i++) {
+
template.sendBody("seda:limited?virtualThreadPerTask=true&concurrentConsumers=2",
"Message " + i);
+ }
+
+ MockEndpoint mock = getMockEndpoint("mock:limited");
+ mock.expectedMessageCount(5);
+ mock.assertIsSatisfied();
+
+ // Stop the context and verify it completes quickly.
+ // Before the fix, the CountDownLatch was initialized with
concurrentConsumers
+ // count (2) but only 1 coordinator thread counts down, so
prepareShutdown()
+ // would wait the full shutdown timeout before proceeding.
+ long start = System.nanoTime();
+ context.stop();
+ long elapsed = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() -
start);
+
+ // Shutdown should complete well within the default timeout (300s).
+ // Use a generous 30s bound to avoid flakiness, but this is still much
less
+ // than the full shutdown strategy timeout that would be hit without
the fix.
+ assertTrue(elapsed < 30, "Context stop took " + elapsed + "s, expected
< 30s. "
+ + "The CountDownLatch count likely does not
match the coordinator thread count.");
+ }
+
@Override
protected RouteBuilder createRouteBuilder() {
return new RouteBuilder() {