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 91e44cf13c8c CAMEL-26701: camel-chaos - a fault caught by a circuit 
breaker fallback counts as completed, and a recovered fault marks no later 
error (#29363)
91e44cf13c8c is described below

commit 91e44cf13c8c37a14318ed234bb150b4f7b02098
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 6 23:55:38 2026 +0100

    CAMEL-26701: camel-chaos - a fault caught by a circuit breaker fallback 
counts as completed, and a recovered fault marks no later error (#29363)
    
    * CAMEL-26701: camel-chaos - a fault caught by a circuit breaker fallback 
counts as completed, and a recovered fault marks no later error
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-26701: camel-jbang - an error without its properties captured keeps 
the mark from the recent faults
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-26701: camel-chaos - the faults that waited too long count once, 
also when a copy merges into them meanwhile
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-26701: ChaosMarksTest - the exchange id of the fixture after the 
merge of main
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../org/apache/camel/catalog/docs/chaos.adoc       |   7 +-
 components/camel-chaos/pom.xml                     |   6 +
 components/camel-chaos/src/main/docs/chaos.adoc    |   7 +-
 .../camel/component/chaos/ChaosEventNotifier.java  |  11 +-
 .../camel/component/chaos/ChaosExchangeState.java  |  67 ++++++++-
 .../apache/camel/component/chaos/ChaosManager.java |  69 +++++++++
 .../apache/camel/component/chaos/ChaosRule.java    |   5 +
 .../chaos/ChaosCircuitBreakerEffectTest.java       | 163 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/ChaosMarks.java    |  12 +-
 .../jbang/core/commands/tui/ChaosMarksTest.java    |   5 +
 10 files changed, 337 insertions(+), 15 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
index 35485f8e0688..9776b4e8b418 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
@@ -567,13 +567,16 @@ unit of work counts (one that a route processes), as that 
tells when it is done.
 exchanges sent to the endpoint of its route while the route is down count, 
such as `direct:inventory` or
 `seda:shipping` (not an HTTP call to a `platform-http` route, which is another 
endpoint): a 3 second stop of a route
 called with `direct:inventory?block=false` that sends 5 orders to the dead 
letter channel shows as
-`5 handled by the error handler`, as the redelivery is too short to ride it 
out;
+`5 handled by the error handler`, as the redelivery is too short to ride it 
out. A circuit breaker runs its steps on
+a copy of the exchange: a fault in it counts with how the exchange was done, 
so a fault that the fallback took over
+shows as `completed`, and a message that the circuit breaker runs again at a 
redelivery counts once;
 * the last 20 faults of all rules, the latest first: when, the rule, the 
fault, the hook, the route, the endpoint
 (without its query) or the step, and the exchange id.
 
 An exchange that a rule faulted (or that was sent to the route of a lifecycle 
rule while it was down) gets the exchange
 property `CamelChaosRule` with the id of the rule (of the last one, when more 
rules did), so the tracer, the message history and a route of the dead letter 
channel can tell why
-it is there:
+it is there. When the error handler redelivers the exchange, the property is 
removed, as the faulted attempt is tried
+again: a later failure that chaos did not make is not marked (a fault at the 
redelivery marks it again):
 
 [source,text]
 ----
diff --git a/components/camel-chaos/pom.xml b/components/camel-chaos/pom.xml
index 638a9c8e6f26..3a2851d18b34 100644
--- a/components/camel-chaos/pom.xml
+++ b/components/camel-chaos/pom.xml
@@ -90,6 +90,12 @@
             <artifactId>camel-netty-http</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <!-- a circuit breaker runs its steps on a copy of the exchange, 
for the effect of a fault in it -->
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-resilience4j</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc 
b/components/camel-chaos/src/main/docs/chaos.adoc
index 35485f8e0688..9776b4e8b418 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -567,13 +567,16 @@ unit of work counts (one that a route processes), as that 
tells when it is done.
 exchanges sent to the endpoint of its route while the route is down count, 
such as `direct:inventory` or
 `seda:shipping` (not an HTTP call to a `platform-http` route, which is another 
endpoint): a 3 second stop of a route
 called with `direct:inventory?block=false` that sends 5 orders to the dead 
letter channel shows as
-`5 handled by the error handler`, as the redelivery is too short to ride it 
out;
+`5 handled by the error handler`, as the redelivery is too short to ride it 
out. A circuit breaker runs its steps on
+a copy of the exchange: a fault in it counts with how the exchange was done, 
so a fault that the fallback took over
+shows as `completed`, and a message that the circuit breaker runs again at a 
redelivery counts once;
 * the last 20 faults of all rules, the latest first: when, the rule, the 
fault, the hook, the route, the endpoint
 (without its query) or the step, and the exchange id.
 
 An exchange that a rule faulted (or that was sent to the route of a lifecycle 
rule while it was down) gets the exchange
 property `CamelChaosRule` with the id of the rule (of the last one, when more 
rules did), so the tracer, the message history and a route of the dead letter 
channel can tell why
-it is there:
+it is there. When the error handler redelivers the exchange, the property is 
removed, as the faulted attempt is tried
+again: a later failure that chaos did not make is not marked (a fault at the 
redelivery marks it again):
 
 [source,text]
 ----
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosEventNotifier.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosEventNotifier.java
index a027aedf5926..025e12ad8313 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosEventNotifier.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosEventNotifier.java
@@ -41,6 +41,11 @@ final class ChaosEventNotifier extends EventNotifierSupport {
 
     @Override
     public void notify(CamelEvent event) throws Exception {
+        if ((event instanceof CamelEvent.ExchangeCompletedEvent || event 
instanceof CamelEvent.ExchangeFailedEvent)
+                && manager.isWaitingForOriginals()) {
+            // the faults of a copy of a circuit breaker count when the 
exchange is done (CAMEL-26701)
+            manager.originalDone(((CamelEvent.ExchangeEvent) 
event).getExchange());
+        }
         if (event instanceof CamelEvent.ExchangeFailureHandlingEvent handling) 
{
             if (handling.isDeadLetterChannel()) {
                 ChaosExchangeState.of(handling.getExchange()).deadLetter = 
true;
@@ -60,6 +65,9 @@ final class ChaosEventNotifier extends EventNotifierSupport {
                 // a rule that faulted the exchange before is recovered when 
the exchange then completes
                 state.redelivered();
             }
+            // the faulted attempt is tried again: a later failure that chaos 
did not make is not marked with the rule
+            // (CAMEL-26701); a fault at the redelivery marks it again
+            exchange.removeProperty(ChaosManager.CHAOS_RULE);
             String routeId = ChaosExchangeState.routeId(exchange);
             if (routeId != null) {
                 manager.redelivered(routeId);
@@ -109,7 +117,8 @@ final class ChaosEventNotifier extends EventNotifierSupport 
{
 
     @Override
     public boolean isIgnoreExchangeCompletedEvent() {
-        return true;
+        // only while faults of a copy of a circuit breaker wait for the 
exchange to be done
+        return !manager.isWaitingForOriginals();
     }
 
     @Override
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExchangeState.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExchangeState.java
index 09054c173c47..6cd53cc10e0d 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExchangeState.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExchangeState.java
@@ -23,9 +23,11 @@ import java.util.Map;
 
 import org.apache.camel.Exchange;
 import org.apache.camel.ExchangeExtension;
+import org.apache.camel.ExchangePropertyKey;
 import org.apache.camel.Route;
 import org.apache.camel.SafeCopyProperty;
 import org.apache.camel.spi.UnitOfWork;
+import org.apache.camel.support.ExchangeHelper;
 import org.apache.camel.support.SynchronizationAdapter;
 
 /**
@@ -147,7 +149,7 @@ final class ChaosExchangeState implements SafeCopyProperty {
     }
 
     /** The rules that faulted an exchange, and whether the error handler 
redelivered it after each one. */
-    private static final class Faults extends SynchronizationAdapter {
+    static final class Faults extends SynchronizationAdapter {
 
         private final List<ChaosRule> rules = new ArrayList<>(2);
         private final List<Boolean> redelivered = new ArrayList<>(2);
@@ -170,6 +172,22 @@ final class ChaosExchangeState implements SafeCopyProperty 
{
             }
         }
 
+        /**
+         * Takes the faults of another copy of the same exchange: a rule that 
faulted both counts once, as it is one
+         * message.
+         */
+        void merge(Faults other) {
+            for (int i = 0; i < other.rules.size(); i++) {
+                ChaosRule rule = other.rules.get(i);
+                if (rules.contains(rule)) {
+                    rule.effect.unfaulted();
+                } else {
+                    rules.add(rule);
+                    redelivered.add(other.redelivered.get(i));
+                }
+            }
+        }
+
         @Override
         public boolean allowHandover() {
             // the effect is how this exchange was done: not a copy that a 
seda endpoint (such as the one of a dead
@@ -179,17 +197,44 @@ final class ChaosExchangeState implements 
SafeCopyProperty {
 
         @Override
         public void onDone(Exchange exchange) {
-            // an exchange marked rollback only has no exception, but its 
message is not processed either
-            boolean failed = exchange.isFailed() || exchange.isRollbackOnly() 
|| exchange.isRollbackOnlyLast();
-            ExchangeExtension ee = exchange.getExchangeExtension();
-            boolean handled = !failed && ee.isErrorHandlerHandledSet() && 
ee.isErrorHandlerHandled();
+            if (waitForOriginal(exchange)) {
+                return;
+            }
+            count(exchange);
+        }
+
+        /**
+         * A circuit breaker runs its steps on a correlated copy of the 
exchange, in a try block, with a unit of work of
+         * its own (CAMEL-26701). The exchange then goes on with the result, 
the fallback or the exception, and is the
+         * message the faults had an effect on: they count when it is done, 
with its outcome, once per rule also when
+         * the circuit breaker ran again at a redelivery. The exchange is not 
touched from the thread of the copy; the
+         * faults wait under its id until the event that it is done. A copy of 
a wire tap in a try block is correlated
+         * too, and its exchange may be done before it: when no event comes in 
a minute, its faults count with how the
+         * copy was done.
+         */
+        private boolean waitForOriginal(Exchange copy) {
+            String originalId = 
copy.getProperty(ExchangePropertyKey.CORRELATION_ID, String.class);
+            if (originalId == null || 
!copy.getProperty(ExchangePropertyKey.TRY_ROUTE_BLOCK, false, boolean.class)) {
+                return false;
+            }
+            ChaosManager manager = ChaosManager.get(copy.getContext());
+            return manager != null && manager.waitForOriginal(originalId, 
this, failed(copy), handled(copy));
+        }
+
+        /** Counts the faults in the effect of their rules, with how the 
exchange was done. */
+        void count(Exchange exchange) {
+            count(failed(exchange), handled(exchange), 
ExchangeHelper.isRedelivered(exchange));
+        }
+
+        /** Counts the faults, with whether the exchange failed, was handled, 
or was redelivered. */
+        void count(boolean failed, boolean handled, boolean 
redeliveredExchange) {
             for (int i = 0; i < rules.size(); i++) {
                 ChaosRule.Effect.Outcome outcome;
                 if (failed) {
                     outcome = ChaosRule.Effect.Outcome.FAILED;
                 } else if (handled) {
                     outcome = ChaosRule.Effect.Outcome.HANDLED;
-                } else if (redelivered.get(i)) {
+                } else if (redelivered.get(i) || redeliveredExchange) {
                     outcome = ChaosRule.Effect.Outcome.RECOVERED;
                 } else {
                     outcome = ChaosRule.Effect.Outcome.COMPLETED;
@@ -198,6 +243,16 @@ final class ChaosExchangeState implements SafeCopyProperty 
{
             }
         }
 
+        private static boolean failed(Exchange exchange) {
+            // an exchange marked rollback only has no exception, but its 
message is not processed either
+            return exchange.isFailed() || exchange.isRollbackOnly() || 
exchange.isRollbackOnlyLast();
+        }
+
+        private static boolean handled(Exchange exchange) {
+            ExchangeExtension ee = exchange.getExchangeExtension();
+            return !failed(exchange) && ee.isErrorHandlerHandledSet() && 
ee.isErrorHandlerHandled();
+        }
+
         @Override
         public String toString() {
             return "ChaosFaults" + rules;
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
index b7d348db8a4f..532f439ccdb6 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
@@ -356,6 +356,7 @@ public final class ChaosManager {
      * verdict.
      */
     public ChaosReport report() {
+        countWaitedTooLong(TimeSource.currentTimeMillis());
         List<ChaosRule> current;
         List<ChaosRule> removed;
         int unmet;
@@ -386,6 +387,74 @@ public final class ChaosManager {
         return counting;
     }
 
+    // the faults of the copies of circuit breakers, by the id of the exchange 
they are a copy of, counted when that
+    // exchange is done (CAMEL-26701); bounded, and counted with the copy when 
no event comes for a minute
+    private static final int MAX_WAITING = 10_000;
+    private static final long WAIT_FOR_ORIGINAL_MILLIS = 60_000;
+    private final ConcurrentMap<String, WaitingFaults> waitingForOriginal = 
new ConcurrentHashMap<>();
+
+    /** The faults of copies of an exchange, and how the last copy was done. */
+    private static final class WaitingFaults {
+        final ChaosExchangeState.Faults faults;
+        final boolean copyFailed;
+        final boolean copyHandled;
+        final long since;
+
+        WaitingFaults(ChaosExchangeState.Faults faults, boolean copyFailed, 
boolean copyHandled, long since) {
+            this.faults = faults;
+            this.copyFailed = copyFailed;
+            this.copyHandled = copyHandled;
+            this.since = since;
+        }
+    }
+
+    /**
+     * The faults of a copy of a circuit breaker wait for the exchange it is a 
copy of.
+     *
+     * @return false when too many wait already, so the faults count now, with 
the copy
+     */
+    boolean waitForOriginal(String exchangeId, ChaosExchangeState.Faults 
faults, boolean copyFailed, boolean copyHandled) {
+        long now = TimeSource.currentTimeMillis();
+        countWaitedTooLong(now);
+        if (waitingForOriginal.size() >= MAX_WAITING) {
+            return false;
+        }
+        // another copy when the circuit breaker runs again at a redelivery of 
the exchange: one message
+        waitingForOriginal.merge(exchangeId, new WaitingFaults(faults, 
copyFailed, copyHandled, now), (before, copy) -> {
+            before.faults.merge(copy.faults);
+            return new WaitingFaults(before.faults, copy.copyFailed, 
copy.copyHandled, copy.since);
+        });
+        return true;
+    }
+
+    /** Whether faults wait for an exchange to be done, so the events of done 
exchanges are listened to. */
+    boolean isWaitingForOriginals() {
+        return !waitingForOriginal.isEmpty();
+    }
+
+    /** An exchange is done: the faults of the copies of circuit breakers that 
wait for it count. */
+    void originalDone(Exchange exchange) {
+        WaitingFaults waiting = 
waitingForOriginal.remove(exchange.getExchangeId());
+        if (waiting != null) {
+            waiting.faults.count(exchange);
+        }
+    }
+
+    /** The faults that waited a minute for an exchange count with how their 
copy was done. */
+    void countWaitedTooLong(long now) {
+        if (waitingForOriginal.isEmpty()) {
+            return;
+        }
+        for (Map.Entry<String, WaitingFaults> e : 
waitingForOriginal.entrySet()) {
+            WaitingFaults w = e.getValue();
+            // removed only when no copy merged into it meanwhile (a merge 
replaces the value), so the faults count
+            // once, and no longer in the map, as nothing else can change them
+            if (now - w.since >= WAIT_FOR_ORIGINAL_MILLIS && 
waitingForOriginal.remove(e.getKey(), w)) {
+                w.faults.count(w.copyFailed, w.copyHandled, false);
+            }
+        }
+    }
+
     /** Whether a rule was ever added (for the report when the context stops). 
*/
     boolean isUsed() {
         return used;
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
index 80f77a1d9680..3eaa586e74e7 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
@@ -768,6 +768,11 @@ final class ChaosRule {
             faulted.increment();
         }
 
+        /** A fault of another copy of an exchange that was counted already 
(CAMEL-26701). */
+        void unfaulted() {
+            faulted.decrement();
+        }
+
         void done(Outcome outcome) {
             done[outcome.ordinal()].increment();
         }
diff --git 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosCircuitBreakerEffectTest.java
 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosCircuitBreakerEffectTest.java
new file mode 100644
index 000000000000..67b7ec992725
--- /dev/null
+++ 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosCircuitBreakerEffectTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.chaos;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.util.TimeSource;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-26701: a circuit breaker runs its steps on a copy of the exchange. 
The effect of a fault in it is how the
+ * exchange was done: completed when the fallback took over, handled by the 
dead letter channel, or recovered by a
+ * redelivery; once per message, also when the circuit breaker runs again at a 
redelivery. And a fault that a redelivery
+ * recovered does not mark a later failure of the exchange.
+ */
+class ChaosCircuitBreakerEffectTest extends ChaosTestSupport {
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:fallback").routeId("fallback")
+                        .circuitBreaker()
+                        .to("mock:pay")
+                        .onFallback()
+                        .setBody(constant("payment later"))
+                        .end()
+                        .to("mock:shipped");
+
+                // the error handler of a circuit breaker without a fallback 
is the one of the route that calls it
+                from("direct:noFallback").routeId("noFallback")
+                        
.errorHandler(deadLetterChannel("mock:dead").maximumRedeliveries(2).redeliveryDelay(0))
+                        .to("direct:pay");
+                from("direct:pay").routeId("pay")
+                        .errorHandler(noErrorHandler())
+                        .circuitBreaker()
+                        .to("mock:pay2")
+                        .end()
+                        .to("mock:shipped2");
+
+                // a step faulted and recovered by a redelivery, then a 
failure that chaos did not make
+                from("direct:later").routeId("later")
+                        
.errorHandler(deadLetterChannel("mock:dead3").maximumRedeliveries(1).redeliveryDelay(0))
+                        .to("mock:stock").id("callStock")
+                        .process(e -> {
+                            e.getMessage().setHeader("chaosRule", 
e.getProperty(ChaosManager.CHAOS_RULE));
+                            throw new IllegalStateException("a real failure");
+                        });
+            }
+        };
+    }
+
+    private JsonObject effect(String id) {
+        return (JsonObject) ruleReport(id).get("effect");
+    }
+
+    @Test
+    void theFallbackTookOverSoTheExchangeCompleted() throws Exception {
+        String id = chaos().addRule("endpoint=mock:pay error");
+        MockEndpoint shipped = getMockEndpoint("mock:shipped");
+        shipped.expectedBodiesReceived("payment later");
+
+        template.sendBody("direct:fallback", "order");
+
+        MockEndpoint.assertIsSatisfied(context);
+        JsonObject effect = effect(id);
+        assertThat(effect.getLong("faulted")).isEqualTo(1);
+        assertThat(effect.getLong("completed")).isEqualTo(1);
+        assertThat(effect.getLong("failed")).isZero();
+        assertThat(effect.getLong("inFlight")).isZero();
+    }
+
+    @Test
+    void withoutAFallbackTheDeadLetterChannelHandledOneMessage() throws 
Exception {
+        String id = chaos().addRule("endpoint=mock:pay2 error");
+        getMockEndpoint("mock:dead").expectedMessageCount(1);
+
+        template.sendBody("direct:noFallback", "order");
+
+        MockEndpoint.assertIsSatisfied(context);
+        JsonObject effect = effect(id);
+        // the circuit breaker ran three times (the route that calls it 
redelivered twice), on three copies: one message
+        assertThat(effect.getLong("faulted")).isEqualTo(1);
+        assertThat(effect.getLong("handled")).isEqualTo(1);
+        assertThat(effect.getLong("failed")).isZero();
+        assertThat(effect.getLong("inFlight")).isZero();
+    }
+
+    @Test
+    void aRedeliveryThatSucceedsAfterTheFaultIsRecovered() throws Exception {
+        String id = chaos().addRule("endpoint=mock:pay2 error until=1");
+        getMockEndpoint("mock:shipped2").expectedMessageCount(1);
+
+        template.sendBody("direct:noFallback", "order");
+
+        MockEndpoint.assertIsSatisfied(context);
+        JsonObject effect = effect(id);
+        assertThat(effect.getLong("faulted")).isEqualTo(1);
+        assertThat(effect.getLong("recovered")).isEqualTo(1);
+        assertThat(effect.getLong("failed")).isZero();
+    }
+
+    @Test
+    void aRecoveredFaultDoesNotMarkALaterFailure() throws Exception {
+        chaos().addRule("endpoint=mock:stock error until=1");
+        MockEndpoint dead = getMockEndpoint("mock:dead3");
+        dead.expectedMessageCount(1);
+
+        template.sendBody("direct:later", "order");
+
+        MockEndpoint.assertIsSatisfied(context);
+        Exchange failed = dead.getReceivedExchanges().get(0);
+        // the fault at the stock call was redelivered and recovered: the real 
failure after it is not chaos
+        assertThat(failed.getMessage().getHeader("chaosRule")).isNull();
+        assertThat(failed.getProperty(ChaosManager.CHAOS_RULE)).isNull();
+    }
+
+    @Test
+    void 
theFaultsOfACopyCountOnceWithItsOutcomeWhenTheExchangeIsNotDoneInAMinute() {
+        ChaosRule rule = new ChaosRule(
+                ChaosRuleParser.parse("endpoint=mock:pay error", 
context.getClassResolver(), TimeUnit.MINUTES.toMillis(10)),
+                "r9", 1, 0, TimeUnit.MINUTES.toMillis(10));
+        ChaosExchangeState.Faults faults = new ChaosExchangeState.Faults();
+        faults.add(rule);
+        long now = TimeSource.currentTimeMillis();
+        assertThat(chaos().waitForOriginal("ex-1", faults, false, 
true)).isTrue();
+        // the circuit breaker ran again for the same exchange: one message
+        ChaosExchangeState.Faults again = new ChaosExchangeState.Faults();
+        again.add(rule);
+        chaos().waitForOriginal("ex-1", again, false, true);
+
+        chaos().countWaitedTooLong(now + 30_000);
+        assertThat(chaos().isWaitingForOriginals()).isTrue();
+        
assertThat(rule.effect.count(ChaosRule.Effect.Outcome.HANDLED)).isZero();
+
+        chaos().countWaitedTooLong(now + 120_000);
+        chaos().countWaitedTooLong(now + 240_000);
+        assertThat(chaos().isWaitingForOriginals()).isFalse();
+        assertThat(rule.effect.faultedCount()).isEqualTo(1);
+        
assertThat(rule.effect.count(ChaosRule.Effect.Outcome.HANDLED)).isEqualTo(1);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosMarks.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosMarks.java
index ac97707bac67..1269a8f2768c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosMarks.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosMarks.java
@@ -72,15 +72,19 @@ final class ChaosMarks {
     }
 
     /**
-     * The rule that faulted an exchange: from its {@code CamelChaosRule} 
property when there are properties, else from
-     * the recent faults seen; null when no rule did (or chaos is off).
+     * The rule that faulted an exchange: from its {@code CamelChaosRule} 
property when there are properties (none when
+     * the failure was not made by chaos), else from the recent faults seen; 
null when no rule did (or chaos is off).
      */
     static String ruleOf(MonitorContext ctx, IntegrationInfo info, String 
exchangeId, Map<String, Object> properties) {
         if (info == null || info.chaos == null) {
             return null;
         }
-        if (properties != null && properties.get(PROPERTY) != null) {
-            return String.valueOf(properties.get(PROPERTY));
+        if (properties != null && !properties.isEmpty()) {
+            // the properties of the failure say it: camel-chaos removes the 
mark when the faulted attempt is
+            // redelivered, so a later failure it did not make has none 
(CAMEL-26701); an error without the properties
+            // captured has an empty map
+            Object rule = properties.get(PROPERTY);
+            return rule != null ? String.valueOf(rule) : null;
         }
         return ctx != null && ctx.chaosMarks != null ? 
ctx.chaosMarks.ruleOf(info.pid, exchangeId) : null;
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosMarksTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosMarksTest.java
index da14322f138b..844b62f233bc 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosMarksTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosMarksTest.java
@@ -63,6 +63,11 @@ class ChaosMarksTest {
         assertThat(ChaosMarks.ruleOf(ctx, info, "OTHER", null)).isNull();
         // the property of the exchange, when there are properties
         assertThat(ChaosMarks.ruleOf(ctx, info, "OTHER", 
Map.of(ChaosMarks.PROPERTY, "r7"))).isEqualTo("r7");
+        // CAMEL-26701: a failure with properties and no mark was not made by 
chaos, though a rule faulted the exchange
+        // before (and a redelivery recovered it)
+        assertThat(ChaosMarks.ruleOf(ctx, info, 
"3F2A85F3D4213-000000000000104E", Map.of("other", "x"))).isNull();
+        // without the properties captured, the recent faults of the report 
say it
+        assertThat(ChaosMarks.ruleOf(ctx, info, 
"3F2A85F3D4213-000000000000104E", Map.of())).isEqualTo("r1");
         // no marks without chaos
         info.chaos = null;
         assertThat(ChaosMarks.ruleOf(ctx, info, 
"3F2A85F3D4213-000000000000104E", null)).isNull();

Reply via email to