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 6a40da7960fb CAMEL-26578: camel-core - ProducerTemplate: complete the 
future of an async send or request when the template is stopped before the task 
ran (#29194)
6a40da7960fb is described below

commit 6a40da7960fb414978093abb2dce4439e49da3df
Author: allthingssecurity <[email protected]>
AuthorDate: Thu Nov 5 12:34:09 2026 +0530

    CAMEL-26578: camel-core - ProducerTemplate: complete the future of an async 
send or request when the template is stopped before the task ran (#29194)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/impl/engine/DefaultProducerTemplate.java |  73 ++++++-
 .../impl/DefaultProducerTemplateAsyncStopTest.java | 218 +++++++++++++++++++++
 2 files changed, 289 insertions(+), 2 deletions(-)

diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultProducerTemplate.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultProducerTemplate.java
index 9455fa52f271..0eeca96f4d5d 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultProducerTemplate.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultProducerTemplate.java
@@ -17,11 +17,15 @@
 package org.apache.camel.impl.engine;
 
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Function;
 
 import org.apache.camel.CamelContext;
@@ -51,6 +55,8 @@ public class DefaultProducerTemplate extends ServiceSupport 
implements ProducerT
     private final CamelContext camelContext;
     private volatile ProducerCache producerCache;
     private volatile ExecutorService executor;
+    // async send tasks handed to the executor that have not started yet 
(completed exceptionally on stop)
+    private final Set<AsyncSendTask> pendingAsyncTasks = 
ConcurrentHashMap.newKeySet();
     private Endpoint defaultEndpoint;
     private int maximumCacheSize;
     private boolean eventNotifierEnabled = true;
@@ -742,8 +748,20 @@ public class DefaultProducerTemplate extends 
ServiceSupport implements ProducerT
             Endpoint endpoint, ExchangePattern pattern, Processor processor, 
Processor resultProcessor,
             Exchange inExchange) {
         CompletableFuture<Exchange> exchangeFuture = new CompletableFuture<>();
-        getExecutorService().submit(() -> 
getProducerCache().asyncSendExchange(endpoint, pattern, processor,
-                resultProcessor, inExchange, exchangeFuture));
+        AsyncSendTask task = new AsyncSendTask(endpoint, pattern, processor, 
resultProcessor, inExchange, exchangeFuture);
+        ExecutorService executorService = getExecutorService();
+        pendingAsyncTasks.add(task);
+        try {
+            executorService.execute(task);
+        } catch (RuntimeException e) {
+            pendingAsyncTasks.remove(task);
+            throw e;
+        }
+        if (!isStarted() || executorService.isShutdown()) {
+            // stopped while the task was handed over, so doStop may have 
missed it, or the executor may have
+            // discarded it (such as the caller runs policy of a pool that is 
shut down)
+            task.reject();
+        }
         return exchangeFuture;
     }
 
@@ -804,6 +822,10 @@ public class DefaultProducerTemplate extends 
ServiceSupport implements ProducerT
             camelContext.getExecutorServiceManager().shutdownNow(executor);
             executor = null;
         }
+        // the executor dropped these tasks (or runs them after the stop), so 
complete their futures
+        for (AsyncSendTask task : pendingAsyncTasks) {
+            task.reject();
+        }
     }
 
     @Override
@@ -811,4 +833,51 @@ public class DefaultProducerTemplate extends 
ServiceSupport implements ProducerT
         ServiceHelper.stopAndShutdownService(producerCache);
         producerCache = null;
     }
+
+    /**
+     * Sends the exchange of an async request. When sending throws (such as 
the template is no longer started), the
+     * future is completed exceptionally with that exception. When the 
template stops before the task was run, the
+     * future is completed exceptionally by {@link #reject()}, and the task 
does not send when it runs later.
+     */
+    private final class AsyncSendTask implements Runnable {
+        private final AtomicBoolean claimed = new AtomicBoolean();
+        private final Endpoint endpoint;
+        private final ExchangePattern pattern;
+        private final Processor processor;
+        private final Processor resultProcessor;
+        private final Exchange inExchange;
+        private final CompletableFuture<Exchange> future;
+
+        AsyncSendTask(Endpoint endpoint, ExchangePattern pattern, Processor 
processor, Processor resultProcessor,
+                      Exchange inExchange, CompletableFuture<Exchange> future) 
{
+            this.endpoint = endpoint;
+            this.pattern = pattern;
+            this.processor = processor;
+            this.resultProcessor = resultProcessor;
+            this.inExchange = inExchange;
+            this.future = future;
+        }
+
+        @Override
+        public void run() {
+            if (!claimed.compareAndSet(false, true)) {
+                // already rejected by stop
+                return;
+            }
+            pendingAsyncTasks.remove(this);
+            try {
+                getProducerCache().asyncSendExchange(endpoint, pattern, 
processor, resultProcessor, inExchange, future);
+            } catch (Throwable e) {
+                future.completeExceptionally(e);
+            }
+        }
+
+        void reject() {
+            pendingAsyncTasks.remove(this);
+            if (claimed.compareAndSet(false, true)) {
+                future.completeExceptionally(
+                        new RejectedExecutionException("ProducerTemplate is 
not started, so the exchange was not sent"));
+            }
+        }
+    }
 }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/impl/DefaultProducerTemplateAsyncStopTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/impl/DefaultProducerTemplateAsyncStopTest.java
new file mode 100644
index 000000000000..4407fa61aa89
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/DefaultProducerTemplateAsyncStopTest.java
@@ -0,0 +1,218 @@
+/*
+ * 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.impl;
+
+import java.util.List;
+import java.util.concurrent.AbstractExecutorService;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.Consumer;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Endpoint;
+import org.apache.camel.Exchange;
+import org.apache.camel.ExchangePattern;
+import org.apache.camel.FluentProducerTemplate;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.support.DefaultEndpoint;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The future of an async send or request must complete when the template is 
stopped before the task ran.
+ */
+public class DefaultProducerTemplateAsyncStopTest extends ContextTestSupport {
+
+    @Test
+    public void testQueuedRequestCompletedWhenTemplateStops() throws Exception 
{
+        ProducerTemplate producer = context.createProducerTemplate();
+        ExecutorService single = 
context.getExecutorServiceManager().newSingleThreadExecutor(this, 
"AsyncStopTest");
+        producer.setExecutorService(single);
+        CountDownLatch release = occupy(single);
+
+        Future<Object> future = producer.asyncRequestBody("direct:start", 
"Hello World");
+        // shuts down the executor which drops the queued request
+        producer.stop();
+        release.countDown();
+
+        assertRejected(future);
+    }
+
+    @Test
+    public void testQueuedFluentRequestCompletedWhenTemplateStops() throws 
Exception {
+        ExecutorService single = 
context.getExecutorServiceManager().newSingleThreadExecutor(this, 
"AsyncStopTest");
+        FluentProducerTemplate fluent = context.createFluentProducerTemplate()
+                .withTemplateCustomizer(t -> t.setExecutorService(single));
+        fluent.start();
+        CountDownLatch release = occupy(single);
+
+        Future<Object> future = fluent.withBody("Hello 
World").to("direct:start").asyncRequest();
+        fluent.stop();
+        release.countDown();
+
+        assertRejected(future);
+    }
+
+    @Test
+    public void testRequestNotSentWhenExecutorRunsItAfterStop() throws 
Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedMessageCount(0);
+
+        ProducerTemplate producer = context.createProducerTemplate();
+        // an executor which only runs the task once the template is stopped, 
and does not drop it on shutdown
+        GatedExecutorService gated = new GatedExecutorService();
+        producer.setExecutorService(gated);
+
+        Future<Object> future = producer.asyncRequestBody("direct:start", 
"Hello World");
+        producer.stop();
+        gated.open();
+
+        assertRejected(future);
+        gated.thread.join(TimeUnit.SECONDS.toMillis(10));
+        assertFalse(gated.thread.isAlive());
+        mock.assertIsSatisfied();
+    }
+
+    @Test
+    public void testFailureBeforeSendingCompletesFuture() throws Exception {
+        Endpoint broken = new DefaultEndpoint("broken:test", null) {
+            @Override
+            public Producer createProducer() {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public Consumer createConsumer(Processor processor) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public Exchange createExchange(ExchangePattern pattern) {
+                throw new IllegalStateException("Forced");
+            }
+        };
+        broken.setCamelContext(context);
+
+        Future<Object> future = template.asyncRequestBody(broken, "Hello 
World");
+
+        ExecutionException e = assertThrows(ExecutionException.class, () -> 
future.get(10, TimeUnit.SECONDS));
+        assertInstanceOf(IllegalStateException.class, e.getCause());
+        assertEquals("Forced", e.getCause().getMessage());
+    }
+
+    @Test
+    public void testRequestCompletesNormallyAfterRestart() throws Exception {
+        ProducerTemplate producer = context.createProducerTemplate();
+        assertEquals("Bye World", producer.asyncRequestBody("direct:start", 
"Hello World").get(10, TimeUnit.SECONDS));
+
+        producer.stop();
+        producer.start();
+
+        assertEquals("Bye World", producer.asyncRequestBody("direct:start", 
"Hello World").get(10, TimeUnit.SECONDS));
+    }
+
+    private static CountDownLatch occupy(ExecutorService single) throws 
InterruptedException {
+        CountDownLatch busy = new CountDownLatch(1);
+        CountDownLatch release = new CountDownLatch(1);
+        // keep the only thread busy so the next task stays queued
+        single.execute(() -> {
+            busy.countDown();
+            try {
+                release.await();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        });
+        assertTrue(busy.await(10, TimeUnit.SECONDS));
+        return release;
+    }
+
+    private static void assertRejected(Future<?> future) {
+        ExecutionException e = assertThrows(ExecutionException.class, () -> 
future.get(10, TimeUnit.SECONDS));
+        assertInstanceOf(RejectedExecutionException.class, e.getCause());
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start").to("mock:result").transform(constant("Bye 
World"));
+            }
+        };
+    }
+
+    private static final class GatedExecutorService extends 
AbstractExecutorService {
+        private final CountDownLatch gate = new CountDownLatch(1);
+        private volatile Thread thread;
+
+        void open() {
+            gate.countDown();
+        }
+
+        @Override
+        public void execute(Runnable command) {
+            thread = new Thread(() -> {
+                try {
+                    gate.await();
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+                command.run();
+            });
+            thread.start();
+        }
+
+        @Override
+        public void shutdown() {
+            // keeps running the task
+        }
+
+        @Override
+        public List<Runnable> shutdownNow() {
+            return List.of();
+        }
+
+        @Override
+        public boolean isShutdown() {
+            return false;
+        }
+
+        @Override
+        public boolean isTerminated() {
+            return false;
+        }
+
+        @Override
+        public boolean awaitTermination(long timeout, TimeUnit unit) {
+            return true;
+        }
+    }
+}

Reply via email to