This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch tui-chaos-explore-start in repository https://gitbox.apache.org/repos/asf/camel.git
commit 555c4ba5e713564405307368b297a0f5cd69c133 Author: Claus Ibsen <[email protected]> AuthorDate: Tue Nov 10 21:13:31 2026 +0100 chore: camel-jbang - let there be chaos: a file route takes the files it took again (replay) Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../jbang/camel-tui-chaos-let-there-be-done.png | Bin 0 -> 337934 bytes .../images/jbang/camel-tui-chaos-let-there-be.png | Bin 0 -> 393165 bytes .../ROOT/pages/camel-jbang-tui-observe.adoc | 10 ++++- .../core/commands/tui/ChaosExploreDialog.java | 47 +++++++++++++++++---- .../jbang/core/commands/tui/ChaosExploreRun.java | 8 +++- .../src/main/resources/tui/help/chaos.md | 5 ++- .../jbang/core/commands/tui/ChaosExploreTest.java | 37 ++++++++++++++++ 7 files changed, 93 insertions(+), 14 deletions(-) diff --git a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-let-there-be-done.png b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-let-there-be-done.png new file mode 100644 index 000000000000..b311fb5d460b Binary files /dev/null and b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-let-there-be-done.png differ diff --git a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-let-there-be.png b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-let-there-be.png new file mode 100644 index 000000000000..39375677ff57 Binary files /dev/null and b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-let-there-be.png differ diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc index 8ca57af15b05..69a9cb5184e3 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc @@ -397,13 +397,19 @@ Press *L* to start an exploration by hand, on the integration you look at: pick integration suggests, the most worth trying first, each with what should hold despite its faults, and a budget of 2 to 30 minutes. Picked are a few faults not tried yet, each kind once before it is tried again in another route, and then two faults at once, such as an outage while the dead letter channel fails; only of the routes that get messages by -themselves (a timer, or a route they are called from). A route whose messages come from outside is marked, as its rule -says nothing without messages. *Space* picks a rule, *Left* and *Right* change the budget, and *Enter* starts. The picked rules are tried one +themselves (a timer, or a route they are called from), or that take files and took some already: while the rule runs, +the files the route moved to `.camel` are put back into its folder as it takes them, the same files each time (a route with +`noop=true` never takes a file of the same name again, so it is not one of them). A route whose messages come from +outside is marked, as its rule says nothing without messages. *Space* picks a rule, *Left* and *Right* change the budget, and *Enter* starts. The picked rules are tried one at a time, each for its share of the budget, and each ends with PASS, FAIL or INCONCLUSIVE, while the line above the experiments says which rule is tried. *x* stops it, which removes the rules at once. Every rule is removed when the budget ends, also when the TUI is closed. For a longer exploration, where an AI picks what to try next, use `camel cmd chaos-explore`. +image::jbang/camel-tui-chaos-let-there-be.png[Let there be chaos: the rules to pick, among them an outage of the route a circuit breaker calls, the HTTP calls of a toD, and the routes that take the files a file route took again] + +image::jbang/camel-tui-chaos-let-there-be-done.png[A rule tried for two minutes: the same three files taken again and again, and 45 orders lost at the seda hand-off between two routes] + image::jbang/camel-tui-chaos-explore.png[The exploration of the chaos example on the Chaos tab: six experiments in five minutes on a strip of time, and the failed one in detail: 2 orders lost at the dead letter channel] === CVE Audit diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreDialog.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreDialog.java index 65c21a00d316..6d634cc4044b 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreDialog.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreDialog.java @@ -62,12 +62,29 @@ final class ChaosExploreDialog { /** * A suggestion to try. * - * @param rule its rules, separated by ; for two faults at once - * @param expect what should hold, such as orders:failed<=0 - * @param fed whether its route gets messages without a feed (a timer, or a route it is called from) + * @param rule its rules, separated by ; for two faults at once + * @param expect what should hold, such as orders:failed<=0 + * @param fed whether its route gets messages without a feed (a timer, or a route it is called from) + * @param replay how many files the file route took, which are put back so it takes them again (0 for none) + * @param replayRoute the file route whose files are put back: the route, or a file route that sends to it */ record Item(String route, String kind, String title, String rule, String why, String expect, boolean tried, - boolean fed) { + boolean fed, int replay, String replayRoute) { + + Item(String route, String kind, String title, String rule, String why, String expect, boolean tried, + boolean fed) { + this(route, kind, title, rule, why, expect, tried, fed, 0, null); + } + + /** Whether its route gets messages while the rule runs: by itself, or from the files it took, put back. */ + boolean hasMessages() { + return fed || replay > 0; + } + + /** Whether the files the route took are put back when the rule starts. */ + boolean replays() { + return !fed && replay > 0; + } boolean combined() { return "combined".equals(kind); @@ -108,7 +125,9 @@ final class ChaosExploreDialog { items.add(new Item( jo.getString("route"), jo.getString("kind"), jo.getString("title"), rule, jo.getString("why"), m.find() ? m.group(1) : null, jo.getBooleanOrDefault("tried", false), - feed != null && feed.getBooleanOrDefault("fed", false))); + feed != null && feed.getBooleanOrDefault("fed", false), + feed != null && feed.getString("replay") != null ? feed.getIntegerOrDefault("files", 0) : 0, + feed != null ? feed.getStringOrDefault("replayRoute", jo.getString("route")) : null)); } } } @@ -138,7 +157,7 @@ final class ChaosExploreDialog { } for (int i = 0; i < items.size(); i++) { Item item = items.get(i); - boolean worth = !picked.get(i) && !item.tried() && item.fed() && item.expect() != null && !item.deadLetter() + boolean worth = !picked.get(i) && !item.tried() && item.hasMessages() && item.expect() != null && !item.deadLetter() && (!distinctKinds || !kinds.contains(item.kind())); if (worth && (item.combined() ? combined < PICKED_COMBINED : singles < PICKED_SINGLE)) { picked.set(i, true); @@ -184,7 +203,7 @@ final class ChaosExploreDialog { /** Why the exploration cannot start, or null when it can. */ String cannotStart() { if (picked().isEmpty()) { - return !items.isEmpty() && items.stream().noneMatch(Item::fed) + return !items.isEmpty() && items.stream().noneMatch(Item::hasMessages) ? "The routes here get their messages from outside: pick a rule (Space), and send its route messages" + " while it runs, or its verdict says nothing" : "Pick a rule to try (Space)"; @@ -282,7 +301,10 @@ final class ChaosExploreDialog { if (item.tried()) { spans.add(Span.styled(" tried", Theme.success())); } - if (!item.fed()) { + if (item.replays()) { + spans.add(Span.styled(" replays " + item.replay() + (item.replay() == 1 ? " file" : " files"), + Theme.muted())); + } else if (!item.fed()) { spans.add(Span.styled(" needs messages", Theme.warning())); } lines.add(Line.from(spans)); @@ -311,7 +333,14 @@ final class ChaosExploreDialog { lines.add(Line.from(Span.styled(" A rule of this kind was tried in route " + item.route() + " already", Theme.success()))); } - if (!item.fed()) { + if (item.replays()) { + lines.add(Line.empty()); + String who = item.route().equals(item.replayRoute()) ? "it" : "route " + item.replayRoute(); + lines.add(Line.from(Span.styled(" While the rule runs, the " + item.replay() + + (item.replay() == 1 ? " file " : " files ") + who + " took are put back into" + + " its folder as it takes them, the same files each time", + Theme.muted()))); + } else if (!item.fed()) { lines.add(Line.empty()); lines.add(Line.from(Span.styled(" Route " + item.route() + " gets its messages from outside: send it some" + " while the rule runs, or the verdict says nothing (INCONCLUSIVE)", diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreRun.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreRun.java index 9a8e39801c47..6dd1ebb8ba0e 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreRun.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreRun.java @@ -141,8 +141,11 @@ final class ChaosExploreRun { ChaosExploreDialog.Item pick = picks.get(i); String verdict; try { + // a file route that does not get messages by itself takes the files it took again JsonObject ended = ChaosExperimentRun.run(integration, pick.route() + ": " + pick.title(), - forShare(pick.rule(), share), null, share + GRACE, null); + forShare(pick.rule(), share), + pick.replays() ? ChaosExperimentRun.Feed.replay(pick.replayRoute()) : null, + share + GRACE, null); verdict = ended.getString("verdict"); if (ended.getLongOrDefault("timeLeft", 1) <= 0) { // the deadline of the exploration: camel-chaos has ended it @@ -275,7 +278,8 @@ final class ChaosExploreRun { @Override public JsonObject send(ChaosExperimentRun.Feed feed) { - // the exploration of the TUI sends no messages: it tries the rules of routes that get them by themselves + // the exploration of the TUI sends no messages: its routes get them by themselves, or take the files they + // took again (a replay, which is a chaos command) throw new UnsupportedOperationException("No feed"); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md index accfdea91b6f..98b4085650a9 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md @@ -82,7 +82,10 @@ first, each with what should hold despite its faults. Picked are a few faults not tried yet, each kind once before it is tried again in another route, and then two faults at once (an outage while the dead letter channel fails); only of routes that get messages by themselves (a timer, -or a route they are called from). A route whose messages come from +or a route they are called from), or that take files and took some +already: those are marked *replays 3 files*, and while the rule runs the +files the route moved to `.camel` are put back into its folder as it +takes them, the same files each time. A route whose messages come from outside is marked *needs messages*: send it some while its rule runs, or its verdict says nothing. Space picks a rule, Left and Right change the budget (2m to 30m), and Enter starts. The picked rules are tried one at a diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreTest.java index b0813916ce48..d5b613b184b1 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreTest.java @@ -23,6 +23,9 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import dev.tamboui.buffer.Buffer; +import dev.tamboui.layout.Rect; +import dev.tamboui.terminal.Frame; import dev.tamboui.text.Span; import dev.tamboui.tui.event.KeyCode; import dev.tamboui.tui.event.KeyEvent; @@ -194,6 +197,38 @@ class ChaosExploreTest { assertThat(TuiTestHelper.renderToString(tab, 160, 40)).contains("of 10m: 3 rules tried, 3 FAIL"); } + @Test + void aFileRouteTakesTheFilesItTookAgain() throws Exception { + ChaosExploreDialog dialog = new ChaosExploreDialog( + "shop", (JsonObject) Jsoner.deserialize( + """ + {"suggestions": [ + {"route": "inbox", "kind": "errors", "title": "Fails to write", + "rule": "endpoint=file://archive error exception=java.io.IOException rate=0.25 until=10m expect=inbox:failed<=0", + "why": "a write", "feed": {"kind": "send", "replay": "/work/inbox/.camel", "files": 3, "replayRoute": "files"}}, + {"route": "uploads", "kind": "consumer", "title": "The route fails", + "rule": "route=uploads error rate=0.05 until=10m expect=uploads:failed<=0", + "why": "an input from outside", "feed": {"kind": "file"}} + ]} + """)); + // the files it took are put back, so it has messages; a route that took none needs them from outside + assertThat(dialog.picked()).extracting(ChaosExploreDialog.Item::route).containsExactly("inbox"); + assertThat(dialog.items().get(0).replay()).isEqualTo(3); + Rect area = new Rect(0, 0, 160, 30); + Buffer buffer = Buffer.empty(area); + dialog.render(Frame.forTesting(buffer), area); + assertThat(TuiTestHelper.bufferToString(buffer)) + .contains("[x] Fails to write inbox replays 3 files", "[ ] The route fails uploads needs messages"); + + explorer.start("1234", dialog.picked(), "2m", 120_000); + await().atMost(10, TimeUnit.SECONDS).until(() -> !explorer.isRunning("1234")); + // after the rule is added, so its faults meet the files + assertThat(integration.commands).containsExactly( + "explore-start", "experiment-begin", "replay", "experiment-end", "explore-finish"); + // the file route that sends to it + assertThat(integration.replayed).containsExactly("files"); + } + @Test void stopEndsTheExploration() { integration.decided = false; @@ -255,6 +290,7 @@ class ChaosExploreTest { final List<String> commands = new CopyOnWriteArrayList<>(); final List<String> rules = new CopyOnWriteArrayList<>(); final List<String> hypotheses = new CopyOnWriteArrayList<>(); + final List<String> replayed = new CopyOnWriteArrayList<>(); volatile String budget; volatile boolean decided = true; private volatile int number; @@ -273,6 +309,7 @@ class ChaosExploreTest { hypotheses.add(request.getString("hypothesis")); answer.put("number", number); } + case "replay" -> replayed.add(request.getString("route")); case "experiment-end" -> { answer.put("number", number); answer.put("verdict", "FAIL");
