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 1c36e688ae92 CAMEL-26772: camel-chaos - what to try offers pairs of
faults to try together (#29464)
1c36e688ae92 is described below
commit 1c36e688ae9283ae4693de9d91fccca74f0f5308
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 16:13:47 2026 +0100
CAMEL-26772: camel-chaos - what to try offers pairs of faults to try
together (#29464)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../org/apache/camel/catalog/docs/chaos.adoc | 23 ++++-
components/camel-chaos/src/main/docs/chaos.adoc | 23 ++++-
.../apache/camel/component/chaos/ChaosAdvisor.java | 59 ++++++++++-
.../apache/camel/component/chaos/ChaosManager.java | 10 +-
.../camel/component/chaos/ChaosAdvisorTest.java | 7 +-
.../camel/component/chaos/ChaosPairsTest.java | 114 +++++++++++++++++++++
.../commands/action/CamelChaosSuggestAction.java | 15 ++-
.../dsl/jbang/core/commands/ai/ToolRegistry.java | 6 +-
.../core/commands/action/CamelChaosActionTest.java | 20 ++++
.../src/test/resources/golden/cli-help.txt | 8 +-
.../dsl/jbang/core/commands/mcp/RuntimeTools.java | 4 +-
.../jbang/core/commands/tui/ChaosSuggestList.java | 8 +-
.../jbang/core/commands/tui/ChaosControlTest.java | 19 ++++
13 files changed, 292 insertions(+), 24 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 4d7c00bb6874..746bab5ac884 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
@@ -244,13 +244,30 @@ and not for the route that consumes it.
message is lost between the two routes (see
<<_expectations_on_time_and_loss,Expectations on time and loss>>)
|An input from outside (not `direct` or `seda`) |The route fails at its entry
(what does the sender see: a rollback, a
retry, an error?), or the polls fail for a polling consumer; and the route
stalls for a while
+|A remote call and a dead letter channel |Two faults at once: the call is down
for a minute while the dead letter
+channel fails too. Are messages lost? The report counts what the dead letter
channel did not take as lost
+|A remote call |Two faults at once: the call is slow and fails now and then.
Do the redeliveries pile up, and how long
+does an exchange take?
|===
+Two faults at once (kind `combined`) come after the single faults, as most
bugs that lose messages need two failures
+that meet. Their rules are added together:
+
+[source]
+----
+5. route orders: An outage while the dead letter channel fails (combined)
+ route orders sends what fails to the dead letter channel direct:parked:
when http://payment/charge is down for a minute and the dead letter channel
fails too, are messages lost? The report counts what the dead letter channel
did not take as lost
+ camel cmd chaos-add --rule='endpoint=http://payment/charge error status=503
until=1m' --rule='endpoint=direct:parked error at=dlq until=1m'
+----
+
+Each of their rules counts as its own kind in what has been tried, so a pair
is never "not tried yet".
+
The rules leave the query of the endpoint out: its options do not tell which
endpoint it is, and may hold a secret.
`ChaosManager.suggest()` gives the same as JSON: `suggestions`, each with
`route`, `kind`, `recipe`, `title`, `rule`,
-`why`, `tried`, `routes` for a dead letter channel that several routes share,
and `feed`. A `feed` has its `kind`
-(one of `self`, `http`, `file` and `send`), a `hint` and `real`. The chaos
screen of the Camel TUI lists the suggestions
-(key `w`), where Enter types the rule in to change or add.
+`why`, `tried`, `routes` for a dead letter channel that several routes share,
`feed`, and `rules` for two faults at
+once. A `feed` has its `kind` (one of `self`, `http`, `file` and `send`), a
`hint` and `real`. `rules` has all the
+rules to add together. The chaos screen of the Camel TUI lists the suggestions
(key `w`), where Enter types the rule
+in to change or add (the rules of two faults at once separated by `;`).
=== What has been tried
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc
b/components/camel-chaos/src/main/docs/chaos.adoc
index 4d7c00bb6874..746bab5ac884 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -244,13 +244,30 @@ and not for the route that consumes it.
message is lost between the two routes (see
<<_expectations_on_time_and_loss,Expectations on time and loss>>)
|An input from outside (not `direct` or `seda`) |The route fails at its entry
(what does the sender see: a rollback, a
retry, an error?), or the polls fail for a polling consumer; and the route
stalls for a while
+|A remote call and a dead letter channel |Two faults at once: the call is down
for a minute while the dead letter
+channel fails too. Are messages lost? The report counts what the dead letter
channel did not take as lost
+|A remote call |Two faults at once: the call is slow and fails now and then.
Do the redeliveries pile up, and how long
+does an exchange take?
|===
+Two faults at once (kind `combined`) come after the single faults, as most
bugs that lose messages need two failures
+that meet. Their rules are added together:
+
+[source]
+----
+5. route orders: An outage while the dead letter channel fails (combined)
+ route orders sends what fails to the dead letter channel direct:parked:
when http://payment/charge is down for a minute and the dead letter channel
fails too, are messages lost? The report counts what the dead letter channel
did not take as lost
+ camel cmd chaos-add --rule='endpoint=http://payment/charge error status=503
until=1m' --rule='endpoint=direct:parked error at=dlq until=1m'
+----
+
+Each of their rules counts as its own kind in what has been tried, so a pair
is never "not tried yet".
+
The rules leave the query of the endpoint out: its options do not tell which
endpoint it is, and may hold a secret.
`ChaosManager.suggest()` gives the same as JSON: `suggestions`, each with
`route`, `kind`, `recipe`, `title`, `rule`,
-`why`, `tried`, `routes` for a dead letter channel that several routes share,
and `feed`. A `feed` has its `kind`
-(one of `self`, `http`, `file` and `send`), a `hint` and `real`. The chaos
screen of the Camel TUI lists the suggestions
-(key `w`), where Enter types the rule in to change or add.
+`why`, `tried`, `routes` for a dead letter channel that several routes share,
`feed`, and `rules` for two faults at
+once. A `feed` has its `kind` (one of `self`, `http`, `file` and `send`), a
`hint` and `real`. `rules` has all the
+rules to add together. The chaos screen of the Camel TUI lists the suggestions
(key `w`), where Enter types the rule
+in to change or add (the rules of two faults at once separated by `;`).
=== What has been tried
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 2d44cbb71ecf..5c88f23aca89 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
@@ -77,7 +77,9 @@ final class ChaosAdvisor {
DUPLICATE("duplicate"),
CONSUMER("consumer"),
LIFECYCLE("lifecycle"),
- DEAD_LETTER("dead letter channel");
+ DEAD_LETTER("dead letter channel"),
+ // two faults at once (CAMEL-26772): its rules are tried as their own
kinds
+ COMBINED("combined");
final String key;
@@ -98,12 +100,29 @@ final class ChaosAdvisor {
* @param rank higher is tried first
* @param routes the routes a rule of its kind tries it in: the route, and
the other routes of a dead letter channel
* they share, which is suggested once
+ * @param with the other rules to add with the rule, for two faults at
once (CAMEL-26772); empty for one fault
*/
record Suggestion(String route, Kind kind, String recipe, String title,
String rule, String why, int rank,
- List<String> routes) {
+ List<String> routes, List<String> with) {
Suggestion(String route, Kind kind, String recipe, String title,
String rule, String why, int rank) {
- this(route, kind, recipe, title, rule, why, rank, List.of(route));
+ this(route, kind, recipe, title, rule, why, rank, List.of(route),
List.of());
+ }
+
+ Suggestion(String route, Kind kind, String recipe, String title,
String rule, String why, int rank,
+ List<String> routes) {
+ this(route, kind, recipe, title, rule, why, rank, routes,
List.of());
+ }
+
+ /** The rules to add: the rule, and the rules to add with it. */
+ List<String> rules() {
+ if (with.isEmpty()) {
+ return List.of(rule);
+ }
+ List<String> answer = new ArrayList<>();
+ answer.add(rule);
+ answer.addAll(with);
+ return List.copyOf(answer);
}
}
@@ -230,6 +249,10 @@ final class ChaosAdvisor {
static Map<String, Map<Kind, List<String>>>
suggestedKinds(List<Suggestion> suggestions) {
Map<String, Map<Kind, List<String>>> answer = new LinkedHashMap<>();
for (Suggestion s : suggestions) {
+ if (s.kind() == Kind.COMBINED) {
+ // its rules are tried as their own kinds, which the single
suggestions offer
+ continue;
+ }
answer.computeIfAbsent(s.route(), k -> new
LinkedHashMap<>()).putIfAbsent(s.kind(), s.routes());
}
return answer;
@@ -368,6 +391,9 @@ final class ChaosAdvisor {
// the sends: each endpoint once
Set<String> seen = new LinkedHashSet<>();
+ // the first call to a remote system (not a local file), for the
faults to try two at a time
+ String call = null;
+ boolean httpCall = false;
for (Processor processor : route.filter("*")) {
if (!(processor instanceof EndpointAware aware) ||
aware.getEndpoint() == null) {
continue;
@@ -393,6 +419,10 @@ final class ChaosAdvisor {
if (!ChaosStubs.isRemote(to)) {
continue;
}
+ if (call == null && !LOCAL_FILES.contains(scheme)) {
+ call = uri;
+ httpCall = 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),
@@ -436,6 +466,29 @@ final class ChaosAdvisor {
78, shared);
}
+ // two faults at once, after the single ones: most bugs that lose
messages need two failures that meet
+ if (call != null) {
+ String fails = httpCall ? "error status=503" : "error
exception=java.net.ConnectException";
+ if (dead != null) {
+ answer.add(new Suggestion(
+ id, Kind.COMBINED, null, "An outage while the dead
letter channel fails",
+ "endpoint=" + call + " " + fails + " until=1m", "route
" + id + " sends what fails to the dead"
+ + "
letter channel " + dead + ": when " + call
+ + " is
down for a minute and the dead letter channel"
+ + "
fails too, are messages lost? The report counts"
+ + "
what the dead letter channel did not take as lost",
+ 52, List.of(id), List.of("endpoint=" + dead + " error
at=dlq until=1m")));
+ }
+ answer.add(new Suggestion(
+ id, Kind.COMBINED, null, "A slow service that also fails",
+ "endpoint=" + call + " delay duration=1s..3s rate=0.5
until=2m",
+ "a call to " + call + " that is slow and fails now and
then: do the redeliveries pile up, and how"
+
+ " long does an exchange take? An expectation such as expect="
+
+ id
+
+ ":maxTime<=30s says how long is too long",
+ 45, List.of(id), List.of("endpoint=" + call + " " + fails
+ " rate=0.2 until=2m")));
+ }
+
// the input of the route
if (from != null && ChaosStubs.isRemote(from)) {
if (route.getConsumer() instanceof ScheduledPollConsumer) {
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 55ca97db689a..5c88b3888480 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
@@ -511,8 +511,10 @@ public final class ChaosManager {
Map<String, ChaosAdvisor.Feed> feeds = new HashMap<>();
for (ChaosAdvisor.Suggestion s : ChaosAdvisor.suggest(camelContext)) {
JsonObject jo = new JsonObject();
- // a dead letter channel that routes share is tried by a rule of
its kind in any of them
- if (s.routes().stream().anyMatch(r -> tried.contains(r + " " +
s.kind().key))) {
+ // a dead letter channel that routes share is tried by a rule of
its kind in any of them; two faults at once
+ // are tried as their own kinds
+ if (s.kind() != ChaosAdvisor.Kind.COMBINED
+ && s.routes().stream().anyMatch(r -> tried.contains(r + "
" + s.kind().key))) {
jo.put("tried", true);
}
jo.put("route", s.route());
@@ -525,6 +527,10 @@ public final class ChaosManager {
}
jo.put("title", s.title());
jo.put("rule", s.rule());
+ if (!s.with().isEmpty()) {
+ // two faults at once: all the rules to add together
+ jo.put("rules", new JsonArray(s.rules()));
+ }
jo.put("why", s.why());
Route route = camelContext.getRoute(s.route());
ChaosAdvisor.Feed feed = route != null ?
feeds.computeIfAbsent(s.route(), k -> ChaosAdvisor.feed(route)) : null;
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 934e49c1882d..14a6fd12526c 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
@@ -104,8 +104,11 @@ class ChaosAdvisorTest extends ChaosTestSupport {
@Test
void everySuggestedRuleIsValid() {
for (ChaosAdvisor.Suggestion s : ChaosAdvisor.suggest(context)) {
- String id = chaos().addRule(s.rule());
- chaos().removeRule(id);
+ // two faults at once too: each of their rules
+ for (String rule : s.rules()) {
+ String id = chaos().addRule(rule);
+ chaos().removeRule(id);
+ }
}
}
diff --git
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosPairsTest.java
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosPairsTest.java
new file mode 100644
index 000000000000..537276989892
--- /dev/null
+++
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosPairsTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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.List;
+
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * What to try offers two faults at once (CAMEL-26772): an outage while the
dead letter channel fails, and a slow
+ * service that also fails, after the single faults.
+ */
+class ChaosPairsTest extends ChaosTestSupport {
+
+ @BindToRegistry("http")
+ private final TestHttpComponent http = new TestHttpComponent();
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:orders").routeId("orders")
+
.errorHandler(deadLetterChannel("seda:parked").maximumRedeliveries(2))
+ .to("http://payment/charge")
+ .to("mock:shipped");
+
from("seda:parked?pollTimeout=10").routeId("parked").to("mock:parked");
+ // a route that calls nothing remote has no pairs
+ from("direct:audit").routeId("audit").to("mock:audit");
+ }
+ };
+ }
+
+ private ChaosAdvisor.Suggestion pair(String title) {
+ return ChaosAdvisor.suggest(context).stream().filter(s ->
title.equals(s.title())).findFirst().orElseThrow();
+ }
+
+ @Test
+ void anOutageWhileTheDeadLetterChannelFails() {
+ ChaosAdvisor.Suggestion s = pair("An outage while the dead letter
channel fails");
+ assertThat(s.route()).isEqualTo("orders");
+ assertThat(s.kind()).isEqualTo(ChaosAdvisor.Kind.COMBINED);
+ assertThat(s.rules()).containsExactly(
+ "endpoint=http://payment/charge error status=503 until=1m",
+ "endpoint=seda:parked error at=dlq until=1m");
+ assertThat(s.why()).contains("dead letter channel seda:parked", "are
messages lost?");
+ }
+
+ @Test
+ void aSlowServiceThatAlsoFails() {
+ ChaosAdvisor.Suggestion s = pair("A slow service that also fails");
+ assertThat(s.rules()).containsExactly(
+ "endpoint=http://payment/charge delay duration=1s..3s rate=0.5
until=2m",
+ "endpoint=http://payment/charge error status=503 rate=0.2
until=2m");
+ assertThat(s.why()).contains("expect=orders:maxTime<=30s");
+ }
+
+ @Test
+ void afterTheSingleFaults() {
+ List<ChaosAdvisor.Suggestion> suggestions =
ChaosAdvisor.suggest(context);
+ int firstPair = suggestions.indexOf(pair("An outage while the dead
letter channel fails"));
+ // the HTTP error and the dead letter channel on their own come first
+ assertThat(suggestions.subList(0,
firstPair)).extracting(ChaosAdvisor.Suggestion::recipe)
+ .contains("http-error", "dlq-unreachable");
+ assertThat(suggestions).noneMatch(s -> "audit".equals(s.route()));
+ }
+
+ @Test
+ void theJsonHasAllTheRulesAndNoTriedForAPair() {
+ // the rules of the pair fired on their own, as their own kinds
+ chaos().addRule("endpoint=http://payment/charge error status=503
until=1");
+ template.sendBody("direct:orders", "order");
+
+ JsonArray list = (JsonArray) chaos().suggest().get("suggestions");
+ JsonObject pair = list.stream().map(JsonObject.class::cast)
+ .filter(s -> "An outage while the dead letter channel
fails".equals(s.getString("title")))
+ .findFirst().orElseThrow();
+ assertThat(pair.getString("kind")).isEqualTo("combined");
+
assertThat(pair.getString("rule")).isEqualTo("endpoint=http://payment/charge
error status=503 until=1m");
+ List<Object> rules = pair.getCollection("rules");
+ assertThat(rules).containsExactly(
+ "endpoint=http://payment/charge error status=503 until=1m",
+ "endpoint=seda:parked error at=dlq until=1m");
+ assertThat(pair.containsKey("tried")).isFalse();
+ // a single suggestion has no rules list
+ JsonObject single = list.stream().map(JsonObject.class::cast)
+ .filter(s ->
"http-error".equals(s.getString("recipe"))).findFirst().orElseThrow();
+ assertThat(single.containsKey("rules")).isFalse();
+ assertThat(single.getBoolean("tried")).isTrue();
+
+ // and combined is never "not tried yet": its rules are tried as their
own kinds
+ assertThat(chaos().report().toText()).doesNotContain("combined");
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosSuggestAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosSuggestAction.java
index 551b173148c6..e446080ecc1f 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosSuggestAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosSuggestAction.java
@@ -34,9 +34,11 @@ import picocli.CommandLine;
"(Kafka, JMS: the call is made twice), a dead
letter channel (it fails too), a",
"hand-off to another route over seda (the next
route fails, and no message may",
"be lost between the two), an input from outside
(the route fails at its entry,",
- "the polls fail, the route stalls). The most
worth trying come first, each with",
- "why and the command that adds it, and how to
feed the route with messages",
- "(curl, a file, or camel cmd send), unless it
feeds itself. Nothing is added.",
+ "the polls fail, the route stalls), and then two
faults at once (an outage while",
+ "the dead letter channel fails, a slow service
that also fails). The most worth",
+ "trying come first, each with why, the command
that adds it, and how to feed the",
+ "route with messages (curl, a file, or camel cmd
send), unless it feeds itself.",
+ "Nothing is added.",
"%nWithout a name or pid, the only integration
that runs, or the only one with",
"chaos switched on.",
"%nExamples:",
@@ -83,7 +85,12 @@ public class CamelChaosSuggestAction extends
CamelChaosAction {
sb.append(" - tried");
}
sb.append("\n ").append(s.getString("why"));
- sb.append("\n camel cmd chaos-add
--rule='").append(s.getString("rule")).append('\'');
+ // two faults at once are added together
+ List<Object> rules = s.getCollection("rules");
+ sb.append("\n camel cmd chaos-add");
+ for (Object rule : rules != null && !rules.isEmpty() ? rules :
List.of(s.getString("rule"))) {
+ sb.append(" --rule='").append(rule).append('\'');
+ }
JsonObject feed = s.getMap("feed");
if (feed != null && !"self".equals(feed.getString("kind"))) {
// how to send messages into the route, so the rule has
something to act on
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
index 476dbf4030cc..16c490f78347 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
@@ -844,8 +844,10 @@ public final class ToolRegistry {
+ "route, the kind of fault, the rule,
why to try it, and tried=true when a rule "
+ "of that kind has already faulted the
route, and feed: how to send messages "
+ "into the route (kind self, http,
file or send, a hint such as the command, and "
- + "real=true when that goes to the real
system rather than a stub). Nothing is "
- + "added: check a rule with chaos_check
and add it with chaos_add.")
+ + "real=true when that goes to the real
system rather than a stub). Two faults "
+ + "at once (kind combined, after the
single ones) have rules: all the rules to "
+ + "add together (chaos_add with them
separated by ;). Nothing is added: check a "
+ + "rule with chaos_check and add it
with chaos_add.")
.param("name", "string", CHAOS_NAME, false)
.readOnly(true).destructive(false)
.executor((ctx, args) -> {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
index 34d773dd25b7..dfab28212f77 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
@@ -170,6 +170,26 @@ class CamelChaosActionTest extends
ActionCommandTestSupport {
assertThat(out.split("real system", -1)).hasSize(2);
}
+ @Test
+ void suggestAddsTwoFaultsAtOnceTogether() throws Exception {
+ writeStatusFile(TEST_PID, "orders");
+ JsonObject pair = new JsonObject(
+ Map.of("route", "orders", "kind", "combined", "title", "An
outage while the dead letter channel fails",
+ "rule", "endpoint=http://pay error status=503
until=1m",
+ "rules", new JsonArray(
+ List.of("endpoint=http://pay error status=503
until=1m",
+ "endpoint=seda:dlq error at=dlq
until=1m")),
+ "why", "route orders sends what fails to the dead
letter channel seda:dlq"));
+ JsonObject answer = new JsonObject(Map.of("suggestions", new
JsonArray(List.of(pair))));
+
+ assertThat(callWithResponse(command(new
CamelChaosSuggestAction(main()), "orders"), answer)).isZero();
+
+ assertThat(printer.getLines()).contains(
+ "1. route orders: An outage while the dead letter channel
fails (combined)",
+ "camel cmd chaos-add --rule='endpoint=http://pay error
status=503 until=1m'"
+
+ " --rule='endpoint=seda:dlq error at=dlq until=1m'");
+ }
+
@Test
void suggestSaysWhenThereIsNothingToTry() throws Exception {
writeStatusFile(TEST_PID, "orders");
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
index dd3453c3640e..a4b2b6dd9b2d 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
@@ -810,9 +810,11 @@ breaker (the service is down for a while), a call that the
input may repeat
(Kafka, JMS: the call is made twice), a dead letter channel (it fails too), a
hand-off to another route over seda (the next route fails, and no message may
be lost between the two), an input from outside (the route fails at its entry,
-the polls fail, the route stalls). The most worth trying come first, each with
-why and the command that adds it, and how to feed the route with messages
-(curl, a file, or camel cmd send), unless it feeds itself. Nothing is added.
+the polls fail, the route stalls), and then two faults at once (an outage while
+the dead letter channel fails, a slow service that also fails). The most worth
+trying come first, each with why, the command that adds it, and how to feed the
+route with messages (curl, a file, or camel cmd send), unless it feeds itself.
+Nothing is added.
Without a name or pid, the only integration that runs, or the only one with
chaos switched on.
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
index 1a13c17f93ed..1f7f8e290b26 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
@@ -1174,7 +1174,9 @@ public class RuntimeTools {
breaker, a dead letter channel or a hand-off to another
route over seda. Each has the route, the \
kind of fault, the rule, why to try it, tried=true when a
rule of that kind has already faulted \
the route, and feed: how to send messages into the route
(kind self, http, file or send, a hint \
- such as the command, and real=true when that goes to the
real system rather than a stub). \
+ such as the command, and real=true when that goes to the
real system rather than a stub). Two \
+ faults at once (kind combined, after the single ones) have
rules: all the rules to add together \
+ (camel_runtime_chaos_add with them separated by ;). \
Nothing is added: check a rule with
camel_runtime_chaos_check and add it with \
camel_runtime_chaos_add.""")
public JsonObject camel_runtime_chaos_suggest(
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosSuggestList.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosSuggestList.java
index 6fd8e25102f1..c338c44b37ab 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosSuggestList.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosSuggestList.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
import dev.tamboui.layout.Constraint;
import dev.tamboui.layout.Layout;
@@ -71,9 +72,14 @@ final class ChaosSuggestList {
if (reply != null && reply.getCollection("suggestions") != null) {
for (Object o : reply.getCollection("suggestions")) {
if (o instanceof JsonObject jo && jo.getString("rule") !=
null) {
+ // two faults at once (CAMEL-26772): the rules are added
together, separated by ;
+ List<Object> rules = jo.getCollection("rules");
+ String rule = rules != null && !rules.isEmpty()
+ ?
rules.stream().map(String::valueOf).collect(Collectors.joining("; "))
+ : jo.getString("rule");
items.add(new Item(
jo.getString("route"), jo.getString("kind"),
jo.getString("title"),
- jo.getString("rule"), jo.getString("why"),
jo.getBooleanOrDefault("tried", false)));
+ rule, jo.getString("why"),
jo.getBooleanOrDefault("tried", false)));
}
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControlTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControlTest.java
index 549ad0b6108f..b5209809c32b 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControlTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControlTest.java
@@ -255,6 +255,25 @@ class ChaosControlTest {
assertThat(requests).hasSize(1);
}
+ @Test
+ void twoFaultsAtOnceAreTypedInTogether() throws Exception {
+ reply = (JsonObject) Jsoner.deserialize("""
+ {"suggestions": [
+ {"route": "orders", "kind": "combined", "title": "An outage
while the dead letter channel fails",
+ "rule": "endpoint=http://pay error status=503 until=1m",
+ "rules": ["endpoint=http://pay error status=503 until=1m",
"endpoint=seda:dlq error at=dlq until=1m"],
+ "why": "route orders sends what fails to the dead letter
channel seda:dlq"}]}
+ """);
+ key('w');
+ enter();
+ assertThat(tab.isTextInputActive()).isTrue();
+ // added after Enter and the confirmation, both rules in one add
+ enter();
+ key('y');
+ assertThat(requests.get(requests.size() - 1).getString("rule"))
+ .isEqualTo("endpoint=http://pay error status=503 until=1m;
endpoint=seda:dlq error at=dlq until=1m");
+ }
+
@Test
void whatToTryWithNothingToSuggest() throws Exception {
reply = (JsonObject) Jsoner.deserialize("{\"suggestions\": []}");