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 3c5b6dd85c9f CAMEL-26674: camel-chaos - reject rules that cannot work
(#29303)
3c5b6dd85c9f is described below
commit 3c5b6dd85c9fcb52a5b91968dea41246d89c1009
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 6 09:50:40 2026 +0100
CAMEL-26674: camel-chaos - reject rules that cannot work (#29303)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../org/apache/camel/catalog/docs/chaos.adoc | 11 ++++--
components/camel-chaos/src/main/docs/chaos.adoc | 11 ++++--
.../camel/component/chaos/ChaosRuleParser.java | 34 ++++++++++++++++++-
.../camel/component/chaos/ChaosRuleParserTest.java | 39 +++++++++++++++++++++-
4 files changed, 87 insertions(+), 8 deletions(-)
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 37621b180b9a..805e6944485a 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
@@ -160,7 +160,8 @@ match is case-sensitive and a `*` can stand anywhere in the
pattern, unlike the
which ignore case and can be regular expressions: a rule, also one from a
tool, cannot make matching slow. An
`endpoint=` pattern is matched against the endpoint URI, with or without its
query parameters, and the `//` after the
scheme does not count (`kafka:orders*` matches `kafka://orders?brokers=b1`). A
`component=` pattern is matched against
-the scheme of the URI. Some components give their endpoints the URI of their
address:
+the scheme of the URI only, so a pattern with a `:`
(`component=http:localhost`) is rejected, with the `endpoint=`
+pattern to use instead. Some components give their endpoints the URI of their
address:
`undertow:http://inventory/stock` has the endpoint URI
`http://inventory/stock`. When the scheme of the endpoint URI is
not a name of the component in the context, chaos puts the default name of the
component in front, and matches
`undertow:http://inventory/stock` (with `component=undertow`, not
`component=http`). A camel-netty-http endpoint has
@@ -171,7 +172,8 @@ matched with `netty-http` in front too. A `route=` pattern
is matched against th
A rule gets an id when it is added (`r1`, `r2`, ...), which change and remove
take. A change replaces the rule with a
new one with the same id and the next version (a rule is version 1 when it is
added), and the replaced one stays in the
report: so the report has an entry per version of the id, and a tool tells
them apart by the `version` of the entry
-(the latest has the highest). A rule ends by itself after its `until`, at the
latest after `camel.chaos.maxDuration`,
+(the latest has the highest). An `until` count counts the matches, so a rule
with `rate=every:N` and an `until` count
+below N would never fire, and is rejected. A rule ends by itself after its
`until`, at the latest after `camel.chaos.maxDuration`,
so a forgotten rule cannot keep breaking a development instance; a `stall` or
`lifecycle` rule ends when its route is
back (see <<_route_lifecycle_faults,Route lifecycle faults>>). Rules stay when
the routes are reloaded.
@@ -219,7 +221,10 @@ first that fires decides the fault.
|Fails the send or the step with that exception (with the message when the
class has a constructor taking a `String`).
Without `exception` (and without `status`) it is a `ChaosException`, a
`RuntimeCamelException`, with the `message` or a
default one. A rule whose class is not found, is not a `Throwable`, is
abstract or not public, or has neither a public
-constructor taking a `String` nor a public no-argument one is rejected when it
is added. Any such public class on the
+constructor taking a `String` nor a public no-argument one is rejected when it
is added, and so is a `message` for a
+class without a constructor taking a `String` (the message would be lost). An
`InterruptedException` (or a subclass) is
+rejected too: Camel stops routing an exchange that failed with one, without
`onException`, redelivery or the dead
+letter channel, so it would not test them. Any such public class on the
classpath of the application can be named, and its constructor runs when the
fault fires, so a rule is development
configuration of a trusted developer, like a route: it comes only from
`camel.chaos.rules` (marked `insecure:dev`), Java
and the CLI connector (which the CLI, `camel ask` and the MCP tools use),
never over the HTTP server of the application,
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc
b/components/camel-chaos/src/main/docs/chaos.adoc
index 37621b180b9a..805e6944485a 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -160,7 +160,8 @@ match is case-sensitive and a `*` can stand anywhere in the
pattern, unlike the
which ignore case and can be regular expressions: a rule, also one from a
tool, cannot make matching slow. An
`endpoint=` pattern is matched against the endpoint URI, with or without its
query parameters, and the `//` after the
scheme does not count (`kafka:orders*` matches `kafka://orders?brokers=b1`). A
`component=` pattern is matched against
-the scheme of the URI. Some components give their endpoints the URI of their
address:
+the scheme of the URI only, so a pattern with a `:`
(`component=http:localhost`) is rejected, with the `endpoint=`
+pattern to use instead. Some components give their endpoints the URI of their
address:
`undertow:http://inventory/stock` has the endpoint URI
`http://inventory/stock`. When the scheme of the endpoint URI is
not a name of the component in the context, chaos puts the default name of the
component in front, and matches
`undertow:http://inventory/stock` (with `component=undertow`, not
`component=http`). A camel-netty-http endpoint has
@@ -171,7 +172,8 @@ matched with `netty-http` in front too. A `route=` pattern
is matched against th
A rule gets an id when it is added (`r1`, `r2`, ...), which change and remove
take. A change replaces the rule with a
new one with the same id and the next version (a rule is version 1 when it is
added), and the replaced one stays in the
report: so the report has an entry per version of the id, and a tool tells
them apart by the `version` of the entry
-(the latest has the highest). A rule ends by itself after its `until`, at the
latest after `camel.chaos.maxDuration`,
+(the latest has the highest). An `until` count counts the matches, so a rule
with `rate=every:N` and an `until` count
+below N would never fire, and is rejected. A rule ends by itself after its
`until`, at the latest after `camel.chaos.maxDuration`,
so a forgotten rule cannot keep breaking a development instance; a `stall` or
`lifecycle` rule ends when its route is
back (see <<_route_lifecycle_faults,Route lifecycle faults>>). Rules stay when
the routes are reloaded.
@@ -219,7 +221,10 @@ first that fires decides the fault.
|Fails the send or the step with that exception (with the message when the
class has a constructor taking a `String`).
Without `exception` (and without `status`) it is a `ChaosException`, a
`RuntimeCamelException`, with the `message` or a
default one. A rule whose class is not found, is not a `Throwable`, is
abstract or not public, or has neither a public
-constructor taking a `String` nor a public no-argument one is rejected when it
is added. Any such public class on the
+constructor taking a `String` nor a public no-argument one is rejected when it
is added, and so is a `message` for a
+class without a constructor taking a `String` (the message would be lost). An
`InterruptedException` (or a subclass) is
+rejected too: Camel stops routing an exchange that failed with one, without
`onException`, redelivery or the dead
+letter channel, so it would not test them. Any such public class on the
classpath of the application can be named, and its constructor runs when the
fault fires, so a rule is development
configuration of a trusted developer, like a route: it comes only from
`camel.chaos.rules` (marked `insecure:dev`), Java
and the CLI connector (which the CLI, `camel ask` and the MCP tools use),
never over the HTTP server of the application,
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 35dbc0cc3342..19349848459c 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
@@ -184,6 +184,14 @@ final class ChaosRuleParser {
if (b.fault == null) {
throw new IllegalArgumentException("a rule needs a fault: error,
delay, drop, corrupt, stall or lifecycle");
}
+ if (b.target == Target.COMPONENT && b.pattern.indexOf(':') >= 0) {
+ // matched against the scheme only, so a pattern with a colon
would never match
+ throw new IllegalArgumentException(
+ "component=" + b.pattern + " has a ':', but component= is
matched against the scheme of the"
+ + " endpoint URI only (such as
component=http): use endpoint="
+ + b.pattern +
(b.pattern.endsWith("*") ? "" : "*")
+ + " to match the endpoint");
+ }
if (b.fault == Fault.LIFECYCLE) {
// stall and lifecycle act once on the route: no hook, rate or
until
lifecycle(b, params, maxDuration);
@@ -223,6 +231,13 @@ final class ChaosRuleParser {
hooks(b, params.get("at"), http);
rate(b, params.get("rate"));
until(b, params.get("until"), maxDuration);
+ if (b.every > b.untilCount && b.untilCount > 0) {
+ // until counts the matches, so the rule ends before its first
fault
+ throw new IllegalArgumentException(
+ "rate=every:" + b.every + " fires at match " + b.every +
", but until=" + b.untilCount
+ + " ends the rule after " +
b.untilCount + " matches, so it would never"
+ + " fire");
+ }
expectation(b, params.get("expect"), params.get("within"),
maxDuration);
return b;
}
@@ -270,7 +285,7 @@ final class ChaosRuleParser {
while (i < n) {
char c = text.charAt(i);
checkChar(text, i);
- if (c == '\\' && i + 1 < n && (text.charAt(i + 1) == quote
|| text.charAt(i + 1) == '\\')) {
+ if (c == '\\' && i + 1 < n && isEscaped(text.charAt(i +
1))) {
value.append(text.charAt(i + 1));
i += 2;
} else if (c == quote) {
@@ -315,6 +330,11 @@ final class ChaosRuleParser {
return ch == ' ' || ch == '\t';
}
+ /** The characters that a backslash escapes inside a quoted value: either
quote and the backslash. */
+ private static boolean isEscaped(char ch) {
+ return ch == '"' || ch == '\'' || ch == '\\';
+ }
+
private static void checkChar(String text, int i) {
char ch = text.charAt(i);
if (ch < 0x20 && ch != '\t' || ch == 0x7f) {
@@ -442,6 +462,13 @@ final class ChaosRuleParser {
if (!Throwable.class.isAssignableFrom(type)) {
throw new IllegalArgumentException("the exception class " + name +
" is not a Throwable");
}
+ if (InterruptedException.class.isAssignableFrom(type)) {
+ // Camel marks an exchange with an InterruptedException as
interrupted and stops routing it, so
+ // onException, redelivery and the dead letter channel would never
see the failure
+ throw new IllegalArgumentException(
+ "the exception class " + name + " is an
InterruptedException, which makes Camel stop routing the"
+ + " exchange without
onException, redelivery or the dead letter channel");
+ }
if (Modifier.isAbstract(type.getModifiers()) ||
!Modifier.isPublic(type.getModifiers())) {
throw new IllegalArgumentException("the exception class " + name +
" is abstract or not public");
}
@@ -451,6 +478,11 @@ final class ChaosRuleParser {
Constructor<? extends Throwable> constructor = constructor(throwable,
String.class);
b.exceptionTakesMessage = constructor != null;
if (constructor == null) {
+ if (b.message != null) {
+ throw new IllegalArgumentException(
+ "the exception class " + name + " has no public
constructor taking a String message,"
+ + " so it cannot be used
with message=");
+ }
constructor = constructor(throwable);
}
if (constructor == null) {
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 11ef2c3bb5e3..34b20ec9b822 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
@@ -166,6 +166,11 @@ class ChaosRuleParserTest {
void quotedValuesAndEscapes() {
assertThat(parse("endpoint=x error message='it is
\\'down\\''").message).isEqualTo("it is 'down'");
assertThat(parse("endpoint=x error message=\"a \\\\ b ;
c\"").message).isEqualTo("a \\ b ; c");
+ // either quote can be escaped inside either kind of quotes
+ assertThat(parse("endpoint=x error message=\"it\\'s
\\\"down\\\"\"").message).isEqualTo("it's \"down\"");
+ assertThat(parse("endpoint=x error message='say
\\\"hi\\\"'").message).isEqualTo("say \"hi\"");
+ // a backslash before any other character stays
+ assertThat(parse("endpoint=x error
message=\"c:\\temp\"").message).isEqualTo("c:\\temp");
assertThat(parse(" endpoint=x\terror
").fault).isEqualTo(Fault.ERROR);
}
@@ -300,7 +305,11 @@ class ChaosRuleParserTest {
"endpoint=x error at=step|(the hooks of endpoint= are: at=send
offers error, delay, drop",
"step=s1 error at=send|at=send is not a hook of the target step=",
"step=s1 delay duration=1s at=send|at=send is not a hook of the
target step=",
- "Endpoint=x error|a rule needs a target" })
+ "Endpoint=x error|a rule needs a target",
+ "component=http:localhost error|component= is matched against the
scheme of the endpoint URI only",
+ "component=http:localhost error|use endpoint=http:localhost* to
match the endpoint",
+ "component=http:* delay duration=1s|use endpoint=http:* to match
the endpoint",
+ "endpoint=x error rate=every:10 until=5|would never fire" })
void rejectedWithReason(String rule, String reason) {
rejected(rule, reason.replace("\\'", "'"));
}
@@ -320,6 +329,26 @@ class ChaosRuleParserTest {
ChaosRule.Builder b = parse("endpoint=x error exception=" +
NoArgOnly.class.getName());
assertThat(b.exceptionTakesMessage).isFalse();
assertThat(b.exceptionConstructor).isNotNull();
+ // the message would be dropped
+ rejected("endpoint=x error exception=" + NoArgOnly.class.getName() + "
message=down",
+ "has no public constructor taking a String message, so it
cannot be used with message=");
+ }
+
+ @Test
+ void interruptedExceptionIsRejected() {
+ // Camel stops routing an interrupted exchange, so the error handler
would never see the fault
+ rejected("step=s1 error exception=" +
InterruptedException.class.getName(), "is an InterruptedException");
+ rejected("endpoint=x error exception=" + Interrupted.class.getName() +
" message=x",
+ "is an InterruptedException");
+ }
+
+ @Test
+ void everyNFiresWithinAnUntilCount() {
+ ChaosRule.Builder b = parse("endpoint=x error rate=every:5 until=5");
+ assertThat(b.every).isEqualTo(5L);
+ assertThat(b.untilCount).isEqualTo(5L);
+ // a time does not limit the matches
+ assertThat(parse("endpoint=x error rate=every:10
until=5s").every).isEqualTo(10L);
}
@Test
@@ -409,4 +438,12 @@ class ChaosRuleParserTest {
public static class NoArgOnly extends RuntimeException {
private static final long serialVersionUID = 1L;
}
+
+ public static class Interrupted extends InterruptedException {
+ private static final long serialVersionUID = 1L;
+
+ public Interrupted(String message) {
+ super(message);
+ }
+ }
}