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 59203c7e32a2 CAMEL-26555: camel-core - DefaultShutdownStrategy: a 
shutdown task cancelled on timeout must not stop the deferred consumers (#29115)
59203c7e32a2 is described below

commit 59203c7e32a2337d9f7b5e47e2502003cdddcbbe
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 4 20:00:45 2026 +0100

    CAMEL-26555: camel-core - DefaultShutdownStrategy: a shutdown task 
cancelled on timeout must not stop the deferred consumers (#29115)
    
    - CAMEL-26555: camel-core - DefaultShutdownStrategy: a shutdown task 
cancelled on timeout must not stop the deferred consumers
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/impl/engine/DefaultShutdownStrategy.java |  17 ++++
 ...ltShutdownStrategyTimeoutTaskCancelledTest.java | 105 +++++++++++++++++++++
 2 files changed, 122 insertions(+)

diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultShutdownStrategy.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultShutdownStrategy.java
index 67cbe7bff753..ae8df13f0546 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultShutdownStrategy.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultShutdownStrategy.java
@@ -739,6 +739,10 @@ public class DefaultShutdownStrategy extends 
ServiceSupport implements ShutdownS
                         TimeSource.sleep(loopDelaySeconds * 1000);
                     } catch (InterruptedException e) {
                         Thread.currentThread().interrupt();
+                        if (timeoutOccurred.get()) {
+                            // the timeout of this shutdown cancelled this 
task (see below)
+                            break;
+                        }
                         if (abortAfterTimeout) {
                             LOG.warn("Interrupted while waiting during 
graceful shutdown, will abort.");
                             return;
@@ -752,6 +756,14 @@ public class DefaultShutdownStrategy extends 
ServiceSupport implements ShutdownS
                 }
             }
 
+            if (timeoutOccurred.get()) {
+                // the shutdown timed out: the thread that called it has 
cancelled this task, and forces, aborts or
+                // ignores the remainder routes, so this task must not stop 
the deferred consumers as well (at the
+                // same time, and on an interrupted thread where a consumer 
can fail to stop)
+                LOG.debug("Timeout occurred during graceful shutdown, the 
shutdown task stops now");
+                return;
+            }
+
             // prepare for shutdown
             for (ShutdownDeferredConsumer deferred : deferredConsumers) {
                 Consumer consumer = deferred.getConsumer();
@@ -766,6 +778,11 @@ public class DefaultShutdownStrategy extends 
ServiceSupport implements ShutdownS
 
             // now all messages has been completed then stop the deferred 
consumers
             for (ShutdownDeferredConsumer deferred : deferredConsumers) {
+                if (timeoutOccurred.get()) {
+                    // the shutdown timed out meanwhile, and the thread that 
called it shuts down the routes now
+                    LOG.debug("Timeout occurred during graceful shutdown, the 
shutdown task stops now");
+                    return;
+                }
                 Consumer consumer = deferred.getConsumer();
                 if (suspendOnly) {
                     suspendNow(deferred.getRoute().getId(), consumer);
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultShutdownStrategyTimeoutTaskCancelledTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultShutdownStrategyTimeoutTaskCancelledTest.java
new file mode 100644
index 000000000000..02e573247f19
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultShutdownStrategyTimeoutTaskCancelledTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.engine;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Consumer;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.ShutdownRoute;
+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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * When the graceful shutdown of a route times out, the thread that stops the 
route forces it to shutdown. The shutdown
+ * task, cancelled (interrupted) by the timeout, must not stop the deferred 
consumer as well: at the same time, and on
+ * an interrupted thread where a consumer can fail to stop.
+ */
+class DefaultShutdownStrategyTimeoutTaskCancelledTest extends 
ContextTestSupport {
+
+    private final CountDownLatch release = new CountDownLatch(1);
+    // the consumers stopped by the shutdown strategy: the thread, and whether 
it was interrupted
+    private final List<String> stops = new CopyOnWriteArrayList<>();
+
+    @AfterEach
+    void releaseExchange() {
+        release.countDown();
+    }
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+        context.setShutdownStrategy(new RecordingShutdownStrategy(context, 
stops));
+        return context;
+    }
+
+    @Test
+    void testCancelledTaskDoesNotStopDeferredConsumer() throws Exception {
+        template.sendBody("seda:a", "A");
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
context.getInflightRepository().size("a") == 1);
+
+        // the exchange is still in flight, so the graceful shutdown times out 
and the route is forced to shutdown
+        String thread = Thread.currentThread().getName();
+        context.getRouteController().stopRoute("a", 1, TimeUnit.SECONDS);
+        assertTrue(context.getShutdownStrategy().isTimeoutOccurred());
+        
assertTrue(context.getRouteController().getRouteStatus("a").isStopped());
+
+        // the cancelled task would stop the deferred consumer right after the 
interrupt
+        await().during(1, TimeUnit.SECONDS).atMost(2, TimeUnit.SECONDS)
+                .untilAsserted(() -> assertEquals(List.of(thread), stops,
+                        "the consumer must be stopped once, by the thread that 
stopped the route"));
+        assertFalse(Thread.currentThread().isInterrupted());
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("seda:a").routeId("a").shutdownRoute(ShutdownRoute.Defer)
+                        .process(exchange -> release.await(30, 
TimeUnit.SECONDS));
+            }
+        };
+    }
+
+    // a named class, as the shutdown strategy names its thread pool after its 
class
+    private static class RecordingShutdownStrategy extends 
DefaultShutdownStrategy {
+
+        private final List<String> stops;
+
+        RecordingShutdownStrategy(CamelContext camelContext, List<String> 
stops) {
+            super(camelContext);
+            this.stops = stops;
+        }
+
+        @Override
+        protected void shutdownNow(String routeId, Consumer consumer) {
+            stops.add(Thread.currentThread().getName() + 
(Thread.currentThread().isInterrupted() ? " interrupted" : ""));
+            super.shutdownNow(routeId, consumer);
+        }
+    }
+}

Reply via email to