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 6c343ef27820 CAMEL-26605: camel-seda - receive() without a timeout 
returns null once the polling consumer is stopped (#29375)
6c343ef27820 is described below

commit 6c343ef27820df3e557353c91b0675bded2f5007
Author: allthingssecurity <[email protected]>
AuthorDate: Sat Nov 7 13:31:38 2026 +0530

    CAMEL-26605: camel-seda - receive() without a timeout returns null once the 
polling consumer is stopped (#29375)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/component/seda/SedaPollingConsumer.java  |  27 ++++-
 .../seda/SedaPollingConsumerStopTest.java          | 124 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  10 ++
 3 files changed, 160 insertions(+), 1 deletion(-)

diff --git 
a/components/camel-seda/src/main/java/org/apache/camel/component/seda/SedaPollingConsumer.java
 
b/components/camel-seda/src/main/java/org/apache/camel/component/seda/SedaPollingConsumer.java
index 86add56a6561..0c2206511d45 100644
--- 
a/components/camel-seda/src/main/java/org/apache/camel/component/seda/SedaPollingConsumer.java
+++ 
b/components/camel-seda/src/main/java/org/apache/camel/component/seda/SedaPollingConsumer.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.component.seda;
 
+import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.TimeUnit;
 
 import org.apache.camel.Endpoint;
@@ -24,9 +25,16 @@ import org.apache.camel.IsSingleton;
 import org.apache.camel.Processor;
 import org.apache.camel.RuntimeCamelException;
 import org.apache.camel.support.PollingConsumerSupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class SedaPollingConsumer extends PollingConsumerSupport implements 
IsSingleton {
 
+    private static final Logger LOG = 
LoggerFactory.getLogger(SedaPollingConsumer.class);
+
+    // how often receive() checks, while it waits for a message, whether this 
consumer was stopped
+    private static final long RUN_ALLOWED_CHECK_INTERVAL = 1000;
+
     public SedaPollingConsumer(Endpoint endpoint) {
         super(endpoint);
     }
@@ -43,8 +51,20 @@ public class SedaPollingConsumer extends 
PollingConsumerSupport implements IsSin
 
     @Override
     public Exchange receive() {
+        BlockingQueue<Exchange> queue = getEndpoint().getQueue();
         try {
-            return getEndpoint().getQueue().take();
+            while (true) {
+                Exchange answer = queue.poll(RUN_ALLOWED_CHECK_INTERVAL, 
TimeUnit.MILLISECONDS);
+                if (answer != null) {
+                    return answer;
+                }
+                // once this consumer is stopped (such as when the 
CamelContext, the consumer template or the route of a
+                // pollEnrich stops) stop waiting; a consumer that was never 
started keeps waiting, as before
+                if (isStoppedAfterStart()) {
+                    LOG.trace("Consumer is stopped, so returning null");
+                    return null;
+                }
+            }
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
             throw RuntimeCamelException.wrapRuntimeCamelException(e);
@@ -76,6 +96,11 @@ public class SedaPollingConsumer extends 
PollingConsumerSupport implements IsSin
         // noop
     }
 
+    private boolean isStoppedAfterStart() {
+        // stopping, stopped, shutting down, shut down or failed
+        return status >= STOPPING;
+    }
+
     @Override
     public boolean isSingleton() {
         return true;
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/seda/SedaPollingConsumerStopTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/seda/SedaPollingConsumerStopTest.java
new file mode 100644
index 000000000000..ebb32a6a455f
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/seda/SedaPollingConsumerStopTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.PollingConsumer;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.AfterEach;
+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.assertNull;
+
+/**
+ * A receive() without a timeout on a seda endpoint must return once the 
polling consumer is stopped, and must keep
+ * waiting on a polling consumer that was never started.
+ */
+class SedaPollingConsumerStopTest extends ContextTestSupport {
+
+    private final ExecutorService executor = 
Executors.newSingleThreadExecutor();
+    private volatile Thread receiver;
+
+    @AfterEach
+    void stopExecutor() {
+        // a receive that still waits (the bug) must not keep the test JVM busy
+        executor.shutdownNow();
+    }
+
+    @Test
+    void testConsumerTemplateReceiveReturnsWhenCamelContextStops() throws 
Exception {
+        Future<Exchange> received = receiveInThread(() -> 
consumer.receive("seda:idle"));
+
+        context.stop();
+
+        assertNull(received.get(10, TimeUnit.SECONDS));
+    }
+
+    @Test
+    void testConsumerTemplateReceiveReturnsWhenTemplateStops() throws 
Exception {
+        Future<Exchange> received = receiveInThread(() -> 
consumer.receive("seda:idle"));
+
+        consumer.stop();
+
+        assertNull(received.get(10, TimeUnit.SECONDS));
+    }
+
+    @Test
+    void testPollEnrichWithoutTimeoutReturnsWhenRouteStops() throws Exception {
+        Future<Exchange> sent = receiveInThread(() -> 
template.send("direct:start", e -> e.getMessage().setBody("x")));
+
+        // the graceful stop waits for the exchange in pollEnrich, the forced 
stop then stops the polling consumer
+        context.getRouteController().stopRoute("poll", 1, TimeUnit.SECONDS);
+
+        // and the waiting receive() returns, so the exchange completes
+        sent.get(10, TimeUnit.SECONDS);
+    }
+
+    @Test
+    void testReceiveOnPollingConsumerNotStartedKeepsWaiting() throws Exception 
{
+        PollingConsumer pollingConsumer = 
context.getEndpoint("seda:idle").createPollingConsumer();
+
+        Future<Exchange> received = receiveInThread(pollingConsumer::receive);
+        // longer than the interval in which receive checks whether it was 
stopped
+        await().during(1500, TimeUnit.MILLISECONDS).atMost(5, 
TimeUnit.SECONDS).until(() -> !received.isDone());
+
+        template.sendBody("seda:idle", "hello");
+
+        assertEquals("hello", received.get(10, 
TimeUnit.SECONDS).getMessage().getBody());
+    }
+
+    private Future<Exchange> receiveInThread(Callable<Exchange> receive) {
+        CompletableFuture<Exchange> answer = new CompletableFuture<>();
+        executor.execute(() -> {
+            receiver = Thread.currentThread();
+            try {
+                answer.complete(receive.call());
+            } catch (Exception e) {
+                answer.completeExceptionally(e);
+            }
+        });
+        // wait until the thread waits for a message in the seda polling 
consumer
+        await().atMost(10, TimeUnit.SECONDS).until(() -> receiver != null
+                && Arrays.stream(receiver.getStackTrace())
+                        .anyMatch(f -> 
f.getClassName().equals(SedaPollingConsumer.class.getName())
+                                && f.getMethodName().equals("receive")));
+        return answer;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start").routeId("poll")
+                        .pollEnrich("seda:idle")
+                        .to("mock:result");
+            }
+        };
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index d66a5266da36..f0a3e7e0ec09 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -678,6 +678,16 @@ and retry the call once, and only when they have an 
`apiKey` or a `password` to
 component get a new token when the current one expires: when the `authToken` 
is set, the component uses only that
 token.
 
+=== camel-seda - receive without a timeout returns when the polling consumer 
stops
+
+A `receive()` without a timeout on a seda endpoint, such as 
`consumerTemplate.receive("seda:foo")` or a `pollEnrich`
+from seda without a timeout, waited for a message for ever, also after the 
CamelContext, the consumer template or the
+route of the `pollEnrich` was stopped. It now returns `null` (no message) 
within about a second once the polling
+consumer is stopped, as `EventDrivenPollingConsumer`, used by most other 
endpoints, does. This also happens when the
+consumer cache of a consumer template evicts the polling consumer (see 
`maximumCacheSize`). A message that is in the
+queue is still returned. A polling consumer that was never started still waits 
for a message, as before. A caller that
+loops on `receive()` should stop on a `null` result.
+
 === camel-main - a failure to start is logged
 
 A Camel Main application that fails to start now logs why, at ERROR: `Error 
starting Camel: ...` with the stack trace,

Reply via email to