Claus Ibsen created CAMEL-26667:
-----------------------------------

             Summary: camel-chaos - findings from the hands-on round of stage 
1: report accuracy, feedback on rules, live rule state, new faults
                 Key: CAMEL-26667
                 URL: https://issues.apache.org/jira/browse/CAMEL-26667
             Project: Camel
          Issue Type: Improvement
          Components: camel-jbang
            Reporter: Claus Ibsen
             Fix For: 4.24.0


h2. Hands-on round 1 of camel-chaos (stage 1): findings and ideas

Stage 1 of camel-chaos (CAMEL-26330) was tried for real with {{camel run 
--chaos}}, driven from the Camel CLI ({{camel cmd chaos-*}}, {{camel get 
chaos}}) and from the MCP tools ({{camel_runtime_chaos_*}}), against a small 
app: a timer route {{orders}} with redelivery (2 attempts, 100ms) and a dead 
letter channel to {{seda:dlq}}, an HTTP call to a local {{platform-http}} route 
{{stock}}, a {{direct}} route with named steps, a {{seda}} hop to {{shipping}} 
and a file poller.

Every hook works as documented: HTTP {{status=503}} faults with redelivery and 
the DLC, {{step=}} error and delay, {{at=poll drop}}, {{route=}} {{corrupt}} at 
the entry, {{stall}} and {{lifecycle action=stop}}, expectations, the shutdown 
report, and the parser refuses wrong rules with clear reasons that say what to 
use instead. This issue collects what felt wrong or missing while using it, as 
input before stage 2 (CAMEL-26607) and the chaos screen (CAMEL-26608).

h3. Usability problems

# *An expectation can be met while every message went to the DLQ.* 
{{expect=orders:20}} with {{endpoint=http://...stock* error status=503}} (every 
send fails) is *met*: a handled exchange counts as completed (as documented). 
The honest check was {{expect=shipping:20}} on the downstream route (FAIL: 
"route shipping completed 0 of 20 exchanges within 8s: 20 lost"). Users and AI 
agents will write the first form.
# *No expectation on the error handler itself.* "N messages should end in the 
DLQ" works only when a route consumes the DLQ ({{expect=dlq:5}} with 
{{seda:dlq}} -> route {{dlq}}). It cannot be said for a DLC to {{log:}}, 
{{mock:}} or {{kafka:dlq}}, and the most common resilience check, "no message 
reaches the DLQ" (at most 0), cannot be said at all. And when such an 
expectation is unmet, the report says "5 lost", while the 5 messages were in 
fact recovered by redelivery: the wording is the opposite of what happened. The 
per-route {{handled}} count is already in the report, so see idea 1.
# *A message lost inside the dead letter channel is not reported.* 
{{component=seda error until=6}} also failed the send to {{seda:dlq}}; with 
{{deadLetterHandleNewException}} (the default) that exchange counts as handled, 
but never reached the DLQ ({{orders handled 11}}, {{dlq completed 10}}, a WARN 
from DeadLetterChannel). The report says {{lost: 0}} for it. The docs warn 
about {{endpoint=*}} and {{component=}} hitting the DLC, but chaos knows the 
send is the DLC send (the failure endpoint) and could report it ("r6 failed the 
send to the dead letter channel of route orders: 1 message gone"), or skip DLC 
sends unless asked for ({{at=dlq}}).
# *A rule that matches nothing gives no feedback.* 
{{endpoint=htp://localhost:8765/stock*}} (typo) and {{step=callStok}} were 
accepted with only an id, and fired 0 times. The expectation 
{{expect=shiping:10}} (no such route) logs a WARN in the application log, but 
the CLI and the MCP result only return {{\{"id":"r3"\}}}, so an AI agent never 
sees it. Suggest: {{chaos-add}} / {{chaos-change}} return warnings: "matches no 
endpoint / step / route of the context now", "expects route shiping, which does 
not exist", and the MCP tools pass them through.
# *{{chaos-clear}} does not start a fresh experiment.* After clear the old FAIL 
verdict, {{lost}} and the history stay until the application restarts. Suggest 
{{chaos-clear --reset}} (or {{chaos-reset}}) that also resets the report, the 
route counts and the verdict (a new "session", optionally a new seed).
# *A removed rule with a pending expectation keeps the report not final for 
good.* Removing the typo rule {{r3}} (pending expectation) made the report 
"FAIL (not final: an expectation is pending)" for the rest of the run. As 
documented, but in practice a removed rule's expectation should become 
{{cancelled}} (not counted, not pending).
# *"skipped" has no reason.* {{route=shipping corrupt mode=charset}} was 
skipped 79 of 79 times: the body after the HTTP call is a stream cache, which 
{{charset}} does not change ({{mode=truncate}} worked, the body became 
{{in-s}}). Neither the CLI table, the JSON, nor the dev console say why, so it 
looks like it worked. Suggest a count per skip reason ({{skipped: \{"stream 
body (charset)": 79\}}}), and a hint in the table.
# *Minor:* the JSON report lists the versions of one id as 3, 1, 2; a rule on 
{{direct:}} lists {{hooks: ["send","poll"]}} although direct has no polls (the 
hooks the target offers, not what applies).

h3. Live rule state (for the TUI chaos screen, CAMEL-26608)

Today a running rule shows {{matches}}, {{fired}} (per hook in JSON), 
{{skipped}}, {{ends in}}, and a pending expectation {{completed}} / {{time 
left}}. For "fail 5%" that is {{117 matches, 6 fired}}. What a person (and the 
chaos screen) needs to see while it runs:
* the observed rate next to the target ({{5.1% of 5%}}), and for {{every:N}} 
the next match that fires;
* when it last fired, and a short ring buffer of the last faults: time, 
exchange id, route, endpoint/step, fault (so a fault can be matched to a DLQ 
entry, a trace or the message history);
* the effect of the rule, not only its faults: of the exchanges it faulted, how 
many were recovered by redelivery, handled by the DLC, failed, still in flight. 
In the run, {{r7}} (stop {{stock}} for 3s) sent 5 orders to the DLQ, because 2 
x 100ms redelivery is too short for a 3s outage: today the report shows 
{{orders handled +5}} but not that r7 caused it;
* a rate over time (faults per second over the last minute) for a sparkline;
* mark the exchange: a faulted exchange gets a property (e.g. 
{{CamelChaosRule=r1}}) so the tracer, message history, the DLQ route and 
{{camel get history}} show "faulted by chaos rule r1".

h3. Ideas for new rules and features

# *Expectations on the error handler:* {{expect=orders:handled<=0}} (redelivery 
must recover everything), {{expect=orders:handled=5}}, 
{{expect=orders:redeliveries<=30}}, {{expect=orders:failed=0}}; the count kinds 
already exist per route.
# *Error after the send* ({{at=reply}} or {{after=true}}): the producer is 
called, then the send fails, as a timeout after the remote side did the work; 
the classic duplicate test for idempotent consumers and retries (a payment that 
is retried after it went through).
# *Duplicate:* {{endpoint=kafka:orders* duplicate}} sends the message twice 
(at-least-once delivery), to test the idempotent consumer.
# *Burst / outage windows:* {{rate=burst:5s/30s}} fails everything for 5s every 
30s, closer to a real outage than an even rate, and what circuit breakers react 
to.
# *Slow then fail:* {{delay duration=30s}} combined with an error ({{error 
after=30s}}), a timeout that happens after a long wait, which tests the 
timeouts of the route.
# *Reply corrupt* at the send: change the reply of the endpoint ({{corrupt 
at=reply mode=truncate}}), not only the request.

h3. From reading the code (each traced end to end; 2 and 3 also confirmed on 
the running app)

# *A route is counted once per pass, not once per exchange.* 
{{ChaosRoutePolicyFactory}} counts in {{onExchangeDone}}, and the route policy 
advice runs each time the route processor runs. An exchange that enters a 
{{direct}} route 3 times (a {{loop(3).to("direct:sub")}}, or a caller that 
redelivers {{to("direct:sub")}} while {{sub}} has {{noErrorHandler()}}) counts 
3 completed or failed exchanges in {{sub}}, completes {{expect=sub:30}} with 10 
messages, and makes a {{route=sub corrupt}} rule fire and use up its {{until=}} 
count 3 times. chaos.adoc says "each counted once when the route is done with 
it". Fix: count once per exchange and route (a marker on the exchange), or 
document "per pass".
# *{{exception=java.lang.InterruptedException}} bypasses all error handling.* 
{{AbstractExchange.setException}} marks the exchange interrupted, so Camel 
stops routing it: no {{onException}}, no redelivery, no DLC. Confirmed: 
{{step=enrich error exception=java.lang.InterruptedException until=3}} gave 3 
failed exchanges in {{orders}}, 0 redeliveries, nothing in the DLQ, only a WARN 
of the timer consumer. Fix: reject it (and its subclasses) in the parser, or 
wrap it.
# *More rules that can never fire are accepted silently* (adds to problem 4): 
{{component=http:localhost}} (a {{component=}} pattern is matched against the 
scheme only, so a {{:}} never matches; confirmed, 0 matches), {{rate=every:10 
until=5}} (fires 0 times), and {{message=}} with an exception class that has 
only a no-argument constructor (the message is dropped). Fix: reject the first 
two, reject or warn on the third.
# *Rules from {{camel.chaos.rules}} start their clock when the context 
initializes, not when it has started.* {{addedAt}} is set in 
{{onContextInitializing}}, so {{until=}}, {{within=}} and the lifecycle times 
include the startup. {{expect=orders:100 within=1m}} with routes that take 20s 
to start (a slow broker connect, the supervising route controller) leaves 40s. 
Fix: re-base the times of the startup rules when the context has started.
# *The report stays not final after a change too* (adds to problem 6): a frozen 
expectation of a changed or replaced rule, or one frozen by a context restart, 
reads as pending, and {{final}} includes the history, so one {{chaos-change}} 
of a rule with an undecided {{expect=}} keeps {{final=false}} for the rest of 
the process. An MCP agent loop that waits for {{final}} never ends. Fix: leave 
frozen expectations out of {{final}}.
# *The CLI text hides the lifecycle state, and an empty result says nothing.* 
{{camel get chaos}} and {{--report}} do not show the {{lifecycle}} object 
(down, back in 12s, skipped: route was stopped, left, cancelled): a {{stall}} 
row shows only {{active 1 1 0}}, and the reason only appears with {{--json}}. 
With no rules, or without camel-chaos on the classpath, {{camel get chaos}} 
prints nothing and exits 0. Fix: a LIFECYCLE column (state plus back-in or 
reason), and a "no chaos rules" / "camel-chaos is not on the classpath" line.
# *Doc nit:* chaos.adoc says {{\"}}, {{\'}} and {{\\}} are escapes inside 
quotes, but only the quote that opened the value and {{\\}} are: 
{{message="it\'s"}} keeps the backslash.

More ideas from the code read, on top of the list above: a jittered delay from 
the seeded sequence ({{step=enrich delay duration=100ms..2s rate=0.3}}); a 
start offset ({{endpoint=kafka:orders* error from=+30s until=1m}}, "the broker 
dies after warm-up", which also avoids the startup clock problem); a flapping 
route ({{route=poller stall duration=10s repeat=3 every=1m}}, for reconnect, 
back-off and supervision); more corrupt modes ({{mode=empty}}, 
{{mode=remove-header header=JMSCorrelationID}}, {{truncate}} for String and 
byte[] bodies, which today are skipped). {{SendHook.afterSend}} is already 
there and unused, so the "error after the send" and "corrupt the reply" ideas 
fit the SPI as it is; {{duplicate}} needs a "send again" result in the SendHook 
SPI, and "slow then fail" needs a delay and an error to combine (today the 
first rule that fires decides).

h3. Not in this issue

Route-entry errors, a "sends/steps of route X" filter, {{when=}} conditions and 
loose AI prompts are stage 2 (CAMEL-26607). Claus's test prompt from the design 
(2026-10-27) "the order route should have chaos, by failing 5% or when a 
message origin country is spain" was tried through MCP: {{route=orders error 
rate=0.05}} is refused (pointing to endpoint=/step=), the 5% works as 
{{endpoint=direct:inventory error rate=0.05}} (6 of 117), and 
{{when="$\{header.country\} == 'ES'"}} is refused with "not supported yet". So 
the AI needed to look up the first send of the route itself: that is the case 
for the stage 2 filter.

_Claude Code on behalf of Claus Ibsen_




--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to