From 5d8407ade46708a901b6d51f46ce0bb3c52d5179 Mon Sep 17 00:00:00 2001 From: gnodet Date: Tue, 20 Oct 2026 06:50:12 +0000 Subject: [PATCH 1/6] CAMEL-25823: camel-core - Defer error handler start in MulticastProcessor from doInit to doStart MulticastProcessor.doInit() calls wrapInErrorHandler() for each child processor, which eagerly called ServiceHelper.startService(answer) on the wrapped error handler. This started the child's SendProcessor (and its producer) before CamelContext was fully started, outside the SupervisingRouteController retry scope. A producer that cannot connect yet (e.g. broker is down) would propagate the exception directly to context.start(), failing the entire context. Fix: defer the ServiceHelper.startService(answer) call in wrapInErrorHandler to when the context is already fully started (isStarted()). When wrapInErrorHandler is called during doInit() (context is STARTING), the built error handler is cached but not started. doStart() then explicitly starts any pre-cached error handlers via ServiceHelper.startService(errorHandlers.values()), which runs under the route lifecycle managed by the SupervisingRouteController. Split extends MulticastProcessor and is also fixed by this change. Co-authored-by: Claude Sonnet 4.6 --- .../camel/processor/MulticastProcessor.java | 15 +- ...singRouteControllerMulticastSplitTest.java | 174 ++++++++++++++++++ 2 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerMulticastSplitTest.java diff --git a/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java b/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java index 243129e08ac9e..e52414680365e 100644 --- a/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java +++ b/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java @@ -1210,13 +1210,17 @@ protected Processor wrapInErrorHandler(Route route, Exchange exchange, Processor // and wrap in unit of work processor so the copy exchange also can run under UoW answer = createUnitOfWorkProcessor(route, processor, exchange); - // must start the error handler - ServiceHelper.startService(answer); - if (errorHandlers != null) { errorHandlers.putIfAbsent(key, answer); } + // start the error handler now if the context is already started, otherwise it will be + // started in doStart() to avoid starting producers before CamelContext is fully started + // (which would bypass the SupervisingRouteController retry logic) + if (getCamelContext().isStarted()) { + ServiceHelper.startService(answer); + } + } catch (Exception e) { throw RuntimeCamelException.wrapRuntimeCamelException(e); } @@ -1278,6 +1282,11 @@ protected void doStart() throws Exception { } CamelContextAware.trySetCamelContext(aggregationStrategy, camelContext); ServiceHelper.startService(aggregationStrategy, processors, processorExchangeFactory); + // start any error handlers that were pre-built during doInit() but not yet started + // (they were deferred to avoid starting producers before CamelContext is fully started) + if (errorHandlers != null) { + ServiceHelper.startService(errorHandlers.values()); + } } /** diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerMulticastSplitTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerMulticastSplitTest.java new file mode 100644 index 0000000000000..257a69f8fb4dd --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerMulticastSplitTest.java @@ -0,0 +1,174 @@ +/* + * 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.time.Duration; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.camel.CamelContext; +import org.apache.camel.Component; +import org.apache.camel.Consumer; +import org.apache.camel.ContextTestSupport; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; +import org.apache.camel.Processor; +import org.apache.camel.Producer; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.spi.SupervisingRouteController; +import org.apache.camel.support.DefaultComponent; +import org.apache.camel.support.DefaultConsumer; +import org.apache.camel.support.DefaultEndpoint; +import org.apache.camel.support.DefaultProducer; +import org.junit.jupiter.api.Test; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests that Split and Multicast EIPs do not start child producers during doInit(), which would bypass the + * SupervisingRouteController retry logic and fail the entire context when a producer cannot connect yet. + *

+ * CAMEL-25823: Split and Multicast start their child producers before CamelContext starts, so with the supervising + * route controller a producer that cannot start yet fails the whole context. + */ +class DefaultSupervisingRouteControllerMulticastSplitTest extends ContextTestSupport { + + @Override + public boolean isUseRouteBuilder() { + return false; + } + + @Override + protected CamelContext createCamelContext() throws Exception { + CamelContext context = super.createCamelContext(); + SupervisingRouteController src = context.getRouteController().supervising(); + src.setBackOffDelay(25); + src.setBackOffMaxAttempts(10); + src.setInitialDelay(10); + src.setThreadPoolSize(2); + return context; + } + + @Test + void testSplitWithFlakyProducer() throws Exception { + context.addComponent("flaky", new FlakyComponent(2)); + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("direct:split") + .routeId("split-route") + .split(body()) + .to("flaky:target"); + } + }); + + // Must not throw - supervising route controller should retry + context.start(); + + // Route should eventually reach Started state after retries + await().atMost(Duration.ofSeconds(5)) + .untilAsserted(() -> assertEquals("Started", + context.getRouteController().getRouteStatus("split-route").toString())); + } + + @Test + void testMulticastWithFlakyProducer() throws Exception { + context.addComponent("flaky", new FlakyComponent(2)); + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("direct:multicast") + .routeId("multicast-route") + .multicast() + .to("flaky:target"); + } + }); + + // Must not throw - supervising route controller should retry + context.start(); + + // Route should eventually reach Started state after retries + await().atMost(Duration.ofSeconds(5)) + .untilAsserted(() -> assertEquals("Started", + context.getRouteController().getRouteStatus("multicast-route").toString())); + } + + /** + * A component whose producer fails its first {@code failCount} doStart() calls with an exception, simulating a + * broker that is temporarily unavailable. The failure count is tracked on the endpoint (shared across producer + * instances) so retries eventually succeed even when a new producer is created on each restart attempt. + */ + static class FlakyComponent extends DefaultComponent { + + private final int failCount; + + FlakyComponent(int failCount) { + this.failCount = failCount; + } + + @Override + protected Endpoint createEndpoint(String uri, String remaining, Map parameters) { + return new FlakyEndpoint(uri, this, failCount); + } + } + + static class FlakyEndpoint extends DefaultEndpoint { + + private final AtomicInteger startAttempts = new AtomicInteger(); + private final int failCount; + + FlakyEndpoint(String uri, Component component, int failCount) { + super(uri, component); + this.failCount = failCount; + } + + @Override + public Producer createProducer() { + return new FlakyProducer(this); + } + + @Override + public Consumer createConsumer(Processor processor) { + return new DefaultConsumer(this, processor); + } + + boolean shouldFail() { + return startAttempts.incrementAndGet() <= failCount; + } + } + + static class FlakyProducer extends DefaultProducer { + + FlakyProducer(FlakyEndpoint endpoint) { + super(endpoint); + } + + @Override + protected void doStart() throws Exception { + FlakyEndpoint ep = (FlakyEndpoint) getEndpoint(); + if (ep.shouldFail()) { + throw new IllegalStateException("Cannot connect"); + } + } + + @Override + public void process(Exchange exchange) { + // noop + } + } +} From 6dde78bb975c19d08a5c8ebb5dfa999cd82ecf96 Mon Sep 17 00:00:00 2001 From: gnodet Date: Wed, 21 Oct 2026 12:30:46 +0000 Subject: [PATCH 2/6] CAMEL-25823: address review: use isRunAllowed() gate, fix errorHandlers lifecycle, improve test and upgrade guide --- .../camel/processor/MulticastProcessor.java | 12 +++--- ...singRouteControllerMulticastSplitTest.java | 37 +++++++++++++++---- .../pages/camel-4x-upgrade-guide-4_24.adoc | 11 ++++++ 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java b/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java index e52414680365e..1e660f213dc35 100644 --- a/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java +++ b/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java @@ -1214,10 +1214,12 @@ protected Processor wrapInErrorHandler(Route route, Exchange exchange, Processor errorHandlers.putIfAbsent(key, answer); } - // start the error handler now if the context is already started, otherwise it will be + // start the error handler now if this processor is already running, otherwise it will be // started in doStart() to avoid starting producers before CamelContext is fully started - // (which would bypass the SupervisingRouteController retry logic) - if (getCamelContext().isStarted()) { + // (which would bypass the SupervisingRouteController retry logic). + // Using isRunAllowed() instead of getCamelContext().isStarted() covers the runtime case + // (e.g. RecipientListProcessor builds wrappers during Starting and Stopping phases). + if (isRunAllowed()) { ServiceHelper.startService(answer); } @@ -1308,12 +1310,12 @@ protected ExecutorService createAggregateExecutorService(String name) { @Override protected void doStop() throws Exception { - ServiceHelper.stopService(processors, errorHandlers, aggregationStrategy, processorExchangeFactory); + ServiceHelper.stopService(processors, errorHandlers != null ? errorHandlers.values() : null, aggregationStrategy, processorExchangeFactory); } @Override protected void doShutdown() throws Exception { - ServiceHelper.stopAndShutdownServices(processors, errorHandlers, aggregationStrategy, processorExchangeFactory); + ServiceHelper.stopAndShutdownServices(processors, errorHandlers != null ? errorHandlers.values() : null, aggregationStrategy, processorExchangeFactory); // only clear error handlers when shutting down if (errorHandlers != null) { errorHandlers.clear(); diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerMulticastSplitTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerMulticastSplitTest.java index 257a69f8fb4dd..770d0e3061717 100644 --- a/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerMulticastSplitTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerMulticastSplitTest.java @@ -28,6 +28,7 @@ import org.apache.camel.Exchange; import org.apache.camel.Processor; import org.apache.camel.Producer; +import org.apache.camel.ServiceStatus; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.spi.SupervisingRouteController; import org.apache.camel.support.DefaultComponent; @@ -66,7 +67,8 @@ protected CamelContext createCamelContext() throws Exception { @Test void testSplitWithFlakyProducer() throws Exception { - context.addComponent("flaky", new FlakyComponent(2)); + FlakyComponent component = new FlakyComponent(2); + context.addComponent("flaky", component); context.addRoutes(new RouteBuilder() { @Override public void configure() { @@ -82,13 +84,22 @@ public void configure() { // Route should eventually reach Started state after retries await().atMost(Duration.ofSeconds(5)) - .untilAsserted(() -> assertEquals("Started", - context.getRouteController().getRouteStatus("split-route").toString())); + .untilAsserted(() -> assertEquals(ServiceStatus.Started, + context.getRouteController().getRouteStatus("split-route"))); + + // Verify the producer was retried (failCount=2 means 3 start attempts total) + FlakyEndpoint endpoint = (FlakyEndpoint) context.getEndpoint("flaky:target"); + assertEquals(3, endpoint.startAttempts.get(), "Expected 3 start attempts (2 failures + 1 success)"); + + // Verify that a message can actually be processed once the route is started + template.sendBody("direct:split", "a b c"); + assertEquals(3, endpoint.processCount.get(), "Expected 3 process calls for 3 split tokens"); } @Test void testMulticastWithFlakyProducer() throws Exception { - context.addComponent("flaky", new FlakyComponent(2)); + FlakyComponent component = new FlakyComponent(2); + context.addComponent("flaky", component); context.addRoutes(new RouteBuilder() { @Override public void configure() { @@ -104,8 +115,16 @@ public void configure() { // Route should eventually reach Started state after retries await().atMost(Duration.ofSeconds(5)) - .untilAsserted(() -> assertEquals("Started", - context.getRouteController().getRouteStatus("multicast-route").toString())); + .untilAsserted(() -> assertEquals(ServiceStatus.Started, + context.getRouteController().getRouteStatus("multicast-route"))); + + // Verify the producer was retried (failCount=2 means 3 start attempts total) + FlakyEndpoint endpoint = (FlakyEndpoint) context.getEndpoint("flaky:target"); + assertEquals(3, endpoint.startAttempts.get(), "Expected 3 start attempts (2 failures + 1 success)"); + + // Verify that a message can actually be processed once the route is started + template.sendBody("direct:multicast", "hello"); + assertEquals(1, endpoint.processCount.get(), "Expected 1 process call for multicast to single target"); } /** @@ -129,7 +148,8 @@ protected Endpoint createEndpoint(String uri, String remaining, Map `openstackSwiftContainerRead()`, `xContainerWrite()` -> `openstackSwiftContainerWrite()`). +=== camel-core - Split and Multicast child producers start with their route + +`Split` and `Multicast` EIPs now start their child output producers during the route's own start, instead of during +`CamelContext` initialization. The visible changes are: + +* With the default route controller, a producer that fails to start now causes a `FailedToStartRouteException` for its + route, instead of failing the whole context start. The route can be restarted independently. +* Each route's producers now start in `startupOrder` with their route, instead of ahead of all routes. +* With the supervising route controller, the producers of an `autoStartup=false` route are no longer started when the + context starts; they start when the route itself is started. + === camel-pqc - an unknown hybridKdfAlgorithm is rejected The hybrid KEM operations now fail with an `IllegalArgumentException` when `hybridKdfAlgorithm` is not one of From 12ef72de19d2d9ac4123abddf5284edab0fa8a84 Mon Sep 17 00:00:00 2001 From: gnodet Date: Wed, 21 Oct 2026 12:45:39 +0000 Subject: [PATCH 3/6] test: fix split delimiter in testSplitWithFlakyProducer (use comma, not space) --- .../DefaultSupervisingRouteControllerMulticastSplitTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerMulticastSplitTest.java b/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerMulticastSplitTest.java index 770d0e3061717..d080aab40d70f 100644 --- a/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerMulticastSplitTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerMulticastSplitTest.java @@ -92,7 +92,7 @@ public void configure() { assertEquals(3, endpoint.startAttempts.get(), "Expected 3 start attempts (2 failures + 1 success)"); // Verify that a message can actually be processed once the route is started - template.sendBody("direct:split", "a b c"); + template.sendBody("direct:split", "a,b,c"); assertEquals(3, endpoint.processCount.get(), "Expected 3 process calls for 3 split tokens"); } From 95d444a2954c6c06c02bd22bf0bef34605cdb575 Mon Sep 17 00:00:00 2001 From: gnodet Date: Wed, 21 Oct 2026 14:13:40 +0000 Subject: [PATCH 4/6] Address review: fix cache-before-start race, line length, upgrade guide accuracy --- .../camel/processor/MulticastProcessor.java | 25 +++++++++++-------- .../pages/camel-4x-upgrade-guide-4_24.adoc | 8 +++--- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java b/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java index 1e660f213dc35..710843a71000d 100644 --- a/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java +++ b/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java @@ -1210,19 +1210,18 @@ protected Processor wrapInErrorHandler(Route route, Exchange exchange, Processor // and wrap in unit of work processor so the copy exchange also can run under UoW answer = createUnitOfWorkProcessor(route, processor, exchange); - if (errorHandlers != null) { - errorHandlers.putIfAbsent(key, answer); - } - - // start the error handler now if this processor is already running, otherwise it will be - // started in doStart() to avoid starting producers before CamelContext is fully started - // (which would bypass the SupervisingRouteController retry logic). - // Using isRunAllowed() instead of getCamelContext().isStarted() covers the runtime case - // (e.g. RecipientListProcessor builds wrappers during Starting and Stopping phases). + // Start before publishing to the cache: a second exchange to the same new recipient + // can take the cache-hit path and use the wrapper before it is started. + // isRunAllowed() covers STARTING/STARTED/SUSPENDED phases; during doInit() + // (INITIALIZING) it is false, so wrappers built there are deferred to doStart(). if (isRunAllowed()) { ServiceHelper.startService(answer); } + if (errorHandlers != null) { + errorHandlers.putIfAbsent(key, answer); + } + } catch (Exception e) { throw RuntimeCamelException.wrapRuntimeCamelException(e); } @@ -1310,12 +1309,16 @@ protected ExecutorService createAggregateExecutorService(String name) { @Override protected void doStop() throws Exception { - ServiceHelper.stopService(processors, errorHandlers != null ? errorHandlers.values() : null, aggregationStrategy, processorExchangeFactory); + ServiceHelper.stopService( + processors, errorHandlers != null ? errorHandlers.values() : null, + aggregationStrategy, processorExchangeFactory); } @Override protected void doShutdown() throws Exception { - ServiceHelper.stopAndShutdownServices(processors, errorHandlers != null ? errorHandlers.values() : null, aggregationStrategy, processorExchangeFactory); + ServiceHelper.stopAndShutdownServices( + processors, errorHandlers != null ? errorHandlers.values() : null, + aggregationStrategy, processorExchangeFactory); // only clear error handlers when shutting down if (errorHandlers != null) { errorHandlers.clear(); 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 e4be5187bd421..b01d7ad04180f 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 @@ -333,11 +333,11 @@ The generated Endpoint DSL header accessors on the Swift `HeaderNameBuilder` are `Split` and `Multicast` EIPs now start their child output producers during the route's own start, instead of during `CamelContext` initialization. The visible changes are: -* With the default route controller, a producer that fails to start now causes a `FailedToStartRouteException` for its - route, instead of failing the whole context start. The route can be restarted independently. +* The producers now start when the route starts, not during `CamelContext` initialization. A `CamelContext` that is + only built or initialized (without starting routes) no longer starts them. * Each route's producers now start in `startupOrder` with their route, instead of ahead of all routes. -* With the supervising route controller, the producers of an `autoStartup=false` route are no longer started when the - context starts; they start when the route itself is started. +* The producers of an `autoStartup=false` route are no longer started when the context starts; they start when the + route itself is started (regardless of which route controller is used). === camel-pqc - an unknown hybridKdfAlgorithm is rejected From 5074998041a8cf311744c9fa169084e05d4261b6 Mon Sep 17 00:00:00 2001 From: gnodet Date: Wed, 21 Oct 2026 14:24:25 +0000 Subject: [PATCH 5/6] fix: correct isRunAllowed() phase comment to include SUSPENDING --- .../java/org/apache/camel/processor/MulticastProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java b/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java index 710843a71000d..ee9cabf93e935 100644 --- a/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java +++ b/core/camel-core-processor/src/main/java/org/apache/camel/processor/MulticastProcessor.java @@ -1212,7 +1212,7 @@ protected Processor wrapInErrorHandler(Route route, Exchange exchange, Processor // Start before publishing to the cache: a second exchange to the same new recipient // can take the cache-hit path and use the wrapper before it is started. - // isRunAllowed() covers STARTING/STARTED/SUSPENDED phases; during doInit() + // isRunAllowed() covers STARTING/STARTED/SUSPENDING/SUSPENDED phases; during doInit() // (INITIALIZING) it is false, so wrappers built there are deferred to doStart(). if (isRunAllowed()) { ServiceHelper.startService(answer); From 895e821244c32bb4e4a9f18dc5bfc695d93636d3 Mon Sep 17 00:00:00 2001 From: gnodet Date: Fri, 23 Oct 2026 14:00:09 +0000 Subject: [PATCH 6/6] fix: log cut-off WARN in SimpleTask.runNotAllowed when error handler is stopped When MulticastProcessor.doStop() correctly stops its cached error handlers (the Map-to-values() fix), a SimpleTask handling an inflight exchange that was interrupted by a route stop goes through runNotAllowed() instead of runInterrupted(). Unlike runInterrupted(), runNotAllowed() did not call logCutOffByRouteStop(), so the expected WARN was never emitted. Fix: add logCutOffByRouteStop() call in runNotAllowed() for the case where the exchange already carries a RouteStoppingException (it came from an actual interruption, not from a never-started exchange) and is not marked as interrupted (interrupted sub-exchanges are logged via the route-level error handler, avoiding double-logging). --- .../processor/errorhandler/RedeliveryErrorHandler.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java b/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java index f0281e9857b6b..01d7503dd483f 100644 --- a/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java +++ b/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java @@ -860,6 +860,12 @@ private void runNotAllowed() { LOG.trace("Run not allowed, will reject executing exchange: {}", exchange); if (exchange.getException() == null) { exchange.setException(new RouteStoppingException(notAllowedReason())); + } else if (!exchange.getExchangeExtension().isInterrupted()) { + // log the cut-off WARN when the exchange already has a RouteStoppingException and + // is not marked as interrupted: this is the route-level exchange whose error handler + // was stopped (e.g. in MulticastProcessor.doStop()) while a sub-exchange was inflight; + // interrupted sub-exchanges are logged only once through their own path + logCutOffByRouteStop(exchange, exchange.getException(), logger.getLevel()); } AsyncCallback cb = callback; taskFactory.release(this);