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 07ae98c61dad CAMEL-26726: camel-chaos - expectations on the time of a
route and on the messages lost between two routes (#29396)
07ae98c61dad is described below
commit 07ae98c61dad6f1241295eba891430f78f526267
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Nov 8 09:08:42 2026 +0100
CAMEL-26726: camel-chaos - expectations on the time of a route and on the
messages lost between two routes (#29396)
New expectations: route:meanTime<=, route:maxTime<= (or >=) on the time of
a route while the rule is active, and a->b:lost<=0 on the messages route a
handed to route b that b did not complete. The report, camel get chaos, the
chaos screen and the explain question show them. The camel-chaos page has a new
section, Expectations on time and loss.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../org/apache/camel/catalog/chaos-model.json | 14 +-
.../org/apache/camel/catalog/docs/chaos.adoc | 49 +++++-
.../org/apache/camel/chaos/chaos-model.json | 14 +-
components/camel-chaos/src/main/docs/chaos.adoc | 49 +++++-
.../camel/component/chaos/ChaosExchangeState.java | 4 +
.../apache/camel/component/chaos/ChaosManager.java | 27 ++++
.../apache/camel/component/chaos/ChaosModel.java | 6 +-
.../apache/camel/component/chaos/ChaosReport.java | 29 +++-
.../component/chaos/ChaosRoutePolicyFactory.java | 15 +-
.../apache/camel/component/chaos/ChaosRule.java | 155 +++++++++++++++---
.../camel/component/chaos/ChaosRuleParser.java | 48 +++++-
.../chaos/ChaosExpectTimeAndLossTest.java | 180 +++++++++++++++++++++
.../camel/component/chaos/ChaosRuleParserTest.java | 2 +-
design/chaos.adoc | 4 +
.../dsl/jbang/core/commands/process/ListChaos.java | 22 ++-
.../dsl/jbang/core/commands/tui/ChaosAiPrompt.java | 10 +-
.../dsl/jbang/core/commands/tui/ChaosInfo.java | 3 +
.../dsl/jbang/core/commands/tui/ChaosTab.java | 10 +-
.../jbang/core/commands/tui/ChaosAiPromptTest.java | 11 ++
19 files changed, 606 insertions(+), 46 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 214f24a833aa..1e7ad670ecd5 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
@@ -448,7 +448,7 @@
"name": "expect",
"type": "expectation",
"required": false,
- "description": "An expectation of a route after the rule is added:
<route>:<count> (at least that many completed) or <route>:<kind><op><count>",
+ "description": "An expectation of a route after the rule is added:
<route>:<count> (at least that many completed), <route>:<kind><op><count>, a
time (<route>:meanTime<=2s) or the messages lost between two routes
(<route>-><route>:lost<=0)",
"example": "orders:handled<=0"
},
{
@@ -519,6 +519,18 @@
{
"name": "redeliveries",
"description": "The redeliveries in the route"
+ },
+ {
+ "name": "meanTime",
+ "description": "The mean time of the exchanges in the route, from
entering it until done, such as meanTime<=2s"
+ },
+ {
+ "name": "maxTime",
+ "description": "The longest time of an exchange in the route, from
entering it until done, such as maxTime<=5s"
+ },
+ {
+ "name": "lost",
+ "description": "The messages completed in the route and not in the
next one, such as orders->shipping:lost<=0"
}
],
"ops": [
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 c9d0d3684c5a..743afbc5e3a6 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
@@ -285,7 +285,10 @@ until: a number of matches (until=100) or a time
(until=2m), counted from the
time: a number with a unit (ms, s, m, h, d), also combined as 1m30s; a
number alone is milliseconds
expect: expect=<route id>:<count> (at least that many completed exchanges), or
expect=<route id>:<kind><op><count> with kind completed | handled |
failed | redeliveries
- and op >= | <= | = (such as expect=orders:handled<=0); with an
optional within=<time>
+ and op >= | <= | = (such as expect=orders:handled<=0), or a time
+ expect=<route id>:meanTime|maxTime<op><time> (such as
expect=orders:meanTime<=2s), or the
+ messages lost between two routes expect=<route id>-><route
id>:lost<op><count> (such as
+ expect=orders->shipping:lost<=0); with an optional within=<time>
----
The patterns of the targets are matched as text, with `*` for any characters;
they are not regular expressions. The
@@ -731,6 +734,50 @@ give their reason only.
In JSON an expectation has its `kind`, `op`, `count` and the `value` counted
so far (and `completed`, the same value, for
an expectation of completed exchanges).
+=== Expectations on time and loss
+
+The delay faults slow a route down, and the question is whether the route
stays fast enough: an expectation on time
+checks it. The question people ask about a lost message is often "did every
order reach shipping", and an expectation on
+two routes checks that.
+
+[cols="2,3"]
+|===
+|Expectation |Is met when
+
+|`orders:meanTime<=2s` |The mean time of the exchanges in route `orders` is at
most 2 s when the time is up.
+|`orders:maxTime<=5s` |No exchange of route `orders` took longer than 5 s. One
that does makes it unmet at once.
+|`orders->shipping:lost<=0` (or `lost=0`) |Every message that completed in
route `orders` also completed in route
+`shipping`: the count of `shipping` is at most 0 behind the count of `orders`
when the time is up.
+|===
+
+* The time of an exchange in a route is measured by chaos, from when the
exchange enters the route until the route is
+done with it the first time (whether it completed, was handled by the error
handler or failed), including the
+redeliveries in between. It is read through `TimeSource`, so in a
deterministic simulation test it is virtual time. A
+time is any time of the rules, such as `500ms`, `2s` or `1m30s`; the op is
`<=` or `>=` (a `meanTime>=1s` checks that a
+delay rule slows the route as it should).
+* A time of no exchange says nothing, so an expectation on time whose route
did no exchange in the time is unmet.
+* A loss counts the exchanges completed in both routes from when the rule is
added until the time is up, and the
+difference is the messages lost (never below 0, as the second route may also
get messages from elsewhere). Messages still
+on their way when the time is up count as lost too: let `within=` run past the
`until=` of the faults, so the last ones
+can arrive. The messages it counts as lost are added to the lost of the
report. The op is `<=` or `=`.
+
+For example, a stock service that is slow for a minute, and the orders that
must still ship, fast enough:
+
+[source,properties]
+----
+camel.chaos.rules = \
+ endpoint=http:stock* delay duration=1s..3s rate=0.3 until=1m
expect=orders:meanTime<=2s within=2m;\
+ endpoint=seda:shipping drop rate=0.01 until=1m
expect=orders->shipping:lost<=0 within=2m
+----
+
+An unmet one gives its reason in the report, such as `route orders: mean
2s340ms of 118 exchanges within 2m0s, at most
+2s expected (rule r1)` or `routes orders->shipping: 1 lost (118 completed in
orders, 117 in shipping) within 2m0s, at
+most 0 expected (rule r2): 1 lost`. In JSON these expectations have their
`spec` as the rule writes it, the `count` and
+the `value` in milliseconds for a time, the `toRoute` of a loss, and the
`samples` (the exchanges of the time, or the
+ones completed in the first route).
+
+The route statistics of Camel have no percentiles, so there is no `p99` yet.
+
=== A rule in plain words
Each rule of the list and the report has its `description`: what it does, to
which share of what, with the
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 214f24a833aa..1e7ad670ecd5 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
@@ -448,7 +448,7 @@
"name": "expect",
"type": "expectation",
"required": false,
- "description": "An expectation of a route after the rule is added:
<route>:<count> (at least that many completed) or <route>:<kind><op><count>",
+ "description": "An expectation of a route after the rule is added:
<route>:<count> (at least that many completed), <route>:<kind><op><count>, a
time (<route>:meanTime<=2s) or the messages lost between two routes
(<route>-><route>:lost<=0)",
"example": "orders:handled<=0"
},
{
@@ -519,6 +519,18 @@
{
"name": "redeliveries",
"description": "The redeliveries in the route"
+ },
+ {
+ "name": "meanTime",
+ "description": "The mean time of the exchanges in the route, from
entering it until done, such as meanTime<=2s"
+ },
+ {
+ "name": "maxTime",
+ "description": "The longest time of an exchange in the route, from
entering it until done, such as maxTime<=5s"
+ },
+ {
+ "name": "lost",
+ "description": "The messages completed in the route and not in the
next one, such as orders->shipping:lost<=0"
}
],
"ops": [
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc
b/components/camel-chaos/src/main/docs/chaos.adoc
index c9d0d3684c5a..743afbc5e3a6 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -285,7 +285,10 @@ until: a number of matches (until=100) or a time
(until=2m), counted from the
time: a number with a unit (ms, s, m, h, d), also combined as 1m30s; a
number alone is milliseconds
expect: expect=<route id>:<count> (at least that many completed exchanges), or
expect=<route id>:<kind><op><count> with kind completed | handled |
failed | redeliveries
- and op >= | <= | = (such as expect=orders:handled<=0); with an
optional within=<time>
+ and op >= | <= | = (such as expect=orders:handled<=0), or a time
+ expect=<route id>:meanTime|maxTime<op><time> (such as
expect=orders:meanTime<=2s), or the
+ messages lost between two routes expect=<route id>-><route
id>:lost<op><count> (such as
+ expect=orders->shipping:lost<=0); with an optional within=<time>
----
The patterns of the targets are matched as text, with `*` for any characters;
they are not regular expressions. The
@@ -731,6 +734,50 @@ give their reason only.
In JSON an expectation has its `kind`, `op`, `count` and the `value` counted
so far (and `completed`, the same value, for
an expectation of completed exchanges).
+=== Expectations on time and loss
+
+The delay faults slow a route down, and the question is whether the route
stays fast enough: an expectation on time
+checks it. The question people ask about a lost message is often "did every
order reach shipping", and an expectation on
+two routes checks that.
+
+[cols="2,3"]
+|===
+|Expectation |Is met when
+
+|`orders:meanTime<=2s` |The mean time of the exchanges in route `orders` is at
most 2 s when the time is up.
+|`orders:maxTime<=5s` |No exchange of route `orders` took longer than 5 s. One
that does makes it unmet at once.
+|`orders->shipping:lost<=0` (or `lost=0`) |Every message that completed in
route `orders` also completed in route
+`shipping`: the count of `shipping` is at most 0 behind the count of `orders`
when the time is up.
+|===
+
+* The time of an exchange in a route is measured by chaos, from when the
exchange enters the route until the route is
+done with it the first time (whether it completed, was handled by the error
handler or failed), including the
+redeliveries in between. It is read through `TimeSource`, so in a
deterministic simulation test it is virtual time. A
+time is any time of the rules, such as `500ms`, `2s` or `1m30s`; the op is
`<=` or `>=` (a `meanTime>=1s` checks that a
+delay rule slows the route as it should).
+* A time of no exchange says nothing, so an expectation on time whose route
did no exchange in the time is unmet.
+* A loss counts the exchanges completed in both routes from when the rule is
added until the time is up, and the
+difference is the messages lost (never below 0, as the second route may also
get messages from elsewhere). Messages still
+on their way when the time is up count as lost too: let `within=` run past the
`until=` of the faults, so the last ones
+can arrive. The messages it counts as lost are added to the lost of the
report. The op is `<=` or `=`.
+
+For example, a stock service that is slow for a minute, and the orders that
must still ship, fast enough:
+
+[source,properties]
+----
+camel.chaos.rules = \
+ endpoint=http:stock* delay duration=1s..3s rate=0.3 until=1m
expect=orders:meanTime<=2s within=2m;\
+ endpoint=seda:shipping drop rate=0.01 until=1m
expect=orders->shipping:lost<=0 within=2m
+----
+
+An unmet one gives its reason in the report, such as `route orders: mean
2s340ms of 118 exchanges within 2m0s, at most
+2s expected (rule r1)` or `routes orders->shipping: 1 lost (118 completed in
orders, 117 in shipping) within 2m0s, at
+most 0 expected (rule r2): 1 lost`. In JSON these expectations have their
`spec` as the rule writes it, the `count` and
+the `value` in milliseconds for a time, the `toRoute` of a loss, and the
`samples` (the exchanges of the time, or the
+ones completed in the first route).
+
+The route statistics of Camel have no percentiles, so there is no `p99` yet.
+
=== A rule in plain words
Each rule of the list and the report has its `description`: what it does, to
which share of what, with the
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExchangeState.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExchangeState.java
index 1dff5138ff72..ed6dc216308f 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExchangeState.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosExchangeState.java
@@ -70,6 +70,10 @@ final class ChaosExchangeState implements SafeCopyProperty {
boolean expectedCompleted;
boolean expectedHandled;
boolean expectedFailed;
+ // when the exchange entered the route, for the expectations on time
(meanTime, maxTime), else -1; and whether
+ // they counted its time
+ long enteredAt = -1;
+ boolean expectedTime;
}
private final Map<String, RouteState> routes = new HashMap<>(4);
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
index ecfcb2c4cabf..44834b350280 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
@@ -29,6 +29,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
+import java.util.stream.Collectors;
import org.apache.camel.CamelContext;
import org.apache.camel.Consumer;
@@ -416,6 +417,10 @@ public final class ChaosManager {
if (rule.expectation != null &&
camelContext.getRoute(rule.expectation.route) == null) {
answer.add("expects route " + rule.expectation.route + ", but
there is no route with that id (yet)");
}
+ if (rule.expectation != null && rule.expectation.toRoute != null
+ && camelContext.getRoute(rule.expectation.toRoute) == null) {
+ answer.add("expects route " + rule.expectation.toRoute + ", but
there is no route with that id (yet)");
+ }
String target = ChaosTargets.check(camelContext, rule);
if (target != null) {
answer.add(target);
@@ -671,6 +676,17 @@ public final class ChaosManager {
state.expectedFailed = true;
record(routeId, ChaosRule.Expectation.Kind.FAILED);
}
+ if (state.enteredAt >= 0 && !state.expectedTime && outcome !=
ChaosExchangeState.Outcome.ENTERED) {
+ // the time of the exchange in the route, from when it entered it
until the route was done with it the
+ // first time
+ state.expectedTime = true;
+ long millis = Math.max(0, TimeSource.currentTimeMillis() -
state.enteredAt);
+ for (ChaosRule.Expectation expectation : ruleSet.expectations) {
+ if (expectation.kind.time() &&
expectation.route.equals(routeId)) {
+ expectation.recordTime(millis);
+ }
+ }
+ }
}
/** The error handler redelivers a step of the route: counted for the
report and the expectations. */
@@ -683,6 +699,13 @@ public final class ChaosManager {
for (ChaosRule.Expectation expectation : ruleSet.expectations) {
if (expectation.kind == kind && expectation.route.equals(routeId))
{
expectation.record();
+ } else if (kind == ChaosRule.Expectation.Kind.COMPLETED &&
expectation.kind == ChaosRule.Expectation.Kind.LOST) {
+ // the messages one route completed, and those the next route
completed
+ if (expectation.route.equals(routeId)) {
+ expectation.recordLost(false);
+ } else if (routeId.equals(expectation.toRoute)) {
+ expectation.recordLost(true);
+ }
}
}
}
@@ -952,6 +975,8 @@ public final class ChaosManager {
final ChaosRule[] lifecycle;
final ChaosRule.Expectation[] expectations;
final ChaosRule[] expectationRules;
+ // the routes with an expectation on their time, whose route policy
notes when an exchange enters
+ final Set<String> timedRoutes;
RuleSet(List<ChaosRule> rules, long now) {
// also the rules that start later (from=): the hooks skip them
until then
@@ -969,6 +994,8 @@ public final class ChaosManager {
expectations = Arrays.stream(expectationRules).map(r ->
r.expectation)
.filter(e -> e.outcome(now) ==
ChaosRule.Expectation.Outcome.PENDING && !e.frozen())
.toArray(ChaosRule.Expectation[]::new);
+ timedRoutes = Arrays.stream(expectations).filter(e ->
e.kind.time()).map(e -> e.route)
+ .collect(Collectors.toUnmodifiableSet());
}
}
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 80159d7452a2..576d2287d61b 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
@@ -339,7 +339,9 @@ final class ChaosModel {
"When the rule starts after it is added, such as after
the warm-up (+30s or 30s)", "+30s");
case "expect" -> param("expect", "expectation", false,
"An expectation of a route after the rule is added:
<route>:<count> (at least that many"
- + "
completed) or <route>:<kind><op><count>",
+ + "
completed), <route>:<kind><op><count>, a time"
+ + "
(<route>:meanTime<=2s) or the messages lost"
+ + "
between two routes (<route>-><route>:lost<=0)",
"orders:handled<=0");
case "within" -> param("within", "duration", false,
"The time of the expectation (by default until the
rule ends)", "2m");
@@ -355,7 +357,7 @@ final class ChaosModel {
for (Expectation.Kind kind : Expectation.Kind.values()) {
JsonObject k = new JsonObject();
k.put("name", kind.key);
- k.put("description", "The " + kind.words(2) + " in the route");
+ k.put("description", kind.description);
kinds.add(k);
}
jo.put("kinds", kinds);
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
index 88885f2a7c35..e56eee623847 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
@@ -104,6 +104,13 @@ public final class ChaosReport {
reasons.add("route " + e.route + " completed " +
entry.value + " of " + e.count + " exchanges within "
+ ChaosRuleParser.printTime(rule.within) + "
(rule " + rule.id + "): " + entry.lost
+ " lost");
+ } else if (e.kind.time() || e.kind ==
ChaosRule.Expectation.Kind.LOST) {
+ // a time, or the messages lost between two routes
(CAMEL-26726)
+ String limit = e.kind.time() ?
ChaosRuleParser.printTime(e.count) : Long.toString(e.count);
+ reasons.add((e.toRoute != null ? "routes " + e.route +
"->" + e.toRoute : "route " + e.route) + ": "
+ + entry.valueText + " within " +
ChaosRuleParser.printTime(rule.within) + ", "
+ + e.op.words + " " + limit + " expected (rule
" + rule.id + ")"
+ + (entry.lost > 0 ? ": " + entry.lost + "
lost" : ""));
} else {
reasons.add("route " + e.route + ": " + entry.value + " "
+ e.kind.words(entry.value) + " within "
+ ChaosRuleParser.printTime(rule.within) + ",
" + e.op.words + " " + e.count
@@ -235,7 +242,7 @@ public final class ChaosReport {
if (r.outcome != null) {
sb.append("\n expect ").append(r.expectSpec).append("
within ")
.append(ChaosRuleParser.printTime(r.within)).append(":
").append(r.outcome.key)
- .append(", ").append(r.value).append('
').append(r.expectKind.key);
+ .append(", ").append(r.valueText);
if (r.outcome == Outcome.PENDING) {
sb.append(",
").append(ChaosRuleParser.printTime(r.timeLeft)).append(" left");
} else if (r.outcome == Outcome.CANCELLED) {
@@ -478,6 +485,15 @@ public final class ChaosReport {
if (r.expectKind == ChaosRule.Expectation.Kind.COMPLETED) {
e.put("completed", r.value);
}
+ // the expectation as the rule writes it, and for a time
(count and value in milliseconds) or the
+ // messages lost between two routes, the exchanges it counted
(CAMEL-26726)
+ e.put("spec", r.expectSpec);
+ if (r.expectToRoute != null) {
+ e.put("toRoute", r.expectToRoute);
+ }
+ if (r.expectKind.time() || r.expectToRoute != null) {
+ e.put("samples", r.samples);
+ }
e.put("outcome", r.outcome.key);
if (r.outcome == Outcome.PENDING) {
e.put("timeLeft", r.timeLeft);
@@ -669,6 +685,11 @@ public final class ChaosReport {
final long within;
final Outcome outcome;
final long value;
+ // the value as the report says it, such as 3 completed or mean
1s200ms of 12 exchanges
+ final String valueText;
+ // the next route of a lost expectation, and the exchanges a time or a
lost expectation counted
+ final String expectToRoute;
+ final long samples;
final long lost;
final long timeLeft;
// a lifecycle rule: its action, route, duration and what it did, else
null
@@ -738,6 +759,9 @@ public final class ChaosReport {
ChaosRule.Expectation.Reading reading = expectation.read(now);
outcome = reading.outcome();
value = reading.value();
+ valueText = expectation.valueText(reading);
+ expectToRoute = expectation.toRoute;
+ samples = reading.samples();
lost = expectation.lost(reading);
timeLeft = Math.max(0, expectation.deadline() - now);
} else {
@@ -748,6 +772,9 @@ public final class ChaosReport {
expectCount = 0;
outcome = null;
value = 0;
+ valueText = null;
+ expectToRoute = null;
+ samples = 0;
lost = 0;
timeLeft = 0;
}
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRoutePolicyFactory.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRoutePolicyFactory.java
index a042dc0240d7..b452c41c4858 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRoutePolicyFactory.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRoutePolicyFactory.java
@@ -60,11 +60,22 @@ final class ChaosRoutePolicyFactory implements
RoutePolicyFactory {
@Override
public void onExchangeBegin(Route route, Exchange exchange) {
- ChaosRule[] rules = manager.ruleSet().entry;
- if (rules.length == 0 ||
!ChaosExchangeState.of(exchange).enter(route.getRouteId())) {
+ ChaosManager.RuleSet set = manager.ruleSet();
+ ChaosRule[] rules = set.entry;
+ // an expectation on the time of the route needs when the exchange
entered it (CAMEL-26726)
+ boolean timed = set.timedRoutes.contains(route.getRouteId());
+ if (rules.length == 0 && !timed) {
return;
}
+ ChaosExchangeState exchangeState = ChaosExchangeState.of(exchange);
+ boolean first = exchangeState.enter(route.getRouteId());
long now = TimeSource.currentTimeMillis();
+ if (timed && first) {
+ exchangeState.route(route.getRouteId()).enteredAt = now;
+ }
+ if (rules.length == 0 || !first) {
+ return;
+ }
for (ChaosRule rule : rules) {
if (rule.live(now) && rule.matchesId(route.getRouteId()) &&
rule.matchesWhen(exchange)
&& rule.fires(now)) {
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
index f557c78c32e0..177dd39d50b3 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRule.java
@@ -199,6 +199,8 @@ final class ChaosRule {
final long untilCount;
final long untilDuration;
final String expectRoute;
+ // the next route of a lost expectation (orders->shipping:lost<=0), else
null
+ final String expectToRoute;
final long expectCount;
final long within;
// the action of a lifecycle rule (stall is suspend), else null
@@ -280,6 +282,7 @@ final class ChaosRule {
this.untilCount = b.untilCount;
this.untilDuration = b.untilDuration;
this.expectRoute = b.expectRoute;
+ this.expectToRoute = b.expectToRoute;
this.expectCount = b.expectCount;
this.action = b.action;
this.stall = b.stall;
@@ -293,7 +296,8 @@ final class ChaosRule {
this.endsAt = plus(startsAt, runFor);
this.within = b.within > 0 ? b.within : plus(from, untilDuration > 0 ?
untilDuration : maxDuration);
this.expectation = expectRoute != null
- ? new Expectation(expectRoute, b.expectKind, b.expectOp,
expectCount, plus(addedAt, within)) : null;
+ ? new Expectation(expectRoute, expectToRoute, b.expectKind,
b.expectOp, expectCount, plus(addedAt, within))
+ : null;
for (int i = 0; i < fired.length; i++) {
fired[i] = new LongAdder();
}
@@ -517,12 +521,24 @@ final class ChaosRule {
if (expectation != null) {
Expectation e = expectation;
sb.append("; expects ");
- if (e.op == Expectation.Op.AT_MOST && e.count == 0) {
- sb.append("no ").append(e.kind.words(0));
+ if (e.kind == Expectation.Kind.MEAN_TIME) {
+ sb.append("a mean time of ").append(e.op.words).append('
').append(ChaosWords.time(e.count))
+ .append(" in route ").append(e.route);
+ } else if (e.kind == Expectation.Kind.MAX_TIME) {
+ sb.append("every exchange in route ").append(e.route).append("
to take ").append(e.op.words).append(' ')
+ .append(ChaosWords.time(e.count));
+ } else if (e.kind == Expectation.Kind.LOST) {
+ sb.append(e.count == 0 ? "no message" : e.op.words + " " +
e.count + " " + e.kind.words(e.count))
+ .append(e.count == 0 ? " lost" : "").append(" between
route ").append(e.route).append(" and route ")
+ .append(e.toRoute);
} else {
- sb.append(e.op.words).append(' ').append(e.count).append('
').append(e.kind.words(e.count));
+ if (e.op == Expectation.Op.AT_MOST && e.count == 0) {
+ sb.append("no ").append(e.kind.words(0));
+ } else {
+ sb.append(e.op.words).append(' ').append(e.count).append('
').append(e.kind.words(e.count));
+ }
+ sb.append(" in route ").append(e.route);
}
- sb.append(" in route ").append(e.route);
if (within != plus(from, runFor)) {
// a within= of its own (the default is until the rule ends)
sb.append(" within ").append(ChaosWords.time(within));
@@ -870,25 +886,41 @@ final class ChaosRule {
/** What an expectation counts in its route. */
enum Kind {
- COMPLETED("completed", "exchange completed", "exchanges
completed"),
- HANDLED("handled", "exchange handled by the error handler",
"exchanges handled by the error handler"),
- FAILED("failed", "exchange failed", "exchanges failed"),
- REDELIVERIES("redeliveries", "redelivery", "redeliveries");
+ COMPLETED("completed", "exchange completed", "exchanges
completed", "The exchanges completed in the route"),
+ HANDLED("handled", "exchange handled by the error handler",
"exchanges handled by the error handler",
+ "The exchanges handled by the error handler in the route"),
+ FAILED("failed", "exchange failed", "exchanges failed", "The
exchanges failed in the route"),
+ REDELIVERIES("redeliveries", "redelivery", "redeliveries", "The
redeliveries in the route"),
+ // the time an exchange is in the route, from when it enters until
the route is done with it (CAMEL-26726)
+ MEAN_TIME("meanTime", "mean time", "mean time",
+ "The mean time of the exchanges in the route, from
entering it until done, such as meanTime<=2s"),
+ MAX_TIME("maxTime", "max time", "max time",
+ "The longest time of an exchange in the route, from
entering it until done, such as maxTime<=5s"),
+ // the messages one route completed that the next one did not:
<route>-><next route>:lost<=0
+ LOST("lost", "message lost", "messages lost",
+ "The messages completed in the route and not in the next one,
such as orders->shipping:lost<=0");
final String key;
// for the reason of an unmet expectation
private final String one;
private final String many;
+ final String description;
- Kind(String key, String one, String many) {
+ Kind(String key, String one, String many, String description) {
this.key = key;
this.one = one;
this.many = many;
+ this.description = description;
}
String words(long n) {
return n == 1 ? one : many;
}
+
+ /** Whether the count of the kind is a time (in milliseconds), not
a number of exchanges. */
+ boolean time() {
+ return this == MEAN_TIME || this == MAX_TIME;
+ }
}
/** How the count of an expectation compares. */
@@ -907,11 +939,19 @@ final class ChaosRule {
}
final String route;
+ // the next route of a lost expectation, else null
+ final String toRoute;
final Kind kind;
final Op op;
+ // a number, or a time in milliseconds for a time kind
final long count;
private long deadline;
private long value;
+ // a time kind: the exchanges done, their time together and the
longest; lost: the completed of both routes
+ private long samples;
+ private long sum;
+ private long max;
+ private long toValue;
// when it was decided before the time was up (met for at least, unmet
for at most and exactly), and how
private long decidedAt = -1;
private Outcome decided;
@@ -922,7 +962,12 @@ final class ChaosRule {
}
Expectation(String route, Kind kind, Op op, long count, long deadline)
{
+ this(route, null, kind, op, count, deadline);
+ }
+
+ Expectation(String route, String toRoute, Kind kind, Op op, long
count, long deadline) {
this.route = route;
+ this.toRoute = toRoute;
this.kind = kind;
this.op = op;
this.count = count;
@@ -950,6 +995,39 @@ final class ChaosRule {
}
}
+ /** An exchange was done in the route, after it was so long in it (a
time kind). */
+ synchronized void recordTime(long millis) {
+ recordTimeAt(millis, TimeSource.currentTimeMillis());
+ }
+
+ synchronized void recordTimeAt(long millis, long now) {
+ if (frozenAt >= 0 || now > deadline) {
+ return;
+ }
+ samples++;
+ sum += millis;
+ max = Math.max(max, millis);
+ if (decided == null && kind == Kind.MAX_TIME && op == Op.AT_MOST
&& millis > count) {
+ // one exchange took too long: it cannot become met
+ decided = Outcome.UNMET;
+ decidedAt = now;
+ }
+ }
+
+ /**
+ * An exchange completed in the route ({@code next} false) or in the
next route (true) of a lost expectation.
+ */
+ synchronized void recordLost(boolean next) {
+ if (frozenAt >= 0 || TimeSource.currentTimeMillis() > deadline) {
+ return;
+ }
+ if (next) {
+ toValue++;
+ } else {
+ samples++;
+ }
+ }
+
synchronized long deadline() {
return deadline;
}
@@ -968,7 +1046,12 @@ final class ChaosRule {
}
synchronized long value() {
- return value;
+ return switch (kind) {
+ case MEAN_TIME -> samples > 0 ? sum / samples : 0;
+ case MAX_TIME -> max;
+ case LOST -> Math.max(0, samples - toValue);
+ default -> value;
+ };
}
synchronized Outcome outcome(long now) {
@@ -982,12 +1065,17 @@ final class ChaosRule {
if (now <= deadline) {
return Outcome.PENDING;
}
- // the time is up
- return switch (op) {
- case AT_LEAST -> Outcome.UNMET;
- case AT_MOST -> Outcome.MET;
- case EXACTLY -> value == count ? Outcome.MET : Outcome.UNMET;
+ // the time is up: a time of no exchange says nothing, so it is
not met
+ if (kind.time() && samples == 0) {
+ return Outcome.UNMET;
+ }
+ long v = value();
+ boolean met = switch (op) {
+ case AT_LEAST -> v >= count;
+ case AT_MOST -> v <= count;
+ case EXACTLY -> v == count;
};
+ return met ? Outcome.MET : Outcome.UNMET;
}
synchronized boolean frozen() {
@@ -996,7 +1084,7 @@ final class ChaosRule {
/** The outcome, the counted value and whether it is frozen, read
together (for the report). */
synchronized Reading read(long now) {
- return new Reading(outcome(now), value, frozenAt >= 0);
+ return new Reading(outcome(now), value(), frozenAt >= 0, samples,
toValue);
}
/**
@@ -1004,18 +1092,40 @@ final class ChaosRule {
* many, misses the ones that did not complete.
*/
long lost(Reading reading) {
- return reading.outcome() == Outcome.UNMET && kind ==
Kind.COMPLETED && op != Op.AT_MOST
- ? Math.max(0, count - reading.value()) : 0;
+ if (reading.outcome() != Outcome.UNMET) {
+ return 0;
+ }
+ if (kind == Kind.LOST) {
+ // the messages the next route did not complete
+ return reading.value();
+ }
+ return kind == Kind.COMPLETED && op != Op.AT_MOST ? Math.max(0,
count - reading.value()) : 0;
}
/** The expectation as the rule writes it, such as orders:100 or
orders:handled<=0. */
String spec() {
- return kind == Kind.COMPLETED && op == Op.AT_LEAST
- ? route + ":" + count : route + ":" + kind.key + op.key +
count;
+ if (kind == Kind.COMPLETED && op == Op.AT_LEAST) {
+ return route + ":" + count;
+ }
+ String routes = toRoute != null ? route + "->" + toRoute : route;
+ return routes + ":" + kind.key + op.key + (kind.time() ?
ChaosRuleParser.printTime(count) : count);
+ }
+
+ /** The value as the report says it, such as {@code 3 completed} or
{@code mean 1s200ms of 12 exchanges}. */
+ String valueText(Reading reading) {
+ return switch (kind) {
+ case MEAN_TIME, MAX_TIME -> reading.samples() == 0
+ ? "no exchange done yet"
+ : (kind == Kind.MEAN_TIME ? "mean " : "max ") +
ChaosRuleParser.printTime(reading.value()) + " of "
+ + reading.samples() + (reading.samples() == 1 ? "
exchange" : " exchanges");
+ case LOST -> reading.value() + " lost (" + reading.samples() +
" completed in " + route + ", "
+ + reading.next() + " in " + toRoute + ")";
+ default -> reading.value() + " " + kind.key;
+ };
}
/** What an expectation reads at one time. */
- record Reading(Outcome outcome, long value, boolean frozen) {
+ record Reading(Outcome outcome, long value, boolean frozen, long
samples, long next) {
}
}
@@ -1102,6 +1212,7 @@ final class ChaosRule {
long untilCount;
long untilDuration;
String expectRoute;
+ String expectToRoute;
Expectation.Kind expectKind = Expectation.Kind.COMPLETED;
Expectation.Op expectOp = Expectation.Op.AT_LEAST;
long expectCount;
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
index e7ed74c0d784..73f83bedf216 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
@@ -927,7 +927,17 @@ final class ChaosRuleParser {
+ " or
expect=orders:handled<=0");
}
b.expectRoute = expect.substring(0, colon);
- if (b.expectRoute.indexOf('*') >= 0) {
+ int arrow = b.expectRoute.indexOf("->");
+ if (arrow >= 0) {
+ // the messages one route completed that the next one did not
(CAMEL-26726)
+ b.expectToRoute = b.expectRoute.substring(arrow + 2);
+ b.expectRoute = b.expectRoute.substring(0, arrow);
+ if (b.expectRoute.isEmpty() || b.expectToRoute.isEmpty() ||
b.expectRoute.equals(b.expectToRoute)) {
+ throw new IllegalArgumentException(
+ "expect=" + expect + " needs two routes, such as
expect=orders->shipping:lost<=0");
+ }
+ }
+ if (b.expectRoute.indexOf('*') >= 0 || b.expectToRoute != null &&
b.expectToRoute.indexOf('*') >= 0) {
throw new IllegalArgumentException(
"expect=" + expect + " needs the id of one route, not a
pattern, such as expect=orders:100");
}
@@ -949,8 +959,9 @@ final class ChaosRuleParser {
if (b.expectKind == null) {
throw new IllegalArgumentException(
"expect=" + expect + " counts " + (kind.isEmpty() ?
"nothing" : kind)
- + ": the kinds are
completed, handled, failed and redeliveries,"
- + " such as
expect=orders:handled<=0");
+ + ": the kinds are
completed, handled, failed, redeliveries, meanTime,"
+ + " maxTime and lost, such
as expect=orders:handled<=0,"
+ + "
expect=orders:meanTime<=2s or expect=orders->shipping:lost<=0");
}
String rest = spec.substring(op);
if (rest.startsWith(">=")) {
@@ -966,6 +977,32 @@ final class ChaosRuleParser {
}
count = rest.substring(b.expectOp.key.length());
}
+ if ((b.expectKind == ChaosRule.Expectation.Kind.LOST) !=
(b.expectToRoute != null)) {
+ throw new IllegalArgumentException(
+ b.expectToRoute != null
+ ? "expect=" + expect + " has two routes, which
only lost counts: such as"
+ + " expect=" + b.expectRoute + "->" +
b.expectToRoute + ":lost<=0"
+ : "expect=" + expect + " counts the messages lost
between two routes, such as"
+ + " expect=orders->shipping:lost<=0");
+ }
+ if (b.expectKind.time()) {
+ if (b.expectOp == ChaosRule.Expectation.Op.EXACTLY) {
+ throw new IllegalArgumentException(
+ "expect=" + expect + " needs <= or >= for a time, such
as expect=" + b.expectRoute + ":"
+ + b.expectKind.key +
"<=2s");
+ }
+ b.expectCount = duration("expect", count);
+ if (b.expectCount < 1) {
+ throw new IllegalArgumentException("expect=" + expect + "
needs a time longer than 0");
+ }
+ withinOf(b, within, maxDuration);
+ return;
+ }
+ if (b.expectKind == ChaosRule.Expectation.Kind.LOST && b.expectOp ==
ChaosRule.Expectation.Op.AT_LEAST) {
+ throw new IllegalArgumentException(
+ "expect=" + expect + " needs <= or = for the messages
lost, such as expect=" + b.expectRoute + "->"
+ + b.expectToRoute + ":lost<=0");
+ }
if (!isDigits(count) || count.length() > 18) {
throw new IllegalArgumentException(
"expect=" + expect + " needs a count that is a whole
number of at most 18 digits");
@@ -975,6 +1012,11 @@ final class ChaosRuleParser {
// at least 0 is always met
throw new IllegalArgumentException("expect=" + expect + " needs a
count of 1 or more");
}
+ withinOf(b, within, maxDuration);
+ }
+
+ /** The time of the expectation (within=), at most camel.chaos.maxDuration
after the start of the rule. */
+ private static void withinOf(ChaosRule.Builder b, String within, long
maxDuration) {
if (within != null) {
b.within = duration("within", within);
if (b.within > maxDuration + b.from) {
diff --git
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExpectTimeAndLossTest.java
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExpectTimeAndLossTest.java
new file mode 100644
index 000000000000..07bc14b73daf
--- /dev/null
+++
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosExpectTimeAndLossTest.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.chaos;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * CAMEL-26726: expectations on the time of a route (meanTime, maxTime),
measured by chaos from when an exchange enters
+ * the route until the route is done with it, and on the messages lost between
two routes (orders->ship:lost<=0). The
+ * clock is virtual: a delay moves it by its length.
+ */
+@Isolated
+class ChaosExpectTimeAndLossTest extends ChaosTestSupport {
+
+ private FakeClock clock;
+
+ @Override
+ protected void setupResources() throws Exception {
+ super.setupResources();
+ clock = FakeClock.install();
+ }
+
+ @Override
+ protected void cleanupResources() throws Exception {
+ clock.close();
+ super.cleanupResources();
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:orders").routeId("orders")
+ .to("mock:stock").id("callStock")
+ .to("direct:ship");
+ from("direct:ship").routeId("ship").to("mock:ship");
+ }
+ };
+ }
+
+ private void orders(int n) {
+ for (int i = 0; i < n; i++) {
+ template.sendBody("direct:orders", "order " + i);
+ }
+ }
+
+ private JsonObject expectation(String id) {
+ return (JsonObject) ruleReport(id).get("expectation");
+ }
+
+ @Test
+ void theMeanTimeOfARouteIsDecidedWhenTheTimeIsUp() {
+ // every second call to stock is 3s slow: a mean of 1.5s over 4 orders
+ String id = chaos().addRule("step=callStock delay duration=3s
rate=every:2 expect=orders:meanTime<=2s within=1m");
+ orders(4);
+
+ assertThat(expectation(id).getString("outcome")).isEqualTo("pending");
+ clock.advance(60_001);
+ JsonObject e = expectation(id);
+ assertThat(e.getString("outcome")).isEqualTo("met");
+ assertThat(e.getLong("value")).isEqualTo(1500);
+ assertThat(e.getLong("samples")).isEqualTo(4);
+ assertThat(e.getString("spec")).isEqualTo("orders:meanTime<=2s");
+ assertThat(chaos().report().getVerdict()).isEqualTo(ChaosReport.PASS);
+ assertThat(chaos().report().toText())
+ .contains("expect orders:meanTime<=2s within 1m0s: met, mean
1s500ms of 4 exchanges");
+ assertThat(chaos().describeRule(id))
+ .endsWith("; expects a mean time of at most 2 seconds in route
orders within 1 minute");
+ }
+
+ @Test
+ void oneSlowExchangeFailsTheMaxTimeAtOnce() {
+ String id = chaos().addRule("step=callStock delay duration=3s
rate=every:3 expect=orders:maxTime<=1s within=1m");
+ orders(2);
+ assertThat(expectation(id).getString("outcome")).isEqualTo("pending");
+ orders(1);
+
+ // decided before the time is up
+ JsonObject e = expectation(id);
+ assertThat(e.getString("outcome")).isEqualTo("unmet");
+ assertThat(e.getLong("value")).isEqualTo(3000);
+ ChaosReport report = chaos().report();
+ assertThat(report.getVerdict()).isEqualTo(ChaosReport.FAIL);
+ assertThat(report.getReasons()).containsExactly(
+ "route orders: max 3s of 3 exchanges within 1m0s, at most 1s
expected (rule " + id + ")");
+ // a time is not a message lost
+ assertThat(report.getLost()).isZero();
+ assertThat(chaos().describeRule(id)).contains("expects every exchange
in route orders to take at most 1 second");
+ }
+
+ @Test
+ void aTimeOfNoExchangeIsNotMet() {
+ String id = chaos().addRule("step=callStock delay duration=1ms
expect=ship:meanTime<=1s within=10s");
+ clock.advance(10_001);
+
+ JsonObject e = expectation(id);
+ assertThat(e.getString("outcome")).isEqualTo("unmet");
+ assertThat(chaos().report().getReasons()).singleElement().asString()
+ .startsWith("route ship: no exchange done yet within 10s");
+ }
+
+ @Test
+ void theMessagesLostBetweenTwoRoutes() {
+ // every second order never reaches the ship route
+ String id = chaos().addRule("endpoint=direct:ship drop rate=every:2
expect=orders->ship:lost<=0 within=1m");
+ orders(4);
+ assertThat(expectation(id).getString("outcome")).isEqualTo("pending");
+ clock.advance(60_001);
+
+ JsonObject e = expectation(id);
+ assertThat(e.getString("outcome")).isEqualTo("unmet");
+ assertThat(e.getString("toRoute")).isEqualTo("ship");
+ assertThat(e.getLong("value")).isEqualTo(2);
+ ChaosReport report = chaos().report();
+ assertThat(report.getLost()).isEqualTo(2);
+ assertThat(report.getReasons()).containsExactly(
+ "routes orders->ship: 2 lost (4 completed in orders, 2 in
ship) within 1m0s, at most 0 expected (rule " + id
+ + "): 2 lost");
+ assertThat(chaos().describeRule(id))
+ .endsWith("; expects no message lost between route orders and
route ship within 1 minute");
+ }
+
+ @Test
+ void nothingLostWhenTheNextRouteCompletesThemAll() {
+ String id = chaos().addRule("step=callStock delay duration=10ms
expect=orders->ship:lost=0 within=1m");
+ orders(3);
+ clock.advance(60_001);
+
+ assertThat(expectation(id).getString("outcome")).isEqualTo("met");
+ assertThat(chaos().report().toText()).contains("met, 0 lost (3
completed in orders, 3 in ship)");
+ }
+
+ @Test
+ void theExampleOfTheDocumentation() {
+ assertThat(chaos().addRules(
+ "endpoint=http:stock* delay duration=1s..3s rate=0.3 until=1m
expect=orders:meanTime<=2s within=2m;"
+ + "endpoint=seda:shipping drop rate=0.01
until=1m expect=orders->shipping:lost<=0 within=2m"))
+ .hasSize(2);
+ assertThat(chaos().addRule("step=callStock delay duration=1s
expect=orders:meanTime>=1s")).isNotNull();
+ }
+
+ @Test
+ void theFormsThatAreRefused() {
+ assertThatThrownBy(() -> chaos().addRule("step=callStock delay
duration=1ms expect=orders:meanTime=2s"))
+ .hasMessageContaining("needs <= or >= for a time");
+ assertThatThrownBy(() -> chaos().addRule("step=callStock delay
duration=1ms expect=orders:maxTime<=soon"))
+ .hasMessageContaining("expect=");
+ assertThatThrownBy(() -> chaos().addRule("step=callStock delay
duration=1ms expect=orders:lost<=0"))
+ .hasMessageContaining("counts the messages lost between two
routes, such as expect=orders->shipping:lost<=0");
+ assertThatThrownBy(() -> chaos().addRule("step=callStock delay
duration=1ms expect=orders->ship:completed>=1"))
+ .hasMessageContaining("has two routes, which only lost
counts");
+ assertThatThrownBy(() -> chaos().addRule("step=callStock delay
duration=1ms expect=orders->ship:lost>=1"))
+ .hasMessageContaining("needs <= or = for the messages lost");
+ assertThatThrownBy(() -> chaos().addRule("step=callStock delay
duration=1ms expect=orders->orders:lost<=0"))
+ .hasMessageContaining("needs two routes");
+ assertThatThrownBy(() -> chaos().addRule("step=callStock delay
duration=1ms expect=orders:speed<=1"))
+ .hasMessageContaining("the kinds are completed, handled,
failed, redeliveries, meanTime, maxTime and lost");
+ }
+}
diff --git
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleParserTest.java
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleParserTest.java
index c776ad4e0201..375aec13f1fe 100644
---
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleParserTest.java
+++
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosRuleParserTest.java
@@ -463,7 +463,7 @@ class ChaosRuleParserTest {
"endpoint=x error expect=orders:|is not <route>:<count>",
"endpoint=x error expect=orders:0|needs a count of 1 or more",
"endpoint=x error expect=orders:completed>=0|needs a count of 1 or
more",
- "endpoint=x error expect=orders:lost<=0|counts lost: the kinds are
completed, handled, failed and redeliveries",
+ "endpoint=x error expect=orders:speed<=0|counts speed: the kinds
are completed, handled, failed, redeliveries, meanTime, maxTime and lost",
"endpoint=x error expect=orders:<=0|counts nothing",
"endpoint=x error expect=orders:handled<0|needs >=, <= or = after
handled",
"endpoint=x error expect=orders:handled<=x|needs a count that is a
whole number",
diff --git a/design/chaos.adoc b/design/chaos.adoc
index 73a115855cd5..c49fb05054ae 100644
--- a/design/chaos.adoc
+++ b/design/chaos.adoc
@@ -383,6 +383,10 @@ when the context stops first (the report of
`reportOnShutdown`) or the rule is r
arrive, so they are not called lost. A tool must treat a pending expectation
as not yet decided, so a PASS with one
pending is not final.
+Stage 2 (CAMEL-26726) adds expectations on time and loss:
`orders:meanTime<=2s` and `orders:maxTime<=5s`, with the time
+of an exchange in the route measured by chaos through `TimeSource` (from
entering the route until done), and
+`orders->shipping:lost<=0`, the messages one route completed and the next did
not, which count as lost.
+
In a test (stage 2, CAMEL-26724), `ChaosAssertions` of camel-chaos waits for
the report to be final and asserts the
verdict, an expectation and the faults of a rule, throwing an `AssertionError`
with the report. It needs no test
framework, so camel-test does not depend on camel-chaos; the rules come from
the properties (`camel.chaos.rules`,
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
index 5fefdafa6669..26b482e16097 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
@@ -435,12 +435,24 @@ public class ListChaos extends ProcessWatchCommand {
String op = e.getStringOrDefault("op", ">=");
Object value = e.get("value") != null ? e.get("value") :
e.get("completed");
StringBuilder sb = new StringBuilder();
- sb.append(e.getString("route")).append(':');
- if (!"completed".equals(kind) || !">=".equals(op)) {
- sb.append(kind).append(op);
+ String spec = e.getString("spec");
+ if (spec != null) {
+ // as the rule writes it, such as orders:meanTime<=2s or
orders->shipping:lost<=0 (CAMEL-26726)
+ sb.append(spec);
+ } else {
+ sb.append(e.getString("route")).append(':');
+ if (!"completed".equals(kind) || !">=".equals(op)) {
+ sb.append(kind).append(op);
+ }
+ sb.append(e.get("count"));
+ }
+ sb.append(' ').append(e.getString("outcome")).append(" (");
+ if (("meanTime".equals(kind) || "maxTime".equals(kind)) && value
instanceof Number n) {
+ // a time in milliseconds
+ sb.append(TimeUtils.printDuration(n.longValue())).append('
').append(kind);
+ } else {
+ sb.append(value).append(' ').append(kind);
}
- sb.append(e.get("count")).append(' ').append(e.getString("outcome"))
- .append(" (").append(value).append(' ').append(kind);
if (e.get("timeLeft") instanceof Number n) {
sb.append(",
").append(TimeUtils.printDuration(n.longValue())).append(" left");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
index 70ef721e3740..0c434d1fe15a 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
@@ -242,9 +242,13 @@ final class ChaosAiPrompt {
sb.append("It fired at
").append(rule.firedAt.get("poll")).append(" polls of a consumer (no
exchange).\n");
}
if (rule.expectRoute != null) {
- sb.append("Expectation:
").append(rule.expectRoute).append(':').append(rule.expectKind).append(rule.expectOp)
- .append(rule.expectCount).append(",
").append(rule.expectOutcome != null ? rule.expectOutcome : "pending")
- .append(" (").append(rule.expectValue).append('
').append(rule.expectKind).append(").\n");
+ sb.append("Expectation: ").append(rule.expectSpec != null
+ ? rule.expectSpec : rule.expectRoute + ":" +
rule.expectKind + rule.expectOp + rule.expectCount)
+ .append(", ").append(rule.expectOutcome != null ?
rule.expectOutcome : "pending")
+ .append(" (").append("meanTime".equals(rule.expectKind) ||
"maxTime".equals(rule.expectKind)
+ // a time is in milliseconds
+ ? ChaosTab.shortTime(rule.expectValue) :
String.valueOf(rule.expectValue))
+ .append(' ').append(rule.expectKind).append(").\n");
}
if (rule.lostAtDlq > 0) {
sb.append("Messages lost at the dead letter channel:
").append(rule.lostAtDlq).append(".\n");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosInfo.java
index 997dc728130b..bfd42f763839 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosInfo.java
@@ -82,6 +82,8 @@ final class ChaosInfo {
long expectValue;
String expectOutcome;
long expectTimeLeft = -1;
+ // the expectation as the rule writes it, such as orders:meanTime<=2s
(null before CAMEL-26726)
+ String expectSpec;
// what a stall or lifecycle rule did to its route, or null
String lifecycleAction;
String lifecycleRoute;
@@ -373,6 +375,7 @@ final class ChaosInfo {
r.expectValue = num(e.get("value") != null ? e.get("value") :
e.get("completed"), 0);
r.expectOutcome = e.getString("outcome");
r.expectTimeLeft = num(e.get("timeLeft"), -1);
+ r.expectSpec = e.getString("spec");
}
if (jo.get("lifecycle") instanceof JsonObject l) {
r.lifecycleAction = l.getString("action");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
index 06a58059ba0f..9f9d93e9202f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
@@ -479,8 +479,9 @@ class ChaosTab extends AbstractTableTab {
}
addImpact(lines, info, chaos, r);
if (r.expectRoute != null) {
- String spec = "completed".equals(r.expectKind) &&
">=".equals(r.expectOp)
- ? r.expectRoute + ":" + r.expectCount
+ String spec = r.expectSpec != null ? r.expectSpec
+ : "completed".equals(r.expectKind) &&
">=".equals(r.expectOp)
+ ? r.expectRoute + ":" + r.expectCount
: r.expectRoute + ":" + r.expectKind + r.expectOp +
r.expectCount;
Style outcome = switch (r.expectOutcome != null ? r.expectOutcome
: "") {
case "met" -> Theme.success().bold();
@@ -492,7 +493,10 @@ class ChaosTab extends AbstractTableTab {
expect.add(Span.styled(" Expect ", label));
expect.add(Span.raw(spec + " "));
expect.add(Span.styled(r.expectOutcome != null ?
r.expectOutcome.toUpperCase(Locale.ENGLISH) : "-", outcome));
- expect.add(Span.raw(" " + r.expectValue + " " + r.expectKind));
+ // a time (meanTime, maxTime) is in milliseconds
+ boolean time = "meanTime".equals(r.expectKind) ||
"maxTime".equals(r.expectKind);
+ expect.add(Span.raw(" " + (time ? shortTime(r.expectValue) :
String.valueOf(r.expectValue)) + " "
+ + r.expectKind));
if (r.expectTimeLeft >= 0 && "pending".equals(r.expectOutcome)) {
expect.add(Span.styled(" " + shortTime(r.expectTimeLeft) + "
left", label));
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPromptTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPromptTest.java
index c552725aa343..9e5fc0e497a3 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPromptTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPromptTest.java
@@ -138,6 +138,17 @@ class ChaosAiPromptTest {
+ " confirm first. Do not change the routes.");
}
+ @Test
+ void aTimeExpectationIsAskedAsATime() {
+ ChaosInfo.Rule rule = info.chaos.rules.get(0);
+ rule.expectKind = "meanTime";
+ rule.expectOp = "<=";
+ rule.expectCount = 2000;
+ rule.expectValue = 2340;
+ rule.expectSpec = "orders:meanTime<=2s";
+ assertThat(ChaosAiPrompt.of(info, rule)).contains("Expectation:
orders:meanTime<=2s, unmet (2s meanTime)");
+ }
+
@Test
void theKnownRulesShowTheFormsTheShortSyntaxLeavesOut() {
ChaosRecipes recipes = ChaosRecipes.parse(ChaosRecipesTest.MODEL);