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 52680d1d0fc3 CAMEL-26747: camel-jbang - chaos: a lost expectation
counts parked messages, file endpoints get connection faults, and a cut
countdown (#29423)
52680d1d0fc3 is described below
commit 52680d1d0fc3d624a130a75e449843de6166a3e6
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Nov 8 21:46:48 2026 +0100
CAMEL-26747: camel-jbang - chaos: a lost expectation counts parked
messages, file endpoints get connection faults, and a cut countdown (#29423)
* CAMEL-26747: regen the catalog copy of the chaos model
* CAMEL-26747: keep the Javadoc of ChaosTab.shortTime on its method
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../org/apache/camel/catalog/chaos-model.json | 46 +++++++++++++++++++++-
.../org/apache/camel/catalog/docs/chaos.adoc | 3 ++
.../org/apache/camel/chaos/chaos-model.json | 46 +++++++++++++++++++++-
components/camel-chaos/src/main/docs/chaos.adoc | 3 ++
.../apache/camel/component/chaos/ChaosAdvisor.java | 8 +++-
.../apache/camel/component/chaos/ChaosManager.java | 14 +++++--
.../apache/camel/component/chaos/ChaosModel.java | 19 ++++++++-
.../camel/component/chaos/ChaosAdvisorTest.java | 6 +++
.../chaos/ChaosExpectTimeAndLossTest.java | 21 ++++++++++
.../dsl/jbang/core/commands/tui/ChaosTab.java | 22 +++++++----
.../core/commands/tui/ChaosTabRenderTest.java | 8 ++++
11 files changed, 182 insertions(+), 14 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
index e99ffa8d907f..6e2ac07ee4ce 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
@@ -638,7 +638,8 @@
"A circuit breaker that does not open"
],
"avoid": [
- "To test an HTTP status from a service that is up: use Returns an
error"
+ "To test an HTTP status from a service that is up: use Returns an
error",
+ "A local file has no connection: use Fails to write"
],
"watch": [
"The redeliveries and the exchanges handled by the error handler",
@@ -671,6 +672,49 @@
}
]
},
+ {
+ "id": "write-error",
+ "group": "Downstream",
+ "title": "Fails to write",
+ "description": "The send fails with an IOException before the file is
written, as when the disk is full or the directory cannot be written",
+ "uncovers": [
+ "Whether a failed write is retried, and whether the retry writes the
file whole",
+ "Where the message goes when the file cannot be written: when the
file is how the dead letter channel parks the message, it is lost"
+ ],
+ "avoid": [
+ "A remote system such as FTP or a database: use Refuses the
connection"
+ ],
+ "watch": [
+ "The redeliveries and the exchanges handled by the error handler",
+ "The files written, and the messages that are in none of them"
+ ],
+ "rule": "endpoint=${endpoint} error exception=java.io.IOException
rate=${rate} until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.25",
+ "example": "0.25"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
{
"id": "slow",
"group": "Downstream",
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 98b689598416..8bc826d1fe7c 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
@@ -851,6 +851,9 @@ delay rule slows the route as it should).
difference is the messages lost (never below 0, as the second route may also
get messages from elsewhere). Messages still
on their way when the time is up count as lost too: let `within=` run past the
`until=` of the faults, so the last ones
can arrive. The messages it counts as lost are added to the lost of the
report. The op is `<=` or `=`.
+* An exchange of the first route that its error handler handled, such as an
order the dead letter channel parked, is not
+counted in the first route: it is not meant to reach the second one, so it is
not lost. In the second route a handled
+exchange counts as completed, as it arrived.
For example, a stock service that is slow for a minute, and the orders that
must still ship, fast enough:
diff --git
a/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
b/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
index e99ffa8d907f..6e2ac07ee4ce 100644
---
a/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
+++
b/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
@@ -638,7 +638,8 @@
"A circuit breaker that does not open"
],
"avoid": [
- "To test an HTTP status from a service that is up: use Returns an
error"
+ "To test an HTTP status from a service that is up: use Returns an
error",
+ "A local file has no connection: use Fails to write"
],
"watch": [
"The redeliveries and the exchanges handled by the error handler",
@@ -671,6 +672,49 @@
}
]
},
+ {
+ "id": "write-error",
+ "group": "Downstream",
+ "title": "Fails to write",
+ "description": "The send fails with an IOException before the file is
written, as when the disk is full or the directory cannot be written",
+ "uncovers": [
+ "Whether a failed write is retried, and whether the retry writes the
file whole",
+ "Where the message goes when the file cannot be written: when the
file is how the dead letter channel parks the message, it is lost"
+ ],
+ "avoid": [
+ "A remote system such as FTP or a database: use Refuses the
connection"
+ ],
+ "watch": [
+ "The redeliveries and the exchanges handled by the error handler",
+ "The files written, and the messages that are in none of them"
+ ],
+ "rule": "endpoint=${endpoint} error exception=java.io.IOException
rate=${rate} until=${until}",
+ "placeholders": [
+ {
+ "name": "endpoint",
+ "type": "endpoint",
+ "description": "The endpoint the route sends to",
+ "example": "kafka:orders",
+ "pick": {
+ "from": "producers"
+ }
+ },
+ {
+ "name": "rate",
+ "type": "rate",
+ "description": "How often: a probability, every:N or
burst:<time>\/<period>",
+ "default": "0.25",
+ "example": "0.25"
+ },
+ {
+ "name": "until",
+ "type": "countOrDuration",
+ "description": "When the rule ends: a number of matches or a time",
+ "default": "10m",
+ "example": "10m"
+ }
+ ]
+ },
{
"id": "slow",
"group": "Downstream",
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc
b/components/camel-chaos/src/main/docs/chaos.adoc
index 98b689598416..8bc826d1fe7c 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -851,6 +851,9 @@ delay rule slows the route as it should).
difference is the messages lost (never below 0, as the second route may also
get messages from elsewhere). Messages still
on their way when the time is up count as lost too: let `within=` run past the
`until=` of the faults, so the last ones
can arrive. The messages it counts as lost are added to the lost of the
report. The op is `<=` or `=`.
+* An exchange of the first route that its error handler handled, such as an
order the dead letter channel parked, is not
+counted in the first route: it is not meant to reach the second one, so it is
not lost. In the second route a handled
+exchange counts as completed, as it arrived.
For example, a stock service that is slow for a minute, and the orders that
must still ship, fast enough:
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
index 28f8ab5c1a79..92e99753bb9d 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
@@ -57,6 +57,8 @@ final class ChaosAdvisor {
"spring-rabbitmq", "rabbitmq", "nats", "paho", "paho-mqtt5",
"azure-servicebus", "ibm-mq");
// a hand-off to another route of the same application, which does not
wait for it
private static final Set<String> HAND_OFF = Set.of("seda", "vm",
"disruptor", "disruptor-vm");
+ // the endpoints that write to the local file system: remote to Camel, but
with no connection to refuse
+ private static final Set<String> LOCAL_FILES = Set.of("file");
/** The kinds of faults, which the report also uses for what has been
tried. */
enum Kind {
@@ -294,7 +296,11 @@ final class ChaosAdvisor {
if (!to.isRemote()) {
continue;
}
- if (ChaosRuleParser.HTTP_SCHEMES.contains(scheme)) {
+ if (LOCAL_FILES.contains(scheme)) {
+ // a local file system: nothing connects, but the write can
fail
+ add(answer, recipes, "write-error", id, Kind.ERRORS,
Map.of("endpoint", uri),
+ "a write to " + uri + ": what does the route do when
the file cannot be written?", 60);
+ } else if (ChaosRuleParser.HTTP_SCHEMES.contains(scheme)) {
if (breaker) {
add(answer, recipes, "outage", id, Kind.ERRORS,
Map.of("endpoint", uri),
"a call to " + uri + " in a route with a circuit
breaker: when the service is down for a"
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 970b2b4621e4..2ae52bfad6e9 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
@@ -738,7 +738,7 @@ public final class ChaosManager {
state.counts = counts;
if (outcome.completed() && !state.expectedCompleted) {
state.expectedCompleted = true;
- record(routeId, ChaosRule.Expectation.Kind.COMPLETED);
+ record(routeId, ChaosRule.Expectation.Kind.COMPLETED, outcome ==
ChaosExchangeState.Outcome.HANDLED);
}
if (outcome == ChaosExchangeState.Outcome.HANDLED &&
!state.expectedHandled) {
state.expectedHandled = true;
@@ -768,13 +768,21 @@ public final class ChaosManager {
}
private void record(String routeId, ChaosRule.Expectation.Kind kind) {
+ record(routeId, kind, false);
+ }
+
+ private void record(String routeId, ChaosRule.Expectation.Kind kind,
boolean handled) {
for (ChaosRule.Expectation expectation : ruleSet.expectations) {
if (expectation.kind == kind && expectation.route.equals(routeId))
{
expectation.record();
} else if (kind == ChaosRule.Expectation.Kind.COMPLETED &&
expectation.kind == ChaosRule.Expectation.Kind.LOST) {
- // the messages one route completed, and those the next route
completed
+ // the messages one route completed, and those the next route
completed; a message the error handler
+ // of the first route handled (such as one parked by its dead
letter channel) is not meant to reach
+ // the next route, so it is not lost
if (expectation.route.equals(routeId)) {
- expectation.recordLost(false);
+ if (!handled) {
+ expectation.recordLost(false);
+ }
} else if (routeId.equals(expectation.toRoute)) {
expectation.recordLost(true);
}
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
index 06785c4907ac..85e3497c57dc 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
@@ -419,6 +419,12 @@ final class ChaosModel {
+ "service is not running or a firewall blocks it",
"endpoint=${endpoint} error
exception=java.net.ConnectException rate=${rate} until=${until}",
List.of(producer(), rate("0.25"), until("10m"))));
+ answer.add(new Recipe(
+ "write-error", "Downstream", "Fails to write",
+ "The send fails with an IOException before the file is
written, as when the disk "
+ + "is full or
the directory cannot be written",
+ "endpoint=${endpoint} error exception=java.io.IOException
rate=${rate} until=${until}",
+ List.of(producer(), rate("0.25"), until("10m"))));
answer.add(new Recipe(
"slow", "Downstream", "Is slow",
"Each send waits a time within the range before it is made, as
a slow service. "
@@ -588,10 +594,21 @@ final class ChaosModel {
"Long redelivery delays that hold the consumer while
the service is down",
"A circuit breaker that does not open"),
List.of(
- "To test an HTTP status from a service that is up: use
Returns an error"),
+ "To test an HTTP status from a service that is up: use
Returns an error",
+ "A local file has no connection: use Fails to write"),
List.of(
"The redeliveries and the exchanges handled by the
error handler",
"The state of a circuit breaker around the call")));
+ answer.put("write-error", new Guide(
+ List.of(
+ "Whether a failed write is retried, and whether the
retry writes the file whole",
+ "Where the message goes when the file cannot be
written: when the file is how the "
+
+ "dead letter channel parks the message, it is
lost"),
+ List.of(
+ "A remote system such as FTP or a database: use
Refuses the connection"),
+ List.of(
+ "The redeliveries and the exchanges handled by the
error handler",
+ "The files written, and the messages that are in none
of them")));
answer.put("slow", new Guide(
List.of(
"A consumer that falls behind: the throughput of the
route drops and messages "
diff --git
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosAdvisorTest.java
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosAdvisorTest.java
index 9939da651eb3..934e49c1882d 100644
---
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosAdvisorTest.java
+++
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosAdvisorTest.java
@@ -57,6 +57,7 @@ class ChaosAdvisorTest extends ChaosTestSupport {
.onFallback().setBody(constant("later"))
.end();
from("testpoll:inbox?delay=100000&initialDelay=100000").routeId("inbox").to("mock:inbox");
+
from("direct:archive").routeId("archive").to("file:target/chaos-archive");
}
};
}
@@ -89,6 +90,11 @@ class ChaosAdvisorTest extends ChaosTestSupport {
assertThat(rules.values()).noneMatch(r ->
r.contains("throwExceptionOnFailure"));
// an internal input (direct) is not a consumer to fail
assertThat(rules).doesNotContainKey("orders route-error consumer");
+ // a local file has no connection to refuse, but the write can fail
+ assertThat(rules)
+ .containsEntry("archive write-error errors",
+ "endpoint=file://target/chaos-archive error
exception=java.io.IOException rate=0.25 until=10m")
+ .doesNotContainKeys("archive connection-refused errors",
"archive slow slow");
// the most worth trying first
assertThat(suggestions.get(0).recipe()).isEqualTo("http-error");
diff --git
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExpectTimeAndLossTest.java
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExpectTimeAndLossTest.java
index 07bc14b73daf..dfbfc352df79 100644
---
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExpectTimeAndLossTest.java
+++
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExpectTimeAndLossTest.java
@@ -55,6 +55,11 @@ class ChaosExpectTimeAndLossTest extends ChaosTestSupport {
.to("mock:stock").id("callStock")
.to("direct:ship");
from("direct:ship").routeId("ship").to("mock:ship");
+ // the orders it cannot charge are parked by its dead letter
channel, and never go to ship
+ from("direct:charged").routeId("charged")
+ .errorHandler(deadLetterChannel("mock:parked"))
+ .to("mock:pay").id("callPay")
+ .to("direct:ship");
}
};
}
@@ -141,6 +146,22 @@ class ChaosExpectTimeAndLossTest extends ChaosTestSupport {
.endsWith("; expects no message lost between route orders and
route ship within 1 minute");
}
+ @Test
+ void theMessagesParkedByTheDeadLetterChannelAreNotLost() {
+ // every second payment fails, and the dead letter channel of route
charged parks the order
+ String id = chaos().addRule("step=callPay error rate=every:2
expect=charged->ship:lost<=0 within=1m");
+ for (int i = 0; i < 4; i++) {
+ template.sendBody("direct:charged", "order " + i);
+ }
+ clock.advance(60_001);
+
+ assertThat(expectation(id).getString("outcome")).isEqualTo("met");
+ ChaosReport report = chaos().report();
+ assertThat(report.getVerdict()).isEqualTo(ChaosReport.PASS);
+ assertThat(report.getLost()).isZero();
+ assertThat(report.toText()).contains("met, 0 lost (2 completed in
charged, 2 in ship)");
+ }
+
@Test
void nothingLostWhenTheNextRouteCompletesThemAll() {
String id = chaos().addRule("step=callStock delay duration=10ms
expect=orders->ship:lost=0 within=1m");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
index 9f9d93e9202f..2cc5cef96a17 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
@@ -401,13 +401,13 @@ class ChaosTab extends AbstractTableTab {
if (r.lifecycleState != null) {
// the route is the target of the rule; between the rounds of
repeat= the route is back
if (r.lifecycleRound > 0 && r.lifecycleActsIn >= 0) {
- return "next round in " + shortTime(r.lifecycleActsIn);
+ return "next round in " + countdown(r.lifecycleActsIn);
}
StringBuilder sb = new StringBuilder(r.lifecycleState);
if (r.lifecycleActsIn >= 0) {
- sb.append(", acts in ").append(shortTime(r.lifecycleActsIn));
+ sb.append(", acts in ").append(countdown(r.lifecycleActsIn));
} else if (r.lifecycleBackIn >= 0) {
- sb.append(", back in ").append(shortTime(r.lifecycleBackIn));
+ sb.append(", back in ").append(countdown(r.lifecycleBackIn));
}
if (r.lifecycleReason != null) {
sb.append(": ").append(r.lifecycleReason);
@@ -416,19 +416,19 @@ class ChaosTab extends AbstractTableTab {
}
List<String> parts = new ArrayList<>();
if (r.startsIn >= 0) {
- parts.add("starts in " + shortTime(r.startsIn));
+ parts.add("starts in " + countdown(r.startsIn));
}
if (r.burstEndsIn >= 0) {
- parts.add("burst, " + shortTime(r.burstEndsIn) + " left");
+ parts.add("burst, " + countdown(r.burstEndsIn) + " left");
} else if (r.nextBurstIn >= 0) {
- parts.add("next burst in " + shortTime(r.nextBurstIn));
+ parts.add("next burst in " + countdown(r.nextBurstIn));
}
if (r.nextFireIn > 0) {
// a count of matches (rate=every:N), not a time
parts.add("fires again in " + r.nextFireIn + (r.nextFireIn == 1 ?
" match" : " matches"));
}
if (r.endsIn >= 0) {
- parts.add("ends in " + shortTime(r.endsIn));
+ parts.add("ends in " + countdown(r.endsIn));
}
if (r.skipped > 0) {
parts.add("skipped " + r.skipped);
@@ -745,6 +745,14 @@ class ChaosTab extends AbstractTableTab {
return sb.toString();
}
+ /**
+ * The time until something happens, in whole seconds rounded up: {@code
back in 1s}, not {@code back in 903ms},
+ * which a narrow note column cuts to {@code 903m} (read as minutes).
+ */
+ static String countdown(long ms) {
+ return ms <= 0 ? "0s" : shortTime((ms + 999) / 1000 * 1000);
+ }
+
/** A time for a column: 800ms, 2s, 1m4s, 2h3m. */
static String shortTime(long ms) {
if (ms < 1000) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTabRenderTest.java
index 649c9747a514..be424cf4fe3f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTabRenderTest.java
@@ -283,6 +283,14 @@ class ChaosTabRenderTest {
assertThat(ChaosTab.shortTime(800)).isEqualTo("800ms");
assertThat(ChaosTab.shortTime(64_000)).isEqualTo("1m4s");
assertThat(ChaosTab.shortTime(120_000)).isEqualTo("2m");
+ // a countdown in the note is in whole seconds: "903ms" cut to "903m"
reads as minutes
+ assertThat(ChaosTab.countdown(903)).isEqualTo("1s");
+ assertThat(ChaosTab.countdown(64_001)).isEqualTo("1m5s");
+ assertThat(ChaosTab.countdown(0)).isEqualTo("0s");
+ ChaosInfo.Rule stalled = new ChaosInfo.Rule();
+ stalled.lifecycleState = "suspended";
+ stalled.lifecycleBackIn = 903;
+ assertThat(ChaosTab.note(stalled)).isEqualTo("suspended, back in 1s");
ChaosInfo.Rule every = new ChaosInfo.Rule();
every.text = "endpoint=x error rate=every:5";