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 85d10ff730d6 CAMEL-26699: camel-chaos - the known rules say what they
uncover, when not to use them and what to watch (#29359)
85d10ff730d6 is described below
commit 85d10ff730d6688dbff70f1f26f3d12ae67e52be
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 6 22:30:17 2026 +0100
CAMEL-26699: camel-chaos - the known rules say what they uncover, when not
to use them and what to watch (#29359)
Each recipe of the chaos model now has a fuller description and three lists
(uncovers, avoid, watch) that guide the one who picks it. The build writes them
into chaos-model.json and the catalog, ChaosModelTest checks every recipe has
them, and the TUI add form shows them as bullets under the recipe and while the
rule is filled in.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../org/apache/camel/catalog/chaos-model.json | 229 ++++++++++++++++--
.../org/apache/camel/catalog/docs/chaos.adoc | 16 +-
.../org/apache/camel/chaos/chaos-model.json | 229 ++++++++++++++++--
components/camel-chaos/src/main/docs/chaos.adoc | 16 +-
.../apache/camel/component/chaos/ChaosModel.java | 266 +++++++++++++++++++--
.../camel/component/chaos/ChaosModelTest.java | 15 ++
.../jbang/core/commands/tui/ChaosRecipeForm.java | 28 ++-
.../dsl/jbang/core/commands/tui/ChaosRecipes.java | 10 +-
.../src/main/resources/tui/help/chaos.md | 4 +-
.../core/commands/tui/ChaosRecipeFormTest.java | 15 ++
.../jbang/core/commands/tui/ChaosRecipesTest.java | 8 +
11 files changed, 773 insertions(+), 63 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 7505fb834508..75d82165406c 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
@@ -492,7 +492,20 @@
"id": "http-error",
"group": "Downstream",
"title": "Returns an error (HTTP status)",
- "description": "An HTTP call fails with a status, as for a service
that is down or overloaded: tests onException, redelivery and the dead letter
channel",
+ "description": "An HTTP call fails with a status, as a service that is
down (503), overloaded (429) or broken (500) answers. The exchange fails with
the exception of the HTTP component, as the real call would",
+ "uncovers": [
+ "An error handler that does not retry, or retries a status that will
not change, such as 400 or 404",
+ "Redeliveries without a delay, which hit an overloaded service again
at once",
+ "Messages that are lost instead of reaching the dead letter channel"
+ ],
+ "avoid": [
+ "The endpoint is not HTTP: use Refuses the connection",
+ "To test a timeout: a status comes back at once, use Times out"
+ ],
+ "watch": [
+ "The redeliveries of the step and the exchanges handled by the error
handler",
+ "The dead letter channel, and a circuit breaker around the call if
there is one"
+ ],
"rule": "endpoint=${endpoint} error status=${status} rate=${rate}
until=${until}",
"placeholders": [
{
@@ -538,7 +551,19 @@
"id": "connection-refused",
"group": "Downstream",
"title": "Refuses the connection",
- "description": "The send fails as when the other side is not there",
+ "description": "The send fails with a ConnectException before anything
is sent, as when the service is not running or a firewall blocks it",
+ "uncovers": [
+ "Whether a connection error is retried: that is safe, as nothing
reached the other side",
+ "Long redelivery delays that hold the consumer while the service is
down",
+ "A circuit breaker that does not open"
+ ],
+ "avoid": [
+ "To test an HTTP status from a service that is up: use Returns an
error"
+ ],
+ "watch": [
+ "The redeliveries and the exchanges handled by the error handler",
+ "The state of a circuit breaker around the call"
+ ],
"rule": "endpoint=${endpoint} error
exception=java.net.ConnectException rate=${rate} until=${until}",
"placeholders": [
{
@@ -570,7 +595,20 @@
"id": "slow",
"group": "Downstream",
"title": "Is slow",
- "description": "Each send waits a different time within a range, as a
slow service does",
+ "description": "Each send waits a time within the range before it is
made, as a slow service. The exchange then goes on as usual: the cost is time,
not errors",
+ "uncovers": [
+ "A consumer that falls behind: the throughput of the route drops and
messages wait in front of it",
+ "Thread pools that fill up with waiting exchanges",
+ "Callers of the route (request\/reply) that time out"
+ ],
+ "avoid": [
+ "To test the timeout of the client: the wait is before the call, so
the client's timeout does not fire. Use Times out",
+ "On a route started by a timer: the timer waits for the exchange, so
there are fewer messages, not more load"
+ ],
+ "watch": [
+ "The mean, p95 and p99 time of the routes, and their throughput",
+ "The exchanges in flight"
+ ],
"rule": "endpoint=${endpoint} delay duration=${duration} rate=${rate}
until=${until}",
"placeholders": [
{
@@ -609,7 +647,19 @@
"id": "timeout",
"group": "Downstream",
"title": "Times out",
- "description": "The send waits, then fails without calling the
producer, as a call that times out",
+ "description": "The send waits, then fails with a
SocketTimeoutException without calling the producer, as a call that times out",
+ "uncovers": [
+ "Whether a timeout is retried, and whether that is safe: in real
life the other side may have done the work",
+ "How long a message takes when every attempt times out: the wait
times the attempts",
+ "A consumer that stops taking messages while its threads wait"
+ ],
+ "avoid": [
+ "A long wait with a high rate: each fault holds a thread for that
time"
+ ],
+ "watch": [
+ "The time of the route and the exchanges in flight",
+ "The redeliveries and the dead letter channel"
+ ],
"rule": "endpoint=${endpoint} error after=${after}
exception=java.net.SocketTimeoutException rate=${rate} until=${until}",
"placeholders": [
{
@@ -648,7 +698,19 @@
"id": "failed-after-success",
"group": "Downstream",
"title": "Did the work, then failed",
- "description": "The send succeeds and then fails, as a reply that is
lost: a redelivery sends it again, so this tests that a duplicate does no harm",
+ "description": "The send is made and succeeds, then the exchange
fails, as when the reply is lost. The error handler sends it again, so the
other side gets it twice",
+ "uncovers": [
+ "Duplicates: an order booked twice, a payment taken twice",
+ "Whether the other side or the route is idempotent, such as with an
idempotent consumer or a key the other side checks"
+ ],
+ "avoid": [
+ "Against a real service where a duplicate costs, such as one that
charges or sends mail to people",
+ "With a high count: until=1 makes one send fail, which is enough to
see one duplicate"
+ ],
+ "watch": [
+ "What the other side received: two of the same",
+ "The redeliveries of the step"
+ ],
"rule": "endpoint=${endpoint} error at=reply until=${until}",
"placeholders": [
{
@@ -673,7 +735,19 @@
"id": "outage",
"group": "Downstream",
"title": "Is down for a while",
- "description": "Every send fails during the first part of each period,
as outage windows",
+ "description": "Every send fails during the first part of each period
and works for the rest, as a service that goes down and comes back",
+ "uncovers": [
+ "A circuit breaker that opens during the outage and closes after it",
+ "Redelivery that gives up before the service is back, so the
messages go to the dead letter channel",
+ "How long the route needs to catch up after an outage"
+ ],
+ "avoid": [
+ "An outage longer than all the redeliveries together sends every
message in it to the dead letter channel: make sure that is what you want to
test"
+ ],
+ "watch": [
+ "The faults per second: on during the outage, off between",
+ "The state of a circuit breaker, and the exchanges handled by the
error handler"
+ ],
"rule": "endpoint=${endpoint} error rate=burst:${outage}\/${period}
until=${until}",
"placeholders": [
{
@@ -712,7 +786,19 @@
"id": "corrupt-entry",
"group": "Messages",
"title": "Arrives corrupt",
- "description": "The messages that enter the route are changed: random
headers, the wrong charset, cut in half or empty",
+ "description": "The messages that enter the route are changed before
its first step: random headers, the wrong charset, cut in half or empty",
+ "uncovers": [
+ "Validation that is missing, so a bad message gets deep into the
route before it fails",
+ "A poison message that is redelivered again and again, instead of
going to the dead letter channel",
+ "Unmarshal and parse errors that are not handled"
+ ],
+ "avoid": [
+ "When the body is a stream or a file: the charset and the cut change
text and bytes only, so those messages are skipped (see the reasons it skipped)"
+ ],
+ "watch": [
+ "The errors of the steps that read the body",
+ "The messages the rule skipped, and why"
+ ],
"rule": "route=${route} corrupt mode=${mode} rate=${rate}
until=${until}",
"placeholders": [
{
@@ -761,7 +847,17 @@
"id": "empty-body",
"group": "Messages",
"title": "Is sent with an empty body",
- "description": "The message sent to the endpoint has an empty body",
+ "description": "The message sent to the endpoint has an empty body, as
a sender with nothing to send",
+ "uncovers": [
+ "A service, or the next route, that does not check for an empty
body",
+ "An empty file or message that is accepted and passed on"
+ ],
+ "avoid": [
+ "To test the route itself with bad input: use Arrives corrupt on the
route"
+ ],
+ "watch": [
+ "The route or the service that receives it"
+ ],
"rule": "endpoint=${endpoint} corrupt mode=empty rate=${rate}
until=${until}",
"placeholders": [
{
@@ -793,7 +889,17 @@
"id": "missing-header",
"group": "Messages",
"title": "Is sent without a header",
- "description": "A header is missing from the message sent to the
endpoint",
+ "description": "A header is removed from the message sent to the
endpoint, such as a correlation id, a trace id or a content type",
+ "uncovers": [
+ "A receiver that needs the header and fails without it, or worse,
goes on and does the wrong thing",
+ "Tracing that breaks, as the trace id does not reach the next
service"
+ ],
+ "avoid": [
+ "With a header the endpoint does not send on, such as most Camel
headers: nothing changes"
+ ],
+ "watch": [
+ "The route or the service that receives it"
+ ],
"rule": "endpoint=${endpoint} corrupt mode=remove-header
header=${header} rate=${rate} until=${until}",
"placeholders": [
{
@@ -834,7 +940,18 @@
"id": "poll-error",
"group": "Consumers and routes",
"title": "Polls fail",
- "description": "The polls of a scheduled poll consumer fail, as when
the source is not reachable",
+ "description": "The polls of a scheduled poll consumer fail, as when
the folder, the server or the database is not reachable. No exchange is made:
the consumer polls again on its schedule",
+ "uncovers": [
+ "A consumer without a backoff, which fails every poll and fills the
log",
+ "A health check that does not show a consumer that cannot poll"
+ ],
+ "avoid": [
+ "On a consumer that does not poll on a schedule, such as a timer,
Kafka or JMS: the rule fires 0 times"
+ ],
+ "watch": [
+ "The errors and the backoff of the consumer",
+ "The log and the health checks"
+ ],
"rule": "endpoint=${endpoint} error at=poll rate=${rate}
until=${until}",
"placeholders": [
{
@@ -866,7 +983,18 @@
"id": "poll-skip",
"group": "Consumers and routes",
"title": "Polls are skipped",
- "description": "The polls of a scheduled poll consumer are skipped, as
a consumer that falls behind",
+ "description": "The polls of a scheduled poll consumer are skipped, as
a consumer that falls behind: the messages wait at the source",
+ "uncovers": [
+ "Messages that pile up at the source",
+ "Alerts on a consumer that goes quiet",
+ "Batching or ordering that counts on every poll"
+ ],
+ "avoid": [
+ "On a consumer that does not poll on a schedule, such as a timer,
Kafka or JMS: the rule fires 0 times"
+ ],
+ "watch": [
+ "The polls of the consumer, and the routes after it that get nothing"
+ ],
"rule": "endpoint=${endpoint} drop at=poll rate=${rate}
until=${until}",
"placeholders": [
{
@@ -898,7 +1026,19 @@
"id": "stall",
"group": "Consumers and routes",
"title": "The route stalls",
- "description": "The route is suspended for a while, and its messages
wait",
+ "description": "The route is suspended for the time, then resumed: its
consumer takes no messages, and they wait in a queue or at the source",
+ "uncovers": [
+ "Senders that block or fail when the route does not take messages,
such as direct callers or a full seda queue",
+ "Health checks and alerts for a route that stopped",
+ "How long the route needs to catch up"
+ ],
+ "avoid": [
+ "On a route others call directly and wait for: they fail while it
stalls, which may or may not be the test"
+ ],
+ "watch": [
+ "The state of the route",
+ "The callers, and the queue in front of the route"
+ ],
"rule": "route=${route} stall duration=${duration}",
"placeholders": [
{
@@ -923,7 +1063,19 @@
"id": "flap",
"group": "Consumers and routes",
"title": "The route flaps",
- "description": "The route stops and starts again several times",
+ "description": "The route stops and starts again, several times, as a
consumer that keeps losing its connection",
+ "uncovers": [
+ "Messages lost or delivered twice across a restart",
+ "Connections or other resources that leak on each start",
+ "Health checks and alerts that flap with it"
+ ],
+ "avoid": [
+ "On a route whose consumer takes long to start: make the time
between longer than the start"
+ ],
+ "watch": [
+ "The state and the uptime of the route",
+ "The effect: completed against failed or lost messages"
+ ],
"rule": "route=${route} lifecycle action=stop duration=${duration}
repeat=${repeat} every=${every}",
"placeholders": [
{
@@ -962,7 +1114,18 @@
"id": "removed",
"group": "Consumers and routes",
"title": "The route is removed",
- "description": "The route is removed, and added back after a while",
+ "description": "The route is removed from the integration and added
back after the time, as when one route is reloaded or redeployed",
+ "uncovers": [
+ "Routes that call it and fail while it is gone",
+ "State kept in memory that is lost with it, such as an aggregator or
an idempotent repository"
+ ],
+ "avoid": [
+ "On a route whose state in memory you want to keep"
+ ],
+ "watch": [
+ "The callers of the route",
+ "Its counters, which start again from zero"
+ ],
"rule": "route=${route} lifecycle action=remove duration=${duration}",
"placeholders": [
{
@@ -987,7 +1150,17 @@
"id": "dlq-unreachable",
"group": "Error handling",
"title": "The dead letter channel is unreachable",
- "description": "The send of the dead letter channel fails, so the
messages it handles are lost (add it before the rule that makes them fail)",
+ "description": "The send of the dead letter channel fails, so the
messages it should keep are lost. Add it before the rule that makes the
messages fail",
+ "uncovers": [
+ "That the dead letter channel is a single point of failure",
+ "Whether lost messages are noticed: the report counts them as lost,
and the verdict fails"
+ ],
+ "avoid": [
+ "On its own: without another rule, or a real error, that sends
messages to the dead letter channel, nothing happens"
+ ],
+ "watch": [
+ "The messages lost, and the verdict"
+ ],
"rule": "endpoint=${endpoint} error at=dlq",
"placeholders": [
{
@@ -1005,7 +1178,19 @@
"id": "step-error",
"group": "Error handling",
"title": "A step fails",
- "description": "A step of a route fails, as when its processor throws",
+ "description": "A step of a route fails, as when its processor throws
because of a bug or bad data",
+ "uncovers": [
+ "onException clauses that do not match the exception",
+ "Redelivery of a step that will fail again: a bug is not a temporary
error",
+ "Work done by the steps before it that is not undone"
+ ],
+ "avoid": [
+ "To test a call to another system: put the fault on its endpoint,
with Returns an error or Refuses the connection"
+ ],
+ "watch": [
+ "The redeliveries of the step and the exchanges handled by the error
handler",
+ "The errors, with the step that failed"
+ ],
"rule": "step=${step} error rate=${rate} until=${until}",
"placeholders": [
{
@@ -1037,7 +1222,17 @@
"id": "step-slow",
"group": "Error handling",
"title": "A step is slow",
- "description": "A step of a route waits before its processor is
called",
+ "description": "A step of a route waits before its processor is
called, as a slow lookup or calculation",
+ "uncovers": [
+ "The step that the time of the route comes from",
+ "Callers of the route that wait for it, and time out"
+ ],
+ "avoid": [
+ "In a route started by a timer: the timer waits for the exchange, so
there are fewer messages, not more load"
+ ],
+ "watch": [
+ "The times of the steps and of the route"
+ ],
"rule": "step=${step} delay duration=${duration} rate=${rate}
until=${until}",
"placeholders": [
{
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 bfb51bdfaba6..35485f8e0688 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
@@ -625,7 +625,8 @@ Camel catalog (`CamelCatalog.chaosModelJSon()`). It has the
targets, the hooks o
offers (and whether it is reached only with `at=`), the parameters of each
fault with their types and limits, the
options, the kinds and ops of an expectation, the limits, and recipes: known
rules in plain words, grouped by what goes
wrong, each a rule with placeholders (`+${endpoint}+`, `+${rate}+`) and what
to pick them from (the endpoints the routes
-send to, the routes, the steps), such as:
+send to, the routes, the steps). To guide the one who picks a recipe, each
says what it does, what it can uncover, when
+not to use it, and what to watch while it runs, such as:
[source,json]
----
@@ -633,6 +634,19 @@ send to, the routes, the steps), such as:
"id": "failed-after-success",
"group": "Downstream",
"title": "Did the work, then failed",
+ "description": "The send is made and succeeds, then the exchange fails, as
when the reply is lost. The error handler sends it again, so the other side
gets it twice",
+ "uncovers": [
+ "Duplicates: an order booked twice, a payment taken twice",
+ "Whether the other side or the route is idempotent, such as with an
idempotent consumer or a key the other side checks"
+ ],
+ "avoid": [
+ "Against a real service where a duplicate costs, such as one that charges
or sends mail to people",
+ "With a high count: until=1 makes one send fail, which is enough to see
one duplicate"
+ ],
+ "watch": [
+ "What the other side received: two of the same",
+ "The redeliveries of the step"
+ ],
"rule": "endpoint=${endpoint} error at=reply until=${until}"
}
----
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 7505fb834508..75d82165406c 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
@@ -492,7 +492,20 @@
"id": "http-error",
"group": "Downstream",
"title": "Returns an error (HTTP status)",
- "description": "An HTTP call fails with a status, as for a service
that is down or overloaded: tests onException, redelivery and the dead letter
channel",
+ "description": "An HTTP call fails with a status, as a service that is
down (503), overloaded (429) or broken (500) answers. The exchange fails with
the exception of the HTTP component, as the real call would",
+ "uncovers": [
+ "An error handler that does not retry, or retries a status that will
not change, such as 400 or 404",
+ "Redeliveries without a delay, which hit an overloaded service again
at once",
+ "Messages that are lost instead of reaching the dead letter channel"
+ ],
+ "avoid": [
+ "The endpoint is not HTTP: use Refuses the connection",
+ "To test a timeout: a status comes back at once, use Times out"
+ ],
+ "watch": [
+ "The redeliveries of the step and the exchanges handled by the error
handler",
+ "The dead letter channel, and a circuit breaker around the call if
there is one"
+ ],
"rule": "endpoint=${endpoint} error status=${status} rate=${rate}
until=${until}",
"placeholders": [
{
@@ -538,7 +551,19 @@
"id": "connection-refused",
"group": "Downstream",
"title": "Refuses the connection",
- "description": "The send fails as when the other side is not there",
+ "description": "The send fails with a ConnectException before anything
is sent, as when the service is not running or a firewall blocks it",
+ "uncovers": [
+ "Whether a connection error is retried: that is safe, as nothing
reached the other side",
+ "Long redelivery delays that hold the consumer while the service is
down",
+ "A circuit breaker that does not open"
+ ],
+ "avoid": [
+ "To test an HTTP status from a service that is up: use Returns an
error"
+ ],
+ "watch": [
+ "The redeliveries and the exchanges handled by the error handler",
+ "The state of a circuit breaker around the call"
+ ],
"rule": "endpoint=${endpoint} error
exception=java.net.ConnectException rate=${rate} until=${until}",
"placeholders": [
{
@@ -570,7 +595,20 @@
"id": "slow",
"group": "Downstream",
"title": "Is slow",
- "description": "Each send waits a different time within a range, as a
slow service does",
+ "description": "Each send waits a time within the range before it is
made, as a slow service. The exchange then goes on as usual: the cost is time,
not errors",
+ "uncovers": [
+ "A consumer that falls behind: the throughput of the route drops and
messages wait in front of it",
+ "Thread pools that fill up with waiting exchanges",
+ "Callers of the route (request\/reply) that time out"
+ ],
+ "avoid": [
+ "To test the timeout of the client: the wait is before the call, so
the client's timeout does not fire. Use Times out",
+ "On a route started by a timer: the timer waits for the exchange, so
there are fewer messages, not more load"
+ ],
+ "watch": [
+ "The mean, p95 and p99 time of the routes, and their throughput",
+ "The exchanges in flight"
+ ],
"rule": "endpoint=${endpoint} delay duration=${duration} rate=${rate}
until=${until}",
"placeholders": [
{
@@ -609,7 +647,19 @@
"id": "timeout",
"group": "Downstream",
"title": "Times out",
- "description": "The send waits, then fails without calling the
producer, as a call that times out",
+ "description": "The send waits, then fails with a
SocketTimeoutException without calling the producer, as a call that times out",
+ "uncovers": [
+ "Whether a timeout is retried, and whether that is safe: in real
life the other side may have done the work",
+ "How long a message takes when every attempt times out: the wait
times the attempts",
+ "A consumer that stops taking messages while its threads wait"
+ ],
+ "avoid": [
+ "A long wait with a high rate: each fault holds a thread for that
time"
+ ],
+ "watch": [
+ "The time of the route and the exchanges in flight",
+ "The redeliveries and the dead letter channel"
+ ],
"rule": "endpoint=${endpoint} error after=${after}
exception=java.net.SocketTimeoutException rate=${rate} until=${until}",
"placeholders": [
{
@@ -648,7 +698,19 @@
"id": "failed-after-success",
"group": "Downstream",
"title": "Did the work, then failed",
- "description": "The send succeeds and then fails, as a reply that is
lost: a redelivery sends it again, so this tests that a duplicate does no harm",
+ "description": "The send is made and succeeds, then the exchange
fails, as when the reply is lost. The error handler sends it again, so the
other side gets it twice",
+ "uncovers": [
+ "Duplicates: an order booked twice, a payment taken twice",
+ "Whether the other side or the route is idempotent, such as with an
idempotent consumer or a key the other side checks"
+ ],
+ "avoid": [
+ "Against a real service where a duplicate costs, such as one that
charges or sends mail to people",
+ "With a high count: until=1 makes one send fail, which is enough to
see one duplicate"
+ ],
+ "watch": [
+ "What the other side received: two of the same",
+ "The redeliveries of the step"
+ ],
"rule": "endpoint=${endpoint} error at=reply until=${until}",
"placeholders": [
{
@@ -673,7 +735,19 @@
"id": "outage",
"group": "Downstream",
"title": "Is down for a while",
- "description": "Every send fails during the first part of each period,
as outage windows",
+ "description": "Every send fails during the first part of each period
and works for the rest, as a service that goes down and comes back",
+ "uncovers": [
+ "A circuit breaker that opens during the outage and closes after it",
+ "Redelivery that gives up before the service is back, so the
messages go to the dead letter channel",
+ "How long the route needs to catch up after an outage"
+ ],
+ "avoid": [
+ "An outage longer than all the redeliveries together sends every
message in it to the dead letter channel: make sure that is what you want to
test"
+ ],
+ "watch": [
+ "The faults per second: on during the outage, off between",
+ "The state of a circuit breaker, and the exchanges handled by the
error handler"
+ ],
"rule": "endpoint=${endpoint} error rate=burst:${outage}\/${period}
until=${until}",
"placeholders": [
{
@@ -712,7 +786,19 @@
"id": "corrupt-entry",
"group": "Messages",
"title": "Arrives corrupt",
- "description": "The messages that enter the route are changed: random
headers, the wrong charset, cut in half or empty",
+ "description": "The messages that enter the route are changed before
its first step: random headers, the wrong charset, cut in half or empty",
+ "uncovers": [
+ "Validation that is missing, so a bad message gets deep into the
route before it fails",
+ "A poison message that is redelivered again and again, instead of
going to the dead letter channel",
+ "Unmarshal and parse errors that are not handled"
+ ],
+ "avoid": [
+ "When the body is a stream or a file: the charset and the cut change
text and bytes only, so those messages are skipped (see the reasons it skipped)"
+ ],
+ "watch": [
+ "The errors of the steps that read the body",
+ "The messages the rule skipped, and why"
+ ],
"rule": "route=${route} corrupt mode=${mode} rate=${rate}
until=${until}",
"placeholders": [
{
@@ -761,7 +847,17 @@
"id": "empty-body",
"group": "Messages",
"title": "Is sent with an empty body",
- "description": "The message sent to the endpoint has an empty body",
+ "description": "The message sent to the endpoint has an empty body, as
a sender with nothing to send",
+ "uncovers": [
+ "A service, or the next route, that does not check for an empty
body",
+ "An empty file or message that is accepted and passed on"
+ ],
+ "avoid": [
+ "To test the route itself with bad input: use Arrives corrupt on the
route"
+ ],
+ "watch": [
+ "The route or the service that receives it"
+ ],
"rule": "endpoint=${endpoint} corrupt mode=empty rate=${rate}
until=${until}",
"placeholders": [
{
@@ -793,7 +889,17 @@
"id": "missing-header",
"group": "Messages",
"title": "Is sent without a header",
- "description": "A header is missing from the message sent to the
endpoint",
+ "description": "A header is removed from the message sent to the
endpoint, such as a correlation id, a trace id or a content type",
+ "uncovers": [
+ "A receiver that needs the header and fails without it, or worse,
goes on and does the wrong thing",
+ "Tracing that breaks, as the trace id does not reach the next
service"
+ ],
+ "avoid": [
+ "With a header the endpoint does not send on, such as most Camel
headers: nothing changes"
+ ],
+ "watch": [
+ "The route or the service that receives it"
+ ],
"rule": "endpoint=${endpoint} corrupt mode=remove-header
header=${header} rate=${rate} until=${until}",
"placeholders": [
{
@@ -834,7 +940,18 @@
"id": "poll-error",
"group": "Consumers and routes",
"title": "Polls fail",
- "description": "The polls of a scheduled poll consumer fail, as when
the source is not reachable",
+ "description": "The polls of a scheduled poll consumer fail, as when
the folder, the server or the database is not reachable. No exchange is made:
the consumer polls again on its schedule",
+ "uncovers": [
+ "A consumer without a backoff, which fails every poll and fills the
log",
+ "A health check that does not show a consumer that cannot poll"
+ ],
+ "avoid": [
+ "On a consumer that does not poll on a schedule, such as a timer,
Kafka or JMS: the rule fires 0 times"
+ ],
+ "watch": [
+ "The errors and the backoff of the consumer",
+ "The log and the health checks"
+ ],
"rule": "endpoint=${endpoint} error at=poll rate=${rate}
until=${until}",
"placeholders": [
{
@@ -866,7 +983,18 @@
"id": "poll-skip",
"group": "Consumers and routes",
"title": "Polls are skipped",
- "description": "The polls of a scheduled poll consumer are skipped, as
a consumer that falls behind",
+ "description": "The polls of a scheduled poll consumer are skipped, as
a consumer that falls behind: the messages wait at the source",
+ "uncovers": [
+ "Messages that pile up at the source",
+ "Alerts on a consumer that goes quiet",
+ "Batching or ordering that counts on every poll"
+ ],
+ "avoid": [
+ "On a consumer that does not poll on a schedule, such as a timer,
Kafka or JMS: the rule fires 0 times"
+ ],
+ "watch": [
+ "The polls of the consumer, and the routes after it that get nothing"
+ ],
"rule": "endpoint=${endpoint} drop at=poll rate=${rate}
until=${until}",
"placeholders": [
{
@@ -898,7 +1026,19 @@
"id": "stall",
"group": "Consumers and routes",
"title": "The route stalls",
- "description": "The route is suspended for a while, and its messages
wait",
+ "description": "The route is suspended for the time, then resumed: its
consumer takes no messages, and they wait in a queue or at the source",
+ "uncovers": [
+ "Senders that block or fail when the route does not take messages,
such as direct callers or a full seda queue",
+ "Health checks and alerts for a route that stopped",
+ "How long the route needs to catch up"
+ ],
+ "avoid": [
+ "On a route others call directly and wait for: they fail while it
stalls, which may or may not be the test"
+ ],
+ "watch": [
+ "The state of the route",
+ "The callers, and the queue in front of the route"
+ ],
"rule": "route=${route} stall duration=${duration}",
"placeholders": [
{
@@ -923,7 +1063,19 @@
"id": "flap",
"group": "Consumers and routes",
"title": "The route flaps",
- "description": "The route stops and starts again several times",
+ "description": "The route stops and starts again, several times, as a
consumer that keeps losing its connection",
+ "uncovers": [
+ "Messages lost or delivered twice across a restart",
+ "Connections or other resources that leak on each start",
+ "Health checks and alerts that flap with it"
+ ],
+ "avoid": [
+ "On a route whose consumer takes long to start: make the time
between longer than the start"
+ ],
+ "watch": [
+ "The state and the uptime of the route",
+ "The effect: completed against failed or lost messages"
+ ],
"rule": "route=${route} lifecycle action=stop duration=${duration}
repeat=${repeat} every=${every}",
"placeholders": [
{
@@ -962,7 +1114,18 @@
"id": "removed",
"group": "Consumers and routes",
"title": "The route is removed",
- "description": "The route is removed, and added back after a while",
+ "description": "The route is removed from the integration and added
back after the time, as when one route is reloaded or redeployed",
+ "uncovers": [
+ "Routes that call it and fail while it is gone",
+ "State kept in memory that is lost with it, such as an aggregator or
an idempotent repository"
+ ],
+ "avoid": [
+ "On a route whose state in memory you want to keep"
+ ],
+ "watch": [
+ "The callers of the route",
+ "Its counters, which start again from zero"
+ ],
"rule": "route=${route} lifecycle action=remove duration=${duration}",
"placeholders": [
{
@@ -987,7 +1150,17 @@
"id": "dlq-unreachable",
"group": "Error handling",
"title": "The dead letter channel is unreachable",
- "description": "The send of the dead letter channel fails, so the
messages it handles are lost (add it before the rule that makes them fail)",
+ "description": "The send of the dead letter channel fails, so the
messages it should keep are lost. Add it before the rule that makes the
messages fail",
+ "uncovers": [
+ "That the dead letter channel is a single point of failure",
+ "Whether lost messages are noticed: the report counts them as lost,
and the verdict fails"
+ ],
+ "avoid": [
+ "On its own: without another rule, or a real error, that sends
messages to the dead letter channel, nothing happens"
+ ],
+ "watch": [
+ "The messages lost, and the verdict"
+ ],
"rule": "endpoint=${endpoint} error at=dlq",
"placeholders": [
{
@@ -1005,7 +1178,19 @@
"id": "step-error",
"group": "Error handling",
"title": "A step fails",
- "description": "A step of a route fails, as when its processor throws",
+ "description": "A step of a route fails, as when its processor throws
because of a bug or bad data",
+ "uncovers": [
+ "onException clauses that do not match the exception",
+ "Redelivery of a step that will fail again: a bug is not a temporary
error",
+ "Work done by the steps before it that is not undone"
+ ],
+ "avoid": [
+ "To test a call to another system: put the fault on its endpoint,
with Returns an error or Refuses the connection"
+ ],
+ "watch": [
+ "The redeliveries of the step and the exchanges handled by the error
handler",
+ "The errors, with the step that failed"
+ ],
"rule": "step=${step} error rate=${rate} until=${until}",
"placeholders": [
{
@@ -1037,7 +1222,17 @@
"id": "step-slow",
"group": "Error handling",
"title": "A step is slow",
- "description": "A step of a route waits before its processor is
called",
+ "description": "A step of a route waits before its processor is
called, as a slow lookup or calculation",
+ "uncovers": [
+ "The step that the time of the route comes from",
+ "Callers of the route that wait for it, and time out"
+ ],
+ "avoid": [
+ "In a route started by a timer: the timer waits for the exchange, so
there are fewer messages, not more load"
+ ],
+ "watch": [
+ "The times of the steps and of the route"
+ ],
"rule": "step=${step} delay duration=${duration} rate=${rate}
until=${until}",
"placeholders": [
{
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc
b/components/camel-chaos/src/main/docs/chaos.adoc
index bfb51bdfaba6..35485f8e0688 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -625,7 +625,8 @@ Camel catalog (`CamelCatalog.chaosModelJSon()`). It has the
targets, the hooks o
offers (and whether it is reached only with `at=`), the parameters of each
fault with their types and limits, the
options, the kinds and ops of an expectation, the limits, and recipes: known
rules in plain words, grouped by what goes
wrong, each a rule with placeholders (`+${endpoint}+`, `+${rate}+`) and what
to pick them from (the endpoints the routes
-send to, the routes, the steps), such as:
+send to, the routes, the steps). To guide the one who picks a recipe, each
says what it does, what it can uncover, when
+not to use it, and what to watch while it runs, such as:
[source,json]
----
@@ -633,6 +634,19 @@ send to, the routes, the steps), such as:
"id": "failed-after-success",
"group": "Downstream",
"title": "Did the work, then failed",
+ "description": "The send is made and succeeds, then the exchange fails, as
when the reply is lost. The error handler sends it again, so the other side
gets it twice",
+ "uncovers": [
+ "Duplicates: an order booked twice, a payment taken twice",
+ "Whether the other side or the route is idempotent, such as with an
idempotent consumer or a key the other side checks"
+ ],
+ "avoid": [
+ "Against a real service where a duplicate costs, such as one that charges
or sends mail to people",
+ "With a high count: until=1 makes one send fail, which is enough to see
one duplicate"
+ ],
+ "watch": [
+ "What the other side received: two of the same",
+ "The redeliveries of the step"
+ ],
"rule": "endpoint=${endpoint} error at=reply until=${until}"
}
----
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 a8c0f2ede91e..0da00fcf3912 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
@@ -373,39 +373,44 @@ final class ChaosModel {
// downstream
answer.add(new Recipe(
"http-error", "Downstream", "Returns an error (HTTP status)",
- "An HTTP call fails with a status, as for a service that is
down or overloaded: tests onException,"
-
+ " redelivery and the dead letter channel",
+ "An HTTP call fails with a status, as a service that is down
(503), overloaded "
+
+ "(429) or broken (500) answers. The exchange fails with the exception of the
HTTP "
+
+ "component, as the real call would",
"endpoint=${endpoint} error status=${status} rate=${rate}
until=${until}",
List.of(httpEndpoint(), new Placeholder("status", "integer",
"The HTTP status", "503", "503", null),
rate("0.25"), until("10m"))));
answer.add(new Recipe(
"connection-refused", "Downstream", "Refuses the connection",
- "The send fails as when the other side is not there",
+ "The send fails with a ConnectException before anything is
sent, as when the "
+
+ "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(
"slow", "Downstream", "Is slow",
- "Each send waits a different time within a range, as a slow
service does",
+ "Each send waits a time within the range before it is made, as
a slow service. "
+ + "The exchange then goes on
as usual: the cost is time, not errors",
"endpoint=${endpoint} delay duration=${duration} rate=${rate}
until=${until}",
List.of(producer(), new Placeholder(
"duration", "duration", "The time, or a range of
times", "200ms..3s",
"200ms..3s", null), rate("0.5"), until("10m"))));
answer.add(new Recipe(
"timeout", "Downstream", "Times out",
- "The send waits, then fails without calling the producer, as a
call that times out",
+ "The send waits, then fails with a SocketTimeoutException
without calling the "
+ + "producer, as a call
that times out",
"endpoint=${endpoint} error after=${after}
exception=java.net.SocketTimeoutException rate=${rate}"
-
+ " until=${until}",
+
+ " until=${until}",
List.of(producer(), new Placeholder("after", "duration", "How
long before it times out", "5s", "5s", null),
rate("0.1"), until("10m"))));
answer.add(new Recipe(
"failed-after-success", "Downstream", "Did the work, then
failed",
- "The send succeeds and then fails, as a reply that is lost: a
redelivery sends it again, so this tests"
-
+ " that a duplicate does no harm",
+ "The send is made and succeeds, then the exchange fails, as
when the reply is "
+
+ "lost. The error handler sends it again, so the other side gets it twice",
"endpoint=${endpoint} error at=reply until=${until}",
List.of(producer(), new Placeholder("until",
"countOrDuration", "How many sends fail", "1", "1", null))));
answer.add(new Recipe(
"outage", "Downstream", "Is down for a while",
- "Every send fails during the first part of each period, as
outage windows",
+ "Every send fails during the first part of each period and
works for the rest, as "
+ + "a service
that goes down and comes back",
"endpoint=${endpoint} error rate=burst:${outage}/${period}
until=${until}",
List.of(producer(), new Placeholder("outage", "duration", "How
long each outage lasts", "10s", "10s", null),
new Placeholder("period", "duration", "The time from
one outage to the next", "1m", "1m", null),
@@ -413,8 +418,8 @@ final class ChaosModel {
// messages
answer.add(new Recipe(
"corrupt-entry", "Messages", "Arrives corrupt",
- "The messages that enter the route are changed: random
headers, the wrong charset, cut in half or"
- + " empty",
+ "The messages that enter the route are changed before its
first step: random "
+ + "headers,
the wrong charset, cut in half or empty",
"route=${route} corrupt mode=${mode} rate=${rate}
until=${until}",
List.of(route(), new Placeholder(
"mode", "enum", "How the message is changed",
"headers", "headers",
@@ -422,12 +427,14 @@ final class ChaosModel {
rate("0.25"), until("10m"))));
answer.add(new Recipe(
"empty-body", "Messages", "Is sent with an empty body",
- "The message sent to the endpoint has an empty body",
+ "The message sent to the endpoint has an empty body, as a
sender with nothing to "
+ +
"send",
"endpoint=${endpoint} corrupt mode=empty rate=${rate}
until=${until}",
List.of(producer(), rate("0.25"), until("10m"))));
answer.add(new Recipe(
"missing-header", "Messages", "Is sent without a header",
- "A header is missing from the message sent to the endpoint",
+ "A header is removed from the message sent to the endpoint,
such as a correlation "
+ +
"id, a trace id or a content type",
"endpoint=${endpoint} corrupt mode=remove-header
header=${header} rate=${rate} until=${until}",
List.of(producer(), new Placeholder(
"header", "string", "The header, or a pattern with *",
"X-Trace-Id",
@@ -435,60 +442,275 @@ final class ChaosModel {
// consumers and routes
answer.add(new Recipe(
"poll-error", "Consumers and routes", "Polls fail",
- "The polls of a scheduled poll consumer fail, as when the
source is not reachable",
+ "The polls of a scheduled poll consumer fail, as when the
folder, the server or "
+ + "the
database is not reachable. No exchange is made: the consumer polls again on "
+ + "its
schedule",
"endpoint=${endpoint} error at=poll rate=${rate}
until=${until}",
List.of(polled(), rate("0.5"), until("10m"))));
answer.add(new Recipe(
"poll-skip", "Consumers and routes", "Polls are skipped",
- "The polls of a scheduled poll consumer are skipped, as a
consumer that falls behind",
+ "The polls of a scheduled poll consumer are skipped, as a
consumer that falls "
+ +
"behind: the messages wait at the source",
"endpoint=${endpoint} drop at=poll rate=${rate}
until=${until}",
List.of(polled(), rate("0.5"), until("10m"))));
answer.add(new Recipe(
"stall", "Consumers and routes", "The route stalls",
- "The route is suspended for a while, and its messages wait",
+ "The route is suspended for the time, then resumed: its
consumer takes no "
+ +
"messages, and they wait in a queue or at the source",
"route=${route} stall duration=${duration}",
List.of(route(), new Placeholder("duration", "duration", "How
long it stalls", "20s", "20s", null))));
answer.add(new Recipe(
"flap", "Consumers and routes", "The route flaps",
- "The route stops and starts again several times",
+ "The route stops and starts again, several times, as a
consumer that keeps losing "
+ + "its
connection",
"route=${route} lifecycle action=stop duration=${duration}
repeat=${repeat} every=${every}",
List.of(route(), new Placeholder("duration", "duration", "How
long it is down each time", "10s", "10s", null),
new Placeholder("repeat", "integer", "How many times",
"3", "3", null),
new Placeholder("every", "duration", "The time from
one time to the next", "1m", "1m", null))));
answer.add(new Recipe(
"removed", "Consumers and routes", "The route is removed",
- "The route is removed, and added back after a while",
+ "The route is removed from the integration and added back
after the time, as when "
+ +
"one route is reloaded or redeployed",
"route=${route} lifecycle action=remove duration=${duration}",
List.of(route(), new Placeholder("duration", "duration", "How
long it is gone", "30s", "30s", null))));
// error handling
answer.add(new Recipe(
"dlq-unreachable", "Error handling", "The dead letter channel
is unreachable",
- "The send of the dead letter channel fails, so the messages it
handles are lost (add it before the rule"
-
+ " that makes them fail)",
+ "The send of the dead letter channel fails, so the messages it
should keep are "
+
+ "lost. Add it before the rule that makes the messages fail",
"endpoint=${endpoint} error at=dlq",
List.of(producer())));
answer.add(new Recipe(
"step-error", "Error handling", "A step fails",
- "A step of a route fails, as when its processor throws",
+ "A step of a route fails, as when its processor throws because
of a bug or bad "
+ + "data",
"step=${step} error rate=${rate} until=${until}",
List.of(step(), rate("0.1"), until("10m"))));
answer.add(new Recipe(
"step-slow", "Error handling", "A step is slow",
- "A step of a route waits before its processor is called",
+ "A step of a route waits before its processor is called, as a
slow lookup or "
+ +
"calculation",
"step=${step} delay duration=${duration} rate=${rate}
until=${until}",
List.of(step(), new Placeholder("duration", "duration", "The
time, or a range of times", "1s", "1s", null),
rate("0.25"), until("10m"))));
return answer;
}
+ /**
+ * What a recipe helps to find, when not to use it, and what to look at
while it runs: to guide a person, or an AI,
+ * that picks a recipe (CAMEL-26699).
+ */
+ record Guide(List<String> uncovers, List<String> avoid, List<String>
watch) {
+ }
+
+ static Map<String, Guide> guides() {
+ Map<String, Guide> answer = new LinkedHashMap<>();
+ answer.put("http-error", new Guide(
+ List.of(
+ "An error handler that does not retry, or retries a
status that will not change, "
+ + "such as 400 or 404",
+ "Redeliveries without a delay, which hit an overloaded
service again at once",
+ "Messages that are lost instead of reaching the dead
letter channel"),
+ List.of(
+ "The endpoint is not HTTP: use Refuses the connection",
+ "To test a timeout: a status comes back at once, use
Times out"),
+ List.of(
+ "The redeliveries of the step and the exchanges
handled by the error handler",
+ "The dead letter channel, and a circuit breaker around
the call if there is one")));
+ answer.put("connection-refused", new Guide(
+ List.of(
+ "Whether a connection error is retried: that is safe,
as nothing reached the "
+ + "other side",
+ "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"),
+ List.of(
+ "The redeliveries and the exchanges handled by the
error handler",
+ "The state of a circuit breaker around the call")));
+ answer.put("slow", new Guide(
+ List.of(
+ "A consumer that falls behind: the throughput of the
route drops and messages "
+ + "wait in front of it",
+ "Thread pools that fill up with waiting exchanges",
+ "Callers of the route (request/reply) that time out"),
+ List.of(
+ "To test the timeout of the client: the wait is before
the call, so the client's "
+ + "timeout does not fire. Use Times out",
+ "On a route started by a timer: the timer waits for
the exchange, so there are "
+ + "fewer
messages, not more load"),
+ List.of(
+ "The mean, p95 and p99 time of the routes, and their
throughput",
+ "The exchanges in flight")));
+ answer.put("timeout", new Guide(
+ List.of(
+ "Whether a timeout is retried, and whether that is
safe: in real life the other "
+ + "side may have done the work",
+ "How long a message takes when every attempt times
out: the wait times the "
+ + "attempts",
+ "A consumer that stops taking messages while its
threads wait"),
+ List.of(
+ "A long wait with a high rate: each fault holds a
thread for that time"),
+ List.of(
+ "The time of the route and the exchanges in flight",
+ "The redeliveries and the dead letter channel")));
+ answer.put("failed-after-success", new Guide(
+ List.of(
+ "Duplicates: an order booked twice, a payment taken
twice",
+ "Whether the other side or the route is idempotent,
such as with an idempotent "
+
+ "consumer or a key the other side checks"),
+ List.of(
+ "Against a real service where a duplicate costs, such
as one that charges or "
+ + "sends mail to people",
+ "With a high count: until=1 makes one send fail, which
is enough to see one "
+ + "duplicate"),
+ List.of(
+ "What the other side received: two of the same",
+ "The redeliveries of the step")));
+ answer.put("outage", new Guide(
+ List.of(
+ "A circuit breaker that opens during the outage and
closes after it",
+ "Redelivery that gives up before the service is back,
so the messages go to the "
+
+ "dead letter channel",
+ "How long the route needs to catch up after an
outage"),
+ List.of(
+ "An outage longer than all the redeliveries together
sends every message in it to "
+ + "the dead letter channel: make sure that is what you
want to test"),
+ List.of(
+ "The faults per second: on during the outage, off
between",
+ "The state of a circuit breaker, and the exchanges
handled by the error handler")));
+ answer.put("corrupt-entry", new Guide(
+ List.of(
+ "Validation that is missing, so a bad message gets
deep into the route before it "
+ + "fails",
+ "A poison message that is redelivered again and again,
instead of going to the "
+ + "dead letter channel",
+ "Unmarshal and parse errors that are not handled"),
+ List.of(
+ "When the body is a stream or a file: the charset and
the cut change text and "
+ + "bytes only, so those messages are skipped (see the
reasons it skipped)"),
+ List.of(
+ "The errors of the steps that read the body",
+ "The messages the rule skipped, and why")));
+ answer.put("empty-body", new Guide(
+ List.of(
+ "A service, or the next route, that does not check for
an empty body",
+ "An empty file or message that is accepted and passed
on"),
+ List.of(
+ "To test the route itself with bad input: use Arrives
corrupt on the route"),
+ List.of(
+ "The route or the service that receives it")));
+ answer.put("missing-header", new Guide(
+ List.of(
+ "A receiver that needs the header and fails without
it, or worse, goes on and "
+ + "does the wrong thing",
+ "Tracing that breaks, as the trace id does not reach
the next service"),
+ List.of(
+ "With a header the endpoint does not send on, such as
most Camel headers: nothing "
+ + "changes"),
+ List.of(
+ "The route or the service that receives it")));
+ answer.put("poll-error", new Guide(
+ List.of(
+ "A consumer without a backoff, which fails every poll
and fills the log",
+ "A health check that does not show a consumer that
cannot poll"),
+ List.of(
+ "On a consumer that does not poll on a schedule, such
as a timer, Kafka or JMS: "
+ + "the rule fires 0 times"),
+ List.of(
+ "The errors and the backoff of the consumer",
+ "The log and the health checks")));
+ answer.put("poll-skip", new Guide(
+ List.of(
+ "Messages that pile up at the source",
+ "Alerts on a consumer that goes quiet",
+ "Batching or ordering that counts on every poll"),
+ List.of(
+ "On a consumer that does not poll on a schedule, such
as a timer, Kafka or JMS: "
+ + "the rule fires 0 times"),
+ List.of(
+ "The polls of the consumer, and the routes after it
that get nothing")));
+ answer.put("stall", new Guide(
+ List.of(
+ "Senders that block or fail when the route does not
take messages, such as direct "
+ + "callers or a full seda queue",
+ "Health checks and alerts for a route that stopped",
+ "How long the route needs to catch up"),
+ List.of(
+ "On a route others call directly and wait for: they
fail while it stalls, which "
+ + "may or may not be the test"),
+ List.of(
+ "The state of the route",
+ "The callers, and the queue in front of the route")));
+ answer.put("flap", new Guide(
+ List.of(
+ "Messages lost or delivered twice across a restart",
+ "Connections or other resources that leak on each
start",
+ "Health checks and alerts that flap with it"),
+ List.of(
+ "On a route whose consumer takes long to start: make
the time between longer than "
+ + "the start"),
+ List.of(
+ "The state and the uptime of the route",
+ "The effect: completed against failed or lost
messages")));
+ answer.put("removed", new Guide(
+ List.of(
+ "Routes that call it and fail while it is gone",
+ "State kept in memory that is lost with it, such as an
aggregator or an "
+ +
"idempotent repository"),
+ List.of(
+ "On a route whose state in memory you want to keep"),
+ List.of(
+ "The callers of the route",
+ "Its counters, which start again from zero")));
+ answer.put("dlq-unreachable", new Guide(
+ List.of(
+ "That the dead letter channel is a single point of
failure",
+ "Whether lost messages are noticed: the report counts
them as lost, and the "
+
+ "verdict fails"),
+ List.of(
+ "On its own: without another rule, or a real error,
that sends messages to the "
+ + "dead letter channel, nothing happens"),
+ List.of(
+ "The messages lost, and the verdict")));
+ answer.put("step-error", new Guide(
+ List.of(
+ "onException clauses that do not match the exception",
+ "Redelivery of a step that will fail again: a bug is
not a temporary error",
+ "Work done by the steps before it that is not undone"),
+ List.of(
+ "To test a call to another system: put the fault on
its endpoint, with Returns an "
+ + "error or Refuses the connection"),
+ List.of(
+ "The redeliveries of the step and the exchanges
handled by the error handler",
+ "The errors, with the step that failed")));
+ answer.put("step-slow", new Guide(
+ List.of(
+ "The step that the time of the route comes from",
+ "Callers of the route that wait for it, and time out"),
+ List.of(
+ "In a route started by a timer: the timer waits for
the exchange, so there are "
+ + "fewer messages, not more load"),
+ List.of(
+ "The times of the steps and of the route")));
+ return answer;
+ }
+
private static JsonArray recipes() {
JsonArray answer = new JsonArray();
+ Map<String, Guide> guides = guides();
for (Recipe recipe : recipeList()) {
JsonObject jo = new JsonObject();
jo.put("id", recipe.id());
jo.put("group", recipe.group());
jo.put("title", recipe.title());
jo.put("description", recipe.description());
+ Guide guide = guides.get(recipe.id());
+ if (guide != null) {
+ jo.put("uncovers", new JsonArray(guide.uncovers()));
+ jo.put("avoid", new JsonArray(guide.avoid()));
+ jo.put("watch", new JsonArray(guide.watch()));
+ }
jo.put("rule", recipe.rule());
JsonArray placeholders = new JsonArray();
for (Placeholder p : recipe.placeholders()) {
diff --git
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosModelTest.java
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosModelTest.java
index 111d5db2d4ff..f14a97807bd3 100644
---
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosModelTest.java
+++
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosModelTest.java
@@ -126,6 +126,21 @@ class ChaosModelTest {
ChaosRuleParser.parse(fill(recipe, true), new DefaultClassResolver(),
MAX);
}
+ @Test
+ void everyRecipeSaysWhatItUncoversWhenNotToUseItAndWhatToWatch() {
+ // CAMEL-26699: the add form guides a person (or an AI) with these
+ Map<String, ChaosModel.Guide> guides = ChaosModel.guides();
+ for (Recipe recipe : ChaosModel.recipeList()) {
+ ChaosModel.Guide guide = guides.get(recipe.id());
+ assertThat(guide).as(recipe.id()).isNotNull();
+ assertThat(guide.uncovers()).as(recipe.id()).isNotEmpty();
+ assertThat(guide.avoid()).as(recipe.id()).isNotEmpty();
+ assertThat(guide.watch()).as(recipe.id()).isNotEmpty();
+ }
+ assertThat(guides.keySet()).containsExactlyInAnyOrderElementsOf(
+ ChaosModel.recipeList().stream().map(Recipe::id).toList());
+ }
+
@Test
void theModeOfACorruptRecipeOffersEveryMode() {
Recipe recipe = ChaosModel.recipeList().stream().filter(r ->
r.id().equals("corrupt-entry")).findFirst()
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeForm.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeForm.java
index 6fef3adeb031..0371afbf8252 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeForm.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeForm.java
@@ -345,8 +345,10 @@ final class ChaosRecipeForm {
new Rect(pad.x(), pad.y(), pad.width(), Math.min(topHeight,
pad.height())));
int y = pad.y() + topHeight;
if (!inForm) {
-
frame.renderWidget(Paragraph.builder().overflow(Overflow.WRAP_WORD).text(Text.from(
- Line.from(Span.styled(recipe.rule(),
Theme.muted())))).build(),
+ List<Line> lines = new ArrayList<>();
+ lines.add(Line.from(Span.styled(recipe.rule(), Theme.muted())));
+ addGuide(lines, recipe);
+
frame.renderWidget(Paragraph.builder().overflow(Overflow.WRAP_WORD).text(Text.from(lines)).build(),
new Rect(pad.x(), y, pad.width(), Math.max(1, pad.bottom()
- y)));
return;
}
@@ -380,11 +382,33 @@ final class ChaosRecipeForm {
bottom.add(missing == null
? Line.from(Span.styled("Enter to add it (asks to confirm)",
Theme.success()))
: Line.from(Span.styled(missing + " needs a value",
Theme.warning())));
+ addGuide(bottom, recipe);
int bottomY = y + 1;
frame.renderWidget(Paragraph.builder().overflow(Overflow.WRAP_WORD).text(Text.from(bottom)).build(),
new Rect(pad.x(), bottomY, pad.width(), Math.max(1,
pad.bottom() - bottomY)));
}
+ /**
+ * What the recipe can uncover, when not to use it and what to watch while
it runs, as the model of camel-chaos
+ * says: under the rule, as far as there is room.
+ */
+ static void addGuide(List<Line> lines, ChaosRecipes.Recipe recipe) {
+ addSection(lines, "What it can uncover", recipe.uncovers(),
Theme.success());
+ addSection(lines, "When not to use it", recipe.avoid(),
Theme.warning());
+ addSection(lines, "What to watch", recipe.watch(), Theme.info());
+ }
+
+ private static void addSection(List<Line> lines, String title,
List<String> items, Style style) {
+ if (items == null || items.isEmpty()) {
+ return;
+ }
+ lines.add(Line.empty());
+ lines.add(Line.from(Span.styled(title, style.bold())));
+ for (String item : items) {
+ lines.add(Line.from(Span.styled("• ", style), Span.raw(item)));
+ }
+ }
+
/** The keys of the form, for the footer. */
void renderFooter(List<Span> spans) {
if (inForm) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipes.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipes.java
index 71200da83209..1e78a9f04fb3 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipes.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipes.java
@@ -41,7 +41,12 @@ final class ChaosRecipes {
}
/** A known rule. */
- record Recipe(String id, String group, String title, String description,
String rule, List<Placeholder> placeholders) {
+ /**
+ * A known rule. Besides what it does, the model says what it can uncover,
when not to use it and what to watch
+ * while it runs (CAMEL-26699): empty with a catalog from before that.
+ */
+ record Recipe(String id, String group, String title, String description,
String rule, List<Placeholder> placeholders,
+ List<String> uncovers, List<String> avoid, List<String> watch) {
/** The rule with the values of the placeholders; a placeholder
without a value stays {@code ${name}}. */
String fill(Map<String, String> values) {
@@ -112,7 +117,8 @@ final class ChaosRecipes {
}
answer.add(new Recipe(
r.getString("id"), r.getStringOrDefault("group", ""),
r.getString("title"),
- r.getString("description"), r.getString("rule"),
placeholders));
+ r.getString("description"), r.getString("rule"),
placeholders,
+ strings(r.get("uncovers")), strings(r.get("avoid")),
strings(r.get("watch"))));
}
return answer.isEmpty() ? null : new ChaosRecipes(answer);
}
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 b812000b0429..a830e2ce3931 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
@@ -60,7 +60,9 @@ error handler (HAND) and the redeliveries (REDEL).
- **a** — Add a rule: pick what should go wrong from the known rules of
camel-chaos (such as "Downstream returns an error", "Did the work, then
- failed", "The route flaps"), then fill in its values: the endpoints,
+ failed", "The route flaps"). Each says what it does, what it can
+ uncover, when not to use it, and what to watch while it runs. Then fill
+ in its values: the endpoints,
routes and steps of the integration are picked with Left and Right, and
the rule is shown as it is built. The last entry, "Write the rule
yourself", types it as one line, such as
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeFormTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeFormTest.java
index 3df8981db10e..d085391eb61b 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeFormTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeFormTest.java
@@ -82,6 +82,21 @@ class ChaosRecipeFormTest {
.contains("endpoint=${endpoint} error status=${status}
rate=${rate}");
}
+ @Test
+ void theRecipeSaysWhatItUncoversWhenNotToUseItAndWhatToWatch() {
+ key('a');
+ assertThat(screen()).contains("What it can uncover").contains("• An
error handler that does not retry")
+ .contains("When not to use it").contains("• The endpoint is
not HTTP: use Refuses the connection")
+ .contains("What to watch").contains("• The redeliveries of the
step");
+ // and while it is filled in
+ key(KeyCode.ENTER);
+ assertThat(screen()).contains("What it can uncover").contains("Enter
to add it");
+ // a recipe without a guide shows none
+ key(KeyCode.ESCAPE);
+ key(KeyCode.DOWN);
+ assertThat(screen()).contains("The route stalls").doesNotContain("What
it can uncover");
+ }
+
@Test
void aRecipeFilledWithTheEndpointOfTheIntegrationAndItsDefaults() {
key('a');
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipesTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipesTest.java
index c27656c89d3f..879a3e2cf6e0 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipesTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipesTest.java
@@ -34,6 +34,9 @@ class ChaosRecipesTest {
{"chaos": {"recipes": [
{"id": "http-error", "group": "Downstream", "title": "Returns an
error (HTTP status)",
"description": "An HTTP call fails with a status",
+ "uncovers": ["An error handler that does not retry"],
+ "avoid": ["The endpoint is not HTTP: use Refuses the
connection"],
+ "watch": ["The redeliveries of the step"],
"rule": "endpoint=${endpoint} error status=${status}
rate=${rate}",
"placeholders": [
{"name": "endpoint", "type": "endpoint", "description": "The
HTTP endpoint the route calls",
@@ -109,6 +112,11 @@ class ChaosRecipesTest {
assertThat(http.placeholders()).extracting(ChaosRecipes.Placeholder::name).containsExactly("endpoint",
"status",
"rate");
assertThat(http.placeholders().get(0).schemes()).containsExactly("http",
"https");
+ // what it can uncover, when not to use it and what to watch
(CAMEL-26699); none from an older catalog
+ assertThat(http.uncovers()).containsExactly("An error handler that
does not retry");
+ assertThat(http.avoid()).containsExactly("The endpoint is not HTTP:
use Refuses the connection");
+ assertThat(http.watch()).containsExactly("The redeliveries of the
step");
+ assertThat(recipes.recipes().get(1).uncovers()).isEmpty();
assertThat(http.fill(Map.of("endpoint", "http://localhost:8765/stock",
"status", "503", "rate", "0.25")))
.isEqualTo("endpoint=http://localhost:8765/stock error
status=503 rate=0.25");
// a placeholder without a value stays, so the form can say what is
missing