gnodet-bot commented on code in PR #28539:
URL: https://github.com/apache/camel/pull/28539#discussion_r4385916704


##########
design/chaos.adoc:
##########
@@ -0,0 +1,408 @@
+---
+title: Chaos
+authors:
+  - "@allthingssecurity"
+reviewers: ["@davsclaus"]
+approvers: []
+creation-date: 2026-10-26
+last-updated: 2026-10-27
+status: proposed
+see-also:
+  - "CAMEL-26330"
+  - "CAMEL-26331"
+  - "CAMEL-26338"
+  - "CAMEL-25911"
+  - "CAMEL-25684"
+  - "CAMEL-25682"
+  - "CAMEL-26286"
+replaces: []
+superseded-by: []
+---
+
+== Summary
+
+`camel-chaos` lets a developer, or an AI tool, turn on faults in a running 
Camel application and see how its routes
+cope. A rule such as "fail 30% of the sends to `kafka:orders` for one minute" 
or "stall route `poller` for 20 s" is
+added while the application runs, for example in dev mode (`camel run --dev`). 
A report then says per route how many
+exchanges completed, failed, were handled by the error handler or were 
redelivered, whether the consumer keeps a
+failed message, and, for a rule with an expectation, how many messages were 
lost, with a verdict.
+
+With the `camel-chaos` JAR and profile `dev` (the default of `camel run`), or 
`camel.chaos.enabled=true`, its hooks
+are installed and stay idle until a rule is added. Without the JAR, as in 
production, nothing of it is there. Faults
+run in real time, and the same rules can run inside a `camel-test-dst` world 
in virtual time.
+
+Chaos needs two small changes in camel-core: a hook for every send to an 
endpoint (CAMEL-26331) and a hook for every
+step in the channel. The other levels use SPIs that exist today.
+
+== Motivation
+
+Routes are written for the happy path, and their error handling is rarely 
exercised before production. Questions such
+as "what happens to the messages in flight when the broker is slow for 30 s", 
"does this `onException` really match the
+error the HTTP endpoint returns" or "does the consumer resume after its route 
was suspended" are answered today by
+hand-written tests, one route and one fault at a time.
+
+A prototype (CAMEL-26330 links it), which hooks only `to`, reproduced 
CAMEL-25684 (camel-slack, a route suspend in the
+middle of a poll) and CAMEL-25682 (camel-resilience4j, a timeout while the 
task writes its result) on the code from
+before their fixes. The recent "remove a route" bugs (CAMEL-26286, 
CAMEL-26287, CAMEL-26288, CAMEL-26302 to
+CAMEL-26305: one route goes, and another route loses the cache, channel or 
session it shares) have a shape a chaos
+rule can produce on demand.
+
+AI tools that write and change routes need the same: a way to try a route 
against failures and read a short result.
+
+== Goals
+
+- Out of the box chaos for any Camel application in development: faults on 
sends, on the messages that enter a route,
+  on consumer polls, on processing steps and on the route lifecycle.
+- Switched on by the classpath with profile `dev` or 
`camel.chaos.enabled=true`, idle until a rule is added; rules
+  added, changed and removed live.
+- Configuration through `camel.chaos.*` options of camel-main, as for 
`camel.debug.*` and `camel.trace.*`.
+- A report per route with counts and a verdict, readable by a person and by a 
tool.
+- Control from the CLI (`camel cmd chaos`) and the camel-jbang MCP server; a 
dev console shows the rules and the report.
+- Real time by default; the same rules in virtual time inside a DST world.
+- A seed makes a run repeatable.
+
+== Non-goals
+
+- Production use. Chaos fails closed, and stays off with profile `prod` even 
when enabled.
+- Custom fault types written by users. The first version has a fixed set; a 
plugin SPI (a JAR with its own faults) can
+  come later.
+- Faults outside Camel: network partitions, killing processes, disk or clock 
faults of the host. Tools outside Camel
+  do that; this is about what Camel does with the failures it sees.
+- The video, the models (TLA+, Lean, Z3) and the demo scripts of the 
prototype. They stay on the prototype branch.
+
+== Proposal
+
+=== Switched on by the classpath
+
+`camel-chaos` registers a `ContextServicePlugin` 
(`META-INF/services/org.apache.camel.spi.ContextServicePlugin`).
+The `CamelContext` loads the plugins found on the classpath while it is built, 
before the routes are created.
+
+In camel-main the profile and the `camel.chaos.*` options are set after the 
plugins are loaded, so the plugin reads
+them when the context initializes (`LifecycleStrategy.onContextInitializing`), 
which is still before the routes are
+created. The profile comes from `ExtendedCamelContext.getProfile()`.
+
+The plugin installs the hooks below only with profile `dev`, the default of 
`camel run`, or with
+`camel.chaos.enabled=true` (a plain `Main`, tests, DST worlds), and never with 
`prod`. Otherwise it fails closed. That
+includes no profile, the usual case for a plain `Main`, Spring Boot and 
Quarkus: the profile is then null, not `prod`
+(`ProfileConfigurer.configureMain` returns early), and a `camel.chaos.rules` 
left in `application.properties` does
+nothing. When it installs the hooks, it logs a WARN that chaos is active. Each 
hook asks the `ChaosManager` whether a
+rule matches; with no rules that is one volatile read, and the call goes on 
unchanged.
+
+=== Getting it
+
+- `camel run --chaos` adds the `camel:chaos` dependency, as `--observe` adds 
`camel:observability-services` in
+  `Run.java`; `--dep camel:chaos` works too.
+- A plain `Main` or a test adds the `camel-chaos` dependency and sets 
`camel.chaos.enabled=true`.
+- Spring Boot and Quarkus: the plugin works with any `CamelContext` and 
installs the hooks with Camel profile `dev`.
+  The options are camel-main only, so rules come from the CLI connector; a 
starter or an extension can bind
+  `camel.chaos.*` later.
+
+=== Options
+
+[cols="1,1,3"]
+|===
+|Option |Default |Description
+
+|`camel.chaos.enabled` |`false`, `true` with profile `dev` |Install the hooks: 
`true` for a plain `Main`, tests and
+DST worlds, ignored with profile `prod`. They do nothing until a rule is added.
+|`camel.chaos.seed` | |Seed of the random choices; without it a seed is picked 
and shown in the report.
+|`camel.chaos.rules` | |Rules to add at startup, in the text form of Rule 
syntax, separated by `;` (marked
+`insecure:dev`, see Risks).
+|`camel.chaos.maxDuration` |`10m` |Upper bound for every rule: a rule without 
`until` ends after it, and a
+`duration` of `stall` or `lifecycle` may not be longer (such a rule is 
rejected when it is added).
+|`camel.chaos.reportOnShutdown` |`true` |Log the report when the context stops.
+|===
+
+They are a `ChaosConfigurationProperties` group in camel-main, as 
`DebuggerConfigurationProperties` is for
+`camel.debug.*`, so the catalog and tooling know them.
+
+=== Rules
+
+A rule has a target, a fault, how often it fires, and when it ends:
+
+[source]
+----
+target:  route=<id or pattern> | endpoint=<uri pattern> | component=<name> | 
step=<processor id pattern>
+fault:   error | delay | drop | corrupt | lifecycle (stall)   (parameters: see 
Rule syntax)
+rate:    a probability (0.3), or every Nth match
+until:   a number of matches, a duration, or removed by hand
+----
+
+Rules are added, changed, listed and removed live. A rule ends by itself after 
its count or duration (at most
+`camel.chaos.maxDuration`), so a forgotten rule cannot keep breaking a dev 
instance. Rules stay when the routes are
+reloaded in dev mode.
+
+==== Rule syntax
+
+A rule is one line: the target, the fault and its parameters as `name=value`, 
then `rate` and `until`. `rate=0.3` is a
+probability and `rate=every:5` fires on every 5th match; without `rate` every 
match fires. `until=100` is a number of
+matches and `until=2m` a duration; without `until` the rule ends after 
`camel.chaos.maxDuration`. A value with spaces
+is quoted. Which hooks a fault can use is in the table of the next section.
+
+[cols="1,2,3"]
+|===
+|Fault |Parameters |Effect
+
+|`error`
+|`exception=<class>`, `message=<text>`; for the HTTP components 
`status=<code>`, `header.<name>=<value>` and
+`body=<text>`
+|Throws that exception. With `status` it is the exception the HTTP component 
throws for that status; when the endpoint
+has `throwExceptionOnFailure=false`, chaos returns the response instead, as 
the real producer does: the status in the
+`CamelHttpResponseCode` header, the `header.*` values and the `body` (empty 
without it). All parameters are optional:
+without `exception` (and without `status`) the fault throws a 
`ChaosException`, a `RuntimeCamelException` of
+camel-chaos, with the `message` or a default one, on any endpoint. A rule 
whose exception class is not found, is not
+a `Throwable`, or has neither a constructor taking a `String` message nor a 
no-argument one is rejected when it is
+added, with an error that names the class.
+
+|`delay`
+|`duration=<time>`, such as `500ms` or `2s`
+|Waits that long before the send, the step or the poll.
+
+|`drop`
+|none
+|A send that reports success without calling the producer, or a skipped poll.
+
+|`corrupt`
+|`mode=headers\|charset\|truncate`
+|Changes the message: header values with random data, a body in an unusual 
charset (characters outside US-ASCII), or
+an `InputStream` body that ends too soon. `charset` applies to a body that is 
text or converts to text, and `truncate`
+to an `InputStream` or stream cache body; on another body the message is left 
as it is and the report counts the rule
+as skipped for that message, so a fault that did nothing is visible.
+
+|`lifecycle`
+|`action=suspend\|stop\|remove`, `duration=<time>`
+|Suspends, stops or removes the target route, then resumes it, starts it or 
adds its definition again after the
+duration. `stall duration=<time>` is short for `lifecycle action=suspend 
duration=<time>`. Both take only a `route=`
+target; a rule with another target is rejected when it is added.
+|===
+
+`stall` and `lifecycle` act once on the route when the rule is added; `rate` 
and `until` do not apply to them, and
+their `duration` is bounded by `camel.chaos.maxDuration`.
+
+In `application.properties`:
+
+[source,properties]
+----
+camel.chaos.seed = 42
+camel.chaos.rules = \
+  endpoint=kafka:orders* error exception=java.net.ConnectException 
message="broker down" rate=0.3 until=1m;\
+  endpoint=https:inventory* error status=503 header.Retry-After=5 rate=every:4 
until=50;\
+  route=poller stall duration=20s
+----
+
+With the CLI, against a running application:
+
+[source,bash]
+----
+camel cmd chaos add --rule='endpoint=kafka:orders* delay duration=2s rate=0.2 
until=5m'
+camel cmd chaos add --rule='route=orders corrupt mode=truncate rate=0.1 
until=20'
+camel cmd chaos add --rule='route=orders lifecycle action=remove duration=10s'
+----
+
+=== Hooks at all levels
+
+[cols="1,2,2"]
+|===
+|Level |Hook |Faults
+
+|Sends to any endpoint
+|The send hook of camel-core (CAMEL-26331): one helper that the six places 
that fire `ExchangeSendingEvent` share
+(`SendProcessor`, `DefaultProducerCache` twice, `EventNotifierCallback`, 
`MulticastProcessor`,
+`EventNotifierProducer`), with a hook before and after the producer.
+|`error` (the producer is not called), `delay`, `drop` (a send that reports 
success), `corrupt` (the message sent).
+
+|Messages that enter a route
+|A `RoutePolicy`, added to every route by a `RoutePolicyFactory`. Its 
`onExchangeBegin` sees every exchange that
+enters a route and can change the message.
+|`corrupt`: header values with random data, a body in an unusual charset, an 
`InputStream` that ends too soon. Not
+`drop`: the exchange already belongs to its consumer, and dropping it there 
would make the consumer commit a message
+the route never processed (a file consumer deletes the file), a loss caused by 
chaos rather than by the route. `drop`
+is at the sends and the polls instead.
+
+|Consumer polls
+|The `PollingConsumerPollStrategy` of a `ScheduledPollConsumer`, which can be 
changed at runtime with
+`setPollStrategy`. Its `begin`, `commit` and `rollback` run around every poll. 
Chaos wraps the consumer's own strategy
+while a rule is active and puts it back when the rule ends.
+|A failed poll (`error`), a slow poll (`delay`), a skipped poll (`drop`).
+
+|Every step
+|The channel (`CamelInternalProcessor`), next to the check that skips a 
processor disabled with
+`camel cmd disable-processor`. That check runs before the error handler of the 
step, so a failure is raised where the
+error handler calls the step, as for a send. A small change in camel-core, 
CAMEL-26338.
+|`delay` or `error` at a step.
+
+|Route lifecycle
+|The `RouteController` (suspend, resume, stop, start), and 
`CamelContext.removeRoute` with the route definition added
+again.
+|`stall` (suspend, then resume); `lifecycle` (suspend, stop or remove the 
route, then bring it back).
+|===
+
+The send hook covers every send whatever started it: `to`, `toD`, `wireTap`, 
`enrich`, `recipientList`, `multicast`
+and a `ProducerTemplate`. The components stay the real ones (a 
`KafkaComponent` stays a `KafkaComponent`), and the
+routes get no extra processors. For a send or a step of a route the fault is 
raised inside the error handler, so it is
+a real failure for `onException`, redelivery and the dead letter channel.
+
+A fault has a real type where it matters, so that the `onException` of the 
route is what gets tested: for the HTTP
+components, the exception the component throws for a status code 
(`HttpOperationFailedException` of camel-http-base,
+`NettyHttpOperationFailedException` for camel-netty-http) with the status code 
and headers given in the rule (or the
+response, with `throwExceptionOnFailure=false`); for any other endpoint, the 
exception class and message given in the
+rule, or a `ChaosException` when the rule gives none.
+
+=== Time
+
+The `delay` fault sleeps through `TimeSource` (camel-util, CAMEL-25911), and 
the report reads its times through it.
+The timed actions (the resume or restart after `stall` or `lifecycle`, the end 
of a rule) are tasks on a scheduled
+executor of the `ExecutorServiceManager`, not threads of their own or a 
`java.util.Timer`. By default that is the
+system clock, so a person sees a 20 s stall last 20 s.
+
+In a `camel-test-dst` world the harness runs those executors and installs its 
virtual clock in `TimeSource`: a 10 minute
+stall takes milliseconds, and a seed gives the same report again, times 
included, as they come from `TimeSource`. Event
+timestamps stay on `System` in a DST run (`test-dst.adoc`), so the report does 
not use them. A `stall` or `lifecycle`
+rule meets the one-stack model of `test-dst.adoc`: a suspend or stop cannot 
see an exchange that is blocked in a
+synchronous call below it on the stack finish, so such a world calls its 
downstream with `DstCamel.asyncStep`. Chaos
+does not need DST.
+
+The Javadoc of `TimeSource` says it is internal and not an API for components. 
`camel-chaos` may use it: it reads the
+clock and sleeps through it, and never installs a provider (only a test 
harness such as `camel-test-dst` does that).
+The Javadoc is updated to say so with the first chaos pull request.
+
+=== The report
+
+The plugin counts per route with an event notifier, as the prototype does: 
completed, failed, handled by the error
+handler, redelivered, and the faults that fired. Attempts are not tied to one 
message: each exchange is counted once,
+so a file that fails and completes on the next poll is one failed and one 
completed exchange. Tying them by a key (the
+file name, `JMSMessageID`, the Kafka offset) is not in the first version.
+
+A failed exchange shows as "failed, kept by the consumer" when Camel knows 
that the consumer keeps the message and
+reads it again, and as "failed" otherwise. (An exchange the error handler 
handled is not failed; its consumer commits
+it.) The plugin reads the consumer's endpoint options through its generated 
configurer (`PropertyConfigurerGetter`),
+so options set on the component count too, without a dependency on the 
components:
+
+[cols="1,3,2"]
+|===
+|Consumer |Kept |Not kept
+
+|`file`, `ftp`, `sftp`, `ftps`
+|Without `moveFailed`: the rollback leaves the file (with `noop=true` it 
removes it from the idempotent repository),
+and the next poll reads it again.
+|With `moveFailed`.
+
+|`jms`, `activemq`, `amqp`
+|`transacted=true`, or `acknowledgementModeName=CLIENT_ACKNOWLEDGE` with 
`asyncConsumer=false` (the default): the
+listener throws, and the session rolls back or recovers.
+|Otherwise.
+
+|`kafka`
+|`breakOnFirstError=true`: the failed offset is not committed, and the 
consumer reads the record again.
+|`breakOnFirstError=false` (the default).
+
+|Other consumers
+|
+|Always.
+|===
+
+Without an expectation the report never says "lost", and the verdict is PASS: 
chaos makes exchanges fail on purpose.
+A rule can carry an expectation ("route `orders` completes 100 more messages 
within 2m"), which also finds messages

Review Comment:
   💡 **Expectation syntax not defined:** The rule syntax section defines 
`target`, `fault`, `rate`, and `until` — but `expectation` is introduced here 
without a formal definition. The example ("route `orders` completes 100 more 
messages within 2m") shows the *concept* but a developer reading the Rule 
Syntax section cannot build the parser. Consider adding an `expectation=` 
parameter line to the rule syntax block, or at minimum a forward reference like 
"(expectation syntax: see implementation PR)".



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to