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();