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 40a9d1f44299 CAMEL-24863: a failure the circuit breaker's fallback
recovered from is recorded as handled in the error registry
40a9d1f44299 is described below
commit 40a9d1f442990ae89231802817501c20b721f1cb
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Sep 21 12:04:51 2026 +0200
CAMEL-24863: a failure the circuit breaker's fallback recovered from is
recorded as handled in the error registry
## Description
`camel get errors`, the error dev console and the camel-jbang-mcp errors
tool listed every call that failed inside a `circuitBreaker` and was answered
by its `onFallback` as an error that was not handled, one per call. An agent
(or a person) reads that as nine failures of a route that in fact handled all
nine.
Traced against a `doTry`/`doCatch` around a `multicast` whose branch
throws, which the registry records as handled, the difference is in the order
of events, not in the registry:
- The breaker runs the call on a correlated copy of the exchange with its
own unit of work. When the call fails, `doneUow` on the copy (in the breaker's
`finally`) fires the exchange-failed event before the fallback runs, and the
registry records the copy's failure, not handled, correlated to the original.
The breaker then runs the fallback on the original and sets the
exception-caught property, but never says the failure was handled, so nothing
follows. The registry's deduplication k [...]
- With `doCatch` the copy is done after the failure propagated and the
catch fired the handled event on the original, so the original's handled entry
exists first and the copy's unhandled one is dropped.
Two changes:
- **camel-resilience4j**: the breaker emits the failure-handling and
failure-handled events on the original around the fallback, the way
`CatchProcessor` does (the handled event only when the fallback itself did not
fail).
- **camel-base-engine**: when the registry gets a handled failure for an
original whose correlated copy is already recorded as not handled, it marks
that entry handled (`BacklogErrorEventMessage.markHandled()`, new in the API,
`@since 4.23`) instead of dropping the event, so the entry keeps the node and
says the truth.
Live check with a breaker route and a doTry/multicast route side by side,
three calls each: before, breaker entries `handled=false` and multicast entries
`handled=true`; after, all six handled, `camel get errors --handled=false`
empty. Upgrade guide note added.
## Tests
- `ResilienceFallbackErrorRegistryTest` (camel-resilience4j): two failed
calls answered by the fallback give two registry entries, both handled, with
the exception type; fails without the breaker change (verified by reverting it).
-
`ErrorRegistryDeduplicateTest.testCopyEntryIsMarkedHandledWhenTheOriginalRecovers`
(camel-core): the doTry/multicast shape stays one handled entry; a guard for
that path, which passed before as well since there the original's entry comes
first.
- The error registry tests of camel-core (18) and the fallback tests of
camel-resilience4j (15) green.
---
.../resilience4j/ResilienceProcessor.java | 7 ++
.../ResilienceFallbackErrorRegistryTest.java | 79 ++++++++++++++++++++++
.../apache/camel/spi/BacklogErrorEventMessage.java | 8 +++
.../camel/impl/engine/DefaultErrorRegistry.java | 13 +++-
.../camel/impl/ErrorRegistryDeduplicateTest.java | 25 +++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 10 +++
6 files changed, 141 insertions(+), 1 deletion(-)
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 c4cd72ed7b1e..b5024f2371fe 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
@@ -68,6 +68,7 @@ import org.apache.camel.spi.ProcessorExchangeFactory;
import org.apache.camel.spi.RouteIdAware;
import org.apache.camel.spi.UnitOfWork;
import org.apache.camel.support.AsyncProcessorConverterHelper;
+import org.apache.camel.support.EventHelper;
import org.apache.camel.support.ExchangeHelper;
import org.apache.camel.support.PluginHelper;
import org.apache.camel.support.UnitOfWorkHelper;
@@ -1019,8 +1020,14 @@ public class ResilienceProcessor extends
BaseProcessorSupport
LOG.trace("Processing exchange: {} using circuit breaker
({}):{} with fallback: {}",
exchange.getExchangeId(), state, id, fallback);
}
+ // the failure is handled by the fallback: say so with the
events a doCatch emits, so the error
+ // registry (camel get errors, the dev console) records a
recovered failure as handled (CAMEL-24863)
+
EventHelper.notifyExchangeFailureHandling(exchange.getContext(), exchange,
fallback, false, null);
// process the fallback until its fully done
fallback.process(exchange);
+ if (exchange.getException() == null) {
+
EventHelper.notifyExchangeFailureHandled(exchange.getContext(), exchange,
fallback, false, null);
+ }
} catch (Throwable e) {
exchange.setException(e);
}
diff --git
a/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/ResilienceFallbackErrorRegistryTest.java
b/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/ResilienceFallbackErrorRegistryTest.java
new file mode 100644
index 000000000000..90ef425c51e1
--- /dev/null
+++
b/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/ResilienceFallbackErrorRegistryTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.Collection;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.BacklogErrorEventMessage;
+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.assertTrue;
+
+/**
+ * CAMEL-24863: a failure the circuit breaker's fallback recovered from is an
error that was handled, in the error
+ * registry (camel get errors, the dev console): the entry keeps the node that
failed and says handled.
+ */
+public class ResilienceFallbackErrorRegistryTest extends CamelTestSupport {
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+ context.getErrorRegistry().setEnabled(true);
+ return context;
+ }
+
+ @Test
+ public void testFallbackRecordsTheFailureAsHandled() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:result");
+ mock.expectedBodiesReceived("Fallback response", "Fallback response");
+
+ template.sendBody("direct:start", "Hello World");
+ template.sendBody("direct:start", "Hello World");
+
+ MockEndpoint.assertIsSatisfied(context);
+
+ Collection<BacklogErrorEventMessage> entries =
context.getErrorRegistry().browse();
+ assertEquals(2, entries.size(),
+ "one registry entry per call: the copy's ExchangeFailedEvent
and the original's ExchangeFailureHandledEvent merge into the same slot");
+ for (BacklogErrorEventMessage e : entries) {
+ assertTrue(e.isHandled(), "the fallback handled the failure: " +
e);
+ assertEquals("java.lang.IllegalStateException",
e.getExceptionType());
+ }
+ }
+
+ @Override
+ protected RoutesBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start")
+ .circuitBreaker()
+ .throwException(new
IllegalStateException("Forced")).id("boom")
+ .onFallback()
+ .transform().constant("Fallback response")
+ .end()
+ .to("mock:result");
+ }
+ };
+ }
+}
diff --git
a/core/camel-api/src/main/java/org/apache/camel/spi/BacklogErrorEventMessage.java
b/core/camel-api/src/main/java/org/apache/camel/spi/BacklogErrorEventMessage.java
index f7d64d643e69..9b1fee6dd0ac 100644
---
a/core/camel-api/src/main/java/org/apache/camel/spi/BacklogErrorEventMessage.java
+++
b/core/camel-api/src/main/java/org/apache/camel/spi/BacklogErrorEventMessage.java
@@ -77,6 +77,14 @@ public interface BacklogErrorEventMessage extends
BacklogEventMessage {
*/
boolean isHandled();
+ /**
+ * Marks the error as handled after the fact: the failure was recorded
from a copy of the exchange (a circuit
+ * breaker's call, a multicast branch) before the original reported it as
handled by its fallback or catch.
+ *
+ * @since 4.23
+ */
+ void markHandled();
+
/**
* The fully qualified class name of the exception (e.g.
"java.lang.IllegalArgumentException").
*/
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java
index ec3e9810524a..22b576b023e9 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultErrorRegistry.java
@@ -196,6 +196,12 @@ public class DefaultErrorRegistry extends
EventNotifierSupport implements ErrorR
} else {
for (BacklogErrorEventMessage e : entries) {
if (exchangeId.equals(e.getExchangeId())) {
+ // the copy's entry stays (it names the node), but the
original reporting the failure as
+ // handled (a circuit breaker's fallback, a doCatch around
a multicast) means the exchange
+ // recovered: the entry is an error that was handled, not
an error (CAMEL-24863)
+ if (handled && !e.isHandled()) {
+ e.markHandled();
+ }
return;
}
}
@@ -449,7 +455,7 @@ public class DefaultErrorRegistry extends
EventNotifierSupport implements ErrorR
private final String threadName;
private final JsonObject data;
private final Throwable exception;
- private final boolean handled;
+ private volatile boolean handled;
private final String[] messageHistory;
private volatile String dataAsJson;
@@ -583,6 +589,11 @@ public class DefaultErrorRegistry extends
EventNotifierSupport implements ErrorR
return handled;
}
+ @Override
+ public void markHandled() {
+ this.handled = true;
+ }
+
@Override
public String getExceptionType() {
return exception.getClass().getName();
diff --git
a/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryDeduplicateTest.java
b/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryDeduplicateTest.java
index 97fa3158b3b9..af8e8548e61a 100644
---
a/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryDeduplicateTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/impl/ErrorRegistryDeduplicateTest.java
@@ -49,6 +49,20 @@ public class ErrorRegistryDeduplicateTest extends
ContextTestSupport {
assertEquals(true, entries.iterator().next().isHandled());
}
+ /**
+ * CAMEL-24863: a copy fails first (recorded as not handled, it names the
node), then the original reports the
+ * failure as handled (a doCatch around a multicast): the copy's entry
stays and is marked handled.
+ */
+ @Test
+ public void testCopyEntryIsMarkedHandledWhenTheOriginalRecovers() throws
Exception {
+ getMockEndpoint("mock:caught").expectedMessageCount(1);
+ template.sendBody("direct:copies", "Hello");
+ assertMockEndpointsSatisfied();
+ Collection<BacklogErrorEventMessage> entries =
context.getErrorRegistry().browse();
+ assertEquals(1, entries.size(), entries.toString());
+ assertEquals(true, entries.iterator().next().isHandled(), "the doCatch
handled the copy's failure");
+ }
+
@Override
protected RouteBuilder createRouteBuilder() {
return new RouteBuilder() {
@@ -65,6 +79,17 @@ public class ErrorRegistryDeduplicateTest extends
ContextTestSupport {
from("direct:sub").routeId("sub")
.errorHandler(deadLetterChannel("mock:dead").maximumRedeliveries(0))
.throwException(new IllegalArgumentException("Forced
error"));
+ // a multicast copy fails (its own unit of work reports the
failure first), the doTry on the
+ // original catches it
+ from("direct:copies").routeId("copies")
+ .doTry()
+ .multicast().to("direct:boom", "log:other").end()
+ .endDoTry().doCatch(Exception.class)
+ .to("mock:caught")
+ .end();
+ from("direct:boom").routeId("boom")
+ .errorHandler(noErrorHandler())
+ .throwException(new IllegalStateException("boom"));
}
};
}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index c1176d685d12..3068b1a2da1f 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -490,6 +490,16 @@ watched one is now registered as it appears, with the
files already in it reload
compile work directory (`camel.main.compileWorkDir`, `.camel-jbang/compile`
for the CLI), where the runtime writes the
class files it compiles, so a compiled source no longer triggers a reload of
its own.
+=== camel-core, camel-resilience4j - a failure the circuit breaker's fallback
recovered from is recorded as handled
+
+The error registry (`camel get errors`, the error dev console) recorded a call
that failed inside a
+`circuitBreaker` and was answered by its `onFallback` as an error that was not
handled, one per call, since the
+breaker runs the call on a copy of the exchange whose failure is reported
before the fallback runs, and the breaker
+did not report the recovery. The breaker now emits the failure-handling and
failure-handled events around the
+fallback, as `doCatch` does, and the registry marks the copy's entry as
handled when the original reports the
+failure as handled, keeping the entry that names the node that failed. `camel
get errors --handled=false` no longer
+lists such calls. The `handled` field of the entries is unchanged in shape.
+
=== camel-core - the type of a bean created by a script or a builder is
optional
The `type` (class name) of a bean definition — `bean` under `beans`,
`templateBean` of a route