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 930d4d31dbcb CAMEL-26422: camel-resilience4j,
camel-microprofile-fault-tolerance - a circuit breaker no longer ends the
enclosing doTry (#28780)
930d4d31dbcb is described below
commit 930d4d31dbcbc608c4d66d58780b9ee8eeb964d1
Author: allthingssecurity <[email protected]>
AuthorDate: Sun Nov 1 15:18:22 2026 +0530
CAMEL-26422: camel-resilience4j, camel-microprofile-fault-tolerance - a
circuit breaker no longer ends the enclosing doTry (#28780)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: GPT-6 <[email protected]>
---
.../faulttolerance/FaultToleranceProcessor.java | 17 ++-
.../FaultToleranceOuterTryScopeTest.java | 128 +++++++++++++++++
.../resilience4j/ResilienceProcessor.java | 26 +++-
.../resilience4j/ResilienceOuterTryScopeTest.java | 152 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 11 ++
5 files changed, 328 insertions(+), 6 deletions(-)
diff --git
a/components/camel-microprofile/camel-microprofile-fault-tolerance/src/main/java/org/apache/camel/component/microprofile/faulttolerance/FaultToleranceProcessor.java
b/components/camel-microprofile/camel-microprofile-fault-tolerance/src/main/java/org/apache/camel/component/microprofile/faulttolerance/FaultToleranceProcessor.java
index 31fb84bc7213..6044525d355e 100644
---
a/components/camel-microprofile/camel-microprofile-fault-tolerance/src/main/java/org/apache/camel/component/microprofile/faulttolerance/FaultToleranceProcessor.java
+++
b/components/camel-microprofile/camel-microprofile-fault-tolerance/src/main/java/org/apache/camel/component/microprofile/faulttolerance/FaultToleranceProcessor.java
@@ -281,6 +281,8 @@ public class FaultToleranceProcessor extends
BaseProcessorSupport
@SuppressWarnings("unchecked")
public boolean process(Exchange exchange, AsyncCallback callback) {
// run this as if we run inside try / catch so there is no regular
Camel error handler
+ Object previousTryRouteBlock =
exchange.getProperty(ExchangePropertyKey.TRY_ROUTE_BLOCK);
+ Object previousHandled =
exchange.removeProperty(ExchangePropertyKey.EXCEPTION_HANDLED);
exchange.setProperty(ExchangePropertyKey.TRY_ROUTE_BLOCK, true);
CircuitBreakerTask task = (CircuitBreakerTask)
taskFactory.acquire(exchange, callback);
// guard to prevent the worker thread from writing results back to the
original exchange
@@ -342,11 +344,24 @@ public class FaultToleranceProcessor extends
BaseProcessorSupport
}
}
- exchange.removeProperty(ExchangePropertyKey.TRY_ROUTE_BLOCK);
+ restoreScopeProperty(exchange, ExchangePropertyKey.TRY_ROUTE_BLOCK,
previousTryRouteBlock);
+ restoreScopeProperty(exchange, ExchangePropertyKey.EXCEPTION_HANDLED,
previousHandled);
callback.done(true);
return true;
}
+ /**
+ * Sets the property back to the value of the enclosing scope, or removes
it when that scope had none (as
+ * {@code TryProcessor} does for a nested doTry).
+ */
+ private static void restoreScopeProperty(Exchange exchange,
ExchangePropertyKey key, Object value) {
+ if (value != null) {
+ exchange.setProperty(key, value);
+ } else {
+ exchange.removeProperty(key);
+ }
+ }
+
@Override
protected void doBuild() throws Exception {
ObjectHelper.notNull(camelContext, "CamelContext", this);
diff --git
a/components/camel-microprofile/camel-microprofile-fault-tolerance/src/test/java/org/apache/camel/component/microprofile/faulttolerance/FaultToleranceOuterTryScopeTest.java
b/components/camel-microprofile/camel-microprofile-fault-tolerance/src/test/java/org/apache/camel/component/microprofile/faulttolerance/FaultToleranceOuterTryScopeTest.java
new file mode 100644
index 000000000000..4267fb19afd8
--- /dev/null
+++
b/components/camel-microprofile/camel-microprofile-fault-tolerance/src/test/java/org/apache/camel/component/microprofile/faulttolerance/FaultToleranceOuterTryScopeTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.microprofile.faulttolerance;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class FaultToleranceOuterTryScopeTest extends CamelTestSupport {
+ private final AtomicInteger attempts = new AtomicInteger();
+
+ @Test
+ void synchronousCircuitBreakerPreservesOuterTry() throws Exception {
+ check("direct:sync");
+ }
+
+ @Test
+ void fallbackPreservesOuterTry() throws Exception {
+ check("direct:fallback");
+ }
+
+ @Test
+ void laterFailureAfterRecoveredFallbackIsCaught() throws Exception {
+ getMockEndpoint("mock:caught").expectedMessageCount(1);
+ Exchange out = template.send("direct:afterFallback", e ->
e.getMessage().setBody("Hello"));
+ MockEndpoint.assertIsSatisfied(context);
+ assertNull(out.getException(), "the doCatch must catch the failure
after the fallback");
+ }
+
+ @Test
+ void failingFallbackIsCaught() throws Exception {
+ // the fallback itself fails: the doCatch catches it, as for a circuit
breaker without a fallback
+ getMockEndpoint("mock:caught").expectedMessageCount(1);
+ Exchange out = template.send("direct:failingFallback", e ->
e.getMessage().setBody("Hello"));
+ MockEndpoint.assertIsSatisfied(context);
+ assertNull(out.getException(), "the doCatch must catch the failure of
the fallback");
+ }
+
+ @Test
+ void circuitBreakerInDoCatchKeepsTheFailureHandled() throws Exception {
+ // a failure after the circuit breaker, inside the doCatch, is not
caught by a later doCatch of the same doTry
+ getMockEndpoint("mock:first").expectedMessageCount(1);
+ getMockEndpoint("mock:second").expectedMessageCount(0);
+ Exchange out = template.send("direct:inCatch", e ->
e.getMessage().setBody("Hello"));
+ MockEndpoint.assertIsSatisfied(context);
+ assertInstanceOf(IllegalArgumentException.class, out.getException());
+ }
+
+ @Test
+ void outsideTryUsesGlobalErrorHandler() throws Exception {
+ getMockEndpoint("mock:dead").expectedMessageCount(1);
+ template.sendBody("direct:outside", "Hello");
+ MockEndpoint.assertIsSatisfied(context);
+ assertEquals(3, attempts.get());
+ assertEquals(0, getMockEndpoint("mock:caught").getReceivedCounter());
+ }
+
+ private void check(String uri) throws Exception {
+ getMockEndpoint("mock:caught").expectedMessageCount(1);
+ getMockEndpoint("mock:dead").expectedMessageCount(0);
+ template.sendBody(uri, "Hello");
+ MockEndpoint.assertIsSatisfied(context);
+ assertEquals(1, attempts.get(), "The outer doTry owns the recipient
list failure");
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
errorHandler(deadLetterChannel("mock:dead").maximumRedeliveries(2).redeliveryDelay(0));
+ from("direct:fail").errorHandler(noErrorHandler()).process(e
-> {
+ attempts.incrementAndGet();
+ throw new IllegalStateException("Failure after circuit
breaker");
+ });
+ from("direct:outside").circuitBreaker().to("mock:inside").end()
+ .recipientList(constant("direct:fail"));
+ from("direct:sync").doTry()
+ .circuitBreaker().to("mock:inside").end()
+ .recipientList(constant("direct:fail")).endDoTry()
+
.doCatch(IllegalStateException.class).to("mock:caught").end();
+ from("direct:fallback").doTry()
+ .circuitBreaker().throwException(new
IllegalArgumentException("Recover me"))
+ .onFallback().setBody(constant("Recovered")).end()
+ .recipientList(constant("direct:fail")).endDoTry()
+
.doCatch(IllegalStateException.class).to("mock:caught").end();
+ from("direct:afterFallback").doTry()
+ .circuitBreaker().throwException(new
IllegalArgumentException("Recover me"))
+ .onFallback().setBody(constant("Recovered")).end()
+ .throwException(new IllegalStateException("Failure after
the fallback")).endDoTry()
+
.doCatch(IllegalStateException.class).to("mock:caught").end();
+ from("direct:failingFallback").doTry()
+ .circuitBreaker().throwException(new
IllegalArgumentException("Recover me"))
+ .onFallback().throwException(new
IllegalStateException("The fallback fails")).end()
+ .endDoTry()
+
.doCatch(IllegalStateException.class).to("mock:caught").end();
+ from("direct:inCatch").doTry()
+ .throwException(new IllegalStateException("First failure"))
+ .doCatch(IllegalStateException.class).to("mock:first")
+ .circuitBreaker().to("mock:inside").end()
+ .throwException(new IllegalArgumentException("Failure
in the doCatch")).endDoTry()
+ .doCatch(Exception.class).to("mock:second").end();
+ }
+ };
+ }
+}
diff --git
a/components/camel-resilience4j/src/main/java/org/apache/camel/component/resilience4j/ResilienceProcessor.java
b/components/camel-resilience4j/src/main/java/org/apache/camel/component/resilience4j/ResilienceProcessor.java
index 792964d56106..c8dcdc38ff72 100644
---
a/components/camel-resilience4j/src/main/java/org/apache/camel/component/resilience4j/ResilienceProcessor.java
+++
b/components/camel-resilience4j/src/main/java/org/apache/camel/component/resilience4j/ResilienceProcessor.java
@@ -566,12 +566,31 @@ public class ResilienceProcessor extends
BaseProcessorSupport
public boolean process(Exchange exchange, AsyncCallback callback) {
// run this as if we run inside try .. catch so there is no regular
// Camel error handler
+ Object previousTryRouteBlock =
exchange.getProperty(ExchangePropertyKey.TRY_ROUTE_BLOCK);
+ Object previousHandled =
exchange.removeProperty(ExchangePropertyKey.EXCEPTION_HANDLED);
exchange.setProperty(ExchangePropertyKey.TRY_ROUTE_BLOCK, true);
+ AsyncCallback restoreScope = doneSync -> {
+ restoreScopeProperty(exchange,
ExchangePropertyKey.TRY_ROUTE_BLOCK, previousTryRouteBlock);
+ restoreScopeProperty(exchange,
ExchangePropertyKey.EXCEPTION_HANDLED, previousHandled);
+ callback.done(doneSync);
+ };
if (asynchronous) {
- return processAsync(exchange, callback);
+ return processAsync(exchange, restoreScope);
+ }
+ return processSync(exchange, restoreScope);
+ }
+
+ /**
+ * Sets the property back to the value of the enclosing scope, or removes
it when that scope had none (as
+ * {@code TryProcessor} does for a nested doTry).
+ */
+ private static void restoreScopeProperty(Exchange exchange,
ExchangePropertyKey key, Object value) {
+ if (value != null) {
+ exchange.setProperty(key, value);
+ } else {
+ exchange.removeProperty(key);
}
- return processSync(exchange, callback);
}
private boolean processSync(Exchange exchange, AsyncCallback callback) {
@@ -632,7 +651,6 @@ public class ResilienceProcessor extends
BaseProcessorSupport
failed);
}
- exchange.removeProperty(ExchangePropertyKey.TRY_ROUTE_BLOCK);
callback.done(true);
return true;
}
@@ -696,7 +714,6 @@ public class ResilienceProcessor extends
BaseProcessorSupport
exchange.getExchangeId(), id, failed);
}
- exchange.removeProperty(ExchangePropertyKey.TRY_ROUTE_BLOCK);
callback.done(false);
});
return false;
@@ -706,7 +723,6 @@ public class ResilienceProcessor extends
BaseProcessorSupport
if (fallbackTask != null) {
fallbackTaskFactory.release(fallbackTask);
}
- exchange.removeProperty(ExchangePropertyKey.TRY_ROUTE_BLOCK);
callback.done(true);
return true;
}
diff --git
a/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/ResilienceOuterTryScopeTest.java
b/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/ResilienceOuterTryScopeTest.java
new file mode 100644
index 000000000000..c139bc573084
--- /dev/null
+++
b/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/ResilienceOuterTryScopeTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.resilience4j;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class ResilienceOuterTryScopeTest extends CamelTestSupport {
+ private final AtomicInteger attempts = new AtomicInteger();
+
+ @Test
+ void synchronousCircuitBreakerPreservesOuterTry() throws Exception {
+ check("direct:sync");
+ }
+
+ @Test
+ void asynchronousCircuitBreakerPreservesOuterTry() throws Exception {
+ check("direct:async");
+ }
+
+ @Test
+ void fallbackPreservesOuterTry() throws Exception {
+ check("direct:fallback");
+ }
+
+ @Test
+ void laterFailureAfterRecoveredFallbackIsCaught() throws Exception {
+ getMockEndpoint("mock:caught").expectedMessageCount(1);
+ Exchange out = template.send("direct:afterFallback", e ->
e.getMessage().setBody("Hello"));
+ MockEndpoint.assertIsSatisfied(context);
+ assertNull(out.getException(), "the doCatch must catch the failure
after the fallback");
+ }
+
+ @Test
+ void asynchronousLaterFailureAfterRecoveredFallbackIsCaught() throws
Exception {
+ getMockEndpoint("mock:caught").expectedMessageCount(1);
+ Exchange out = template.send("direct:asyncAfterFallback", e ->
e.getMessage().setBody("Hello"));
+ MockEndpoint.assertIsSatisfied(context);
+ assertNull(out.getException(), "the doCatch must catch the failure
after the asynchronous fallback");
+ }
+
+ @Test
+ void failingFallbackIsCaught() throws Exception {
+ // the fallback itself fails: the doCatch catches it, as for a circuit
breaker without a fallback
+ getMockEndpoint("mock:caught").expectedMessageCount(1);
+ Exchange out = template.send("direct:failingFallback", e ->
e.getMessage().setBody("Hello"));
+ MockEndpoint.assertIsSatisfied(context);
+ assertNull(out.getException(), "the doCatch must catch the failure of
the fallback");
+ }
+
+ @Test
+ void circuitBreakerInDoCatchKeepsTheFailureHandled() throws Exception {
+ // a failure after the circuit breaker, inside the doCatch, is not
caught by a later doCatch of the same doTry
+ getMockEndpoint("mock:first").expectedMessageCount(1);
+ getMockEndpoint("mock:second").expectedMessageCount(0);
+ Exchange out = template.send("direct:inCatch", e ->
e.getMessage().setBody("Hello"));
+ MockEndpoint.assertIsSatisfied(context);
+ assertInstanceOf(IllegalArgumentException.class, out.getException());
+ }
+
+ @Test
+ void outsideTryUsesGlobalErrorHandler() throws Exception {
+ getMockEndpoint("mock:dead").expectedMessageCount(1);
+ template.sendBody("direct:outside", "Hello");
+ MockEndpoint.assertIsSatisfied(context);
+ assertEquals(3, attempts.get());
+ assertEquals(0, getMockEndpoint("mock:caught").getReceivedCounter());
+ }
+
+ private void check(String uri) throws Exception {
+ getMockEndpoint("mock:caught").expectedMessageCount(1);
+ getMockEndpoint("mock:dead").expectedMessageCount(0);
+ template.sendBody(uri, "Hello");
+ MockEndpoint.assertIsSatisfied(context);
+ assertEquals(1, attempts.get(), "The outer doTry owns the recipient
list failure");
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
errorHandler(deadLetterChannel("mock:dead").maximumRedeliveries(2).redeliveryDelay(0));
+ from("direct:fail").errorHandler(noErrorHandler()).process(e
-> {
+ attempts.incrementAndGet();
+ throw new IllegalStateException("Failure after circuit
breaker");
+ });
+ from("direct:outside").circuitBreaker().to("mock:inside").end()
+ .recipientList(constant("direct:fail"));
+ from("direct:sync").doTry()
+ .circuitBreaker().to("mock:inside").end()
+ .recipientList(constant("direct:fail")).endDoTry()
+
.doCatch(IllegalStateException.class).to("mock:caught").end();
+ from("direct:async").doTry()
+
.circuitBreaker().resilience4jConfiguration().asynchronous(true).end()
+ .to("mock:inside").end()
+ .recipientList(constant("direct:fail")).endDoTry()
+
.doCatch(IllegalStateException.class).to("mock:caught").end();
+ from("direct:fallback").doTry()
+ .circuitBreaker().throwException(new
IllegalArgumentException("Recover me"))
+ .onFallback().setBody(constant("Recovered")).end()
+ .recipientList(constant("direct:fail")).endDoTry()
+
.doCatch(IllegalStateException.class).to("mock:caught").end();
+ from("direct:afterFallback").doTry()
+ .circuitBreaker().throwException(new
IllegalArgumentException("Recover me"))
+ .onFallback().setBody(constant("Recovered")).end()
+ .throwException(new IllegalStateException("Failure after
the fallback")).endDoTry()
+
.doCatch(IllegalStateException.class).to("mock:caught").end();
+ from("direct:asyncAfterFallback").doTry()
+
.circuitBreaker().resilience4jConfiguration().asynchronous(true).end()
+ .throwException(new IllegalArgumentException("Recover
me"))
+ .onFallback().setBody(constant("Recovered")).end()
+ .throwException(new IllegalStateException("Failure after
the fallback")).endDoTry()
+
.doCatch(IllegalStateException.class).to("mock:caught").end();
+ from("direct:failingFallback").doTry()
+ .circuitBreaker().throwException(new
IllegalArgumentException("Recover me"))
+ .onFallback().throwException(new
IllegalStateException("The fallback fails")).end()
+ .endDoTry()
+
.doCatch(IllegalStateException.class).to("mock:caught").end();
+ from("direct:inCatch").doTry()
+ .throwException(new IllegalStateException("First failure"))
+ .doCatch(IllegalStateException.class).to("mock:first")
+ .circuitBreaker().to("mock:inside").end()
+ .throwException(new IllegalArgumentException("Failure
in the doCatch")).endDoTry()
+ .doCatch(Exception.class).to("mock:second").end();
+ }
+ };
+ }
+}
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 a827b2f9e442..e62114bd364f 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
@@ -489,6 +489,17 @@ which embeds its own relocated copy of Vert.x. The `vertx`
option of `camel-aran
See the
https://github.com/fabric8io/kubernetes-client/blob/main/doc/MIGRATION-v8.md[Kubernetes
client 8 migration guide]
for the other changes.
+=== camel-resilience4j, camel-microprofile-fault-tolerance - a circuit breaker
in a doTry
+
+A circuit breaker in the steps of a `doTry` no longer ends that `doTry` for
the steps after it. Before, when the
+circuit breaker was done, a later routing slip, dynamic router, multicast or
recipient list of the same `doTry` used
+the error handler of the route (redelivery and the dead letter channel)
instead of the `doCatch`. And after the
+fallback of the circuit breaker had recovered from a failure, no `doCatch` of
that `doTry` caught a later failure:
+the failure was thrown from the `doTry`. Now such failures go to the
`doCatch`, as for the other steps of the `doTry`.
+A failure of the fallback itself is now also caught by the `doCatch` (before,
it was thrown from the `doTry`).
+
+To have those steps use the error handler of the route, move them out of the
`doTry` block.
+
=== camel-aws2-kinesis - each endpoint uses its own client
Each `aws2-kinesis` endpoint now uses the Kinesis client of its own
configuration (`region`, credentials,