This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26817-suggest-expectation in repository https://gitbox.apache.org/repos/asf/camel.git
commit 235fc46a4d7e9afd49fbbb2dccdb340536e3748f Author: Claus Ibsen <[email protected]> AuthorDate: Tue Nov 10 21:00:42 2026 +0100 CAMEL-26817: camel-chaos - suggestions for a circuit breaker around a call to another route, and for the HTTP calls of toD Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../apache/camel/component/chaos/ChaosAdvisor.java | 87 ++++++++++++++++++++++ .../camel/component/chaos/ChaosAdvisorTest.java | 48 ++++++++++++ 2 files changed, 135 insertions(+) diff --git a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java index 074803331078..a2554f06aebb 100644 --- a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java +++ b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java @@ -36,6 +36,7 @@ import org.apache.camel.Route; import org.apache.camel.model.CircuitBreakerDefinition; import org.apache.camel.model.ProcessorDefinitionHelper; import org.apache.camel.model.RouteDefinition; +import org.apache.camel.model.ToDynamicDefinition; import org.apache.camel.model.errorhandler.DeadLetterChannelDefinition; import org.apache.camel.support.ScheduledPollConsumer; import org.apache.camel.util.URISupport; @@ -528,6 +529,20 @@ final class ChaosAdvisor { 85)); continue; } + if (breaker && "direct".equals(scheme) && consumers.containsKey(uri) && !id.equals(consumers.get(uri))) { + // a call to another route of the integration in a route with a circuit breaker (CAMEL-26817): that + // route stands for the service the breaker guards + String next = consumers.get(uri); + add(answer, recipes, "outage", id, Kind.ERRORS, Map.of("endpoint", uri), + "route " + id + " calls route " + next + " in a circuit breaker: when " + next + + " is down for a while, does the breaker open, and does the fallback answer?", + 70); + add(answer, recipes, "connection-refused", id, Kind.ERRORS, Map.of("endpoint", uri, "rate", "0.5"), + "route " + id + " calls route " + next + " in a circuit breaker: when half of the calls fail," + + " does the breaker open at its failure rate, and close again?", + 62); + continue; + } if (!ChaosStubs.isRemote(to)) { continue; } @@ -578,6 +593,28 @@ final class ChaosAdvisor { } } + // the HTTP calls known only when they send (toD), by the address before the first expression (CAMEL-26817) + for (String pattern : dynamicHttpCalls(route)) { + if (!seen.add(pattern)) { + continue; + } + if (breaker) { + add(answer, recipes, "outage", id, Kind.ERRORS, Map.of("endpoint", pattern), + "the HTTP calls to " + pattern + " in a route with a circuit breaker: when the service is down" + + " for a while, does the breaker open, and does the fallback answer?", + 70); + } else { + add(answer, recipes, "http-error", id, Kind.ERRORS, Map.of("endpoint", pattern), + "the HTTP calls to " + pattern + " without a circuit breaker: what does the route do when the" + + " service answers 503?", + 88); + } + add(answer, recipes, "http-error", id, Kind.ERRORS, Map.of("endpoint", pattern, "status", "404"), + "the HTTP calls to " + pattern + " ask for a resource per message: what does the route do when it" + + " is not there (404)?", + 72); + } + // the dead letter channel of the route: once for the routes that share it, with the first of them String dead = deadLetterUri(route); List<String> shared = dead != null ? deadLetters.getOrDefault(dead, List.of()) : List.of(); @@ -631,6 +668,56 @@ final class ChaosAdvisor { } } + /** + * The HTTP calls of the route that are known only when they send (toD), each as a pattern of its address up to the + * first expression, such as {@code http://localhost:8080/stock/*} for + * {@code http://localhost:8080/stock/${variable.sku}}. A call whose address starts with an expression says nothing + * of where it goes, and is left out. + */ + static List<String> dynamicHttpCalls(Route route) { + if (!(route.getRoute() instanceof RouteDefinition definition)) { + return List.of(); + } + List<String> uris = new ArrayList<>(); + for (ToDynamicDefinition to : ProcessorDefinitionHelper.filterTypeInOutputs(definition.getOutputs(), + ToDynamicDefinition.class)) { + if (to.getUri() != null) { + uris.add(to.getUri()); + } + } + List<String> answer = new ArrayList<>(); + for (String uri : uris) { + try { + uri = route.getCamelContext().resolvePropertyPlaceholders(uri); + } catch (Exception e) { + // a placeholder that does not resolve now: the address up to it + } + int query = uri.indexOf('?'); + String address = query > 0 ? uri.substring(0, query) : uri; + int cut = address.length(); + for (String start : List.of("${", "$simple{", "{{")) { + int at = address.indexOf(start); + if (at >= 0) { + cut = Math.min(cut, at); + } + } + String scheme = ChaosPatterns.scheme(address); + if (cut == address.length() || scheme == null || !ChaosRuleParser.HTTP_SCHEMES.contains(scheme)) { + continue; + } + String prefix = address.substring(0, cut); + // the host must be known: a call to ${header.host} could go anywhere + if (ChaosPatterns.withoutSlashes(prefix).length() <= scheme.length() + 1) { + continue; + } + String pattern = prefix + "*"; + if (!answer.contains(pattern)) { + answer.add(pattern); + } + } + return answer; + } + /** * The URI of an endpoint as a rule targets it, without its query: the options say nothing of which endpoint it is, * and may hold a secret. diff --git a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosAdvisorTest.java b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosAdvisorTest.java index 4df6e1cf6776..e73ea5b9c8bf 100644 --- a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosAdvisorTest.java +++ b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosAdvisorTest.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; import org.apache.camel.BindToRegistry; +import org.apache.camel.Exchange; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; @@ -58,6 +59,17 @@ class ChaosAdvisorTest extends ChaosTestSupport { .end(); from("testpoll:inbox?delay=100000&initialDelay=100000").routeId("inbox").to("mock:inbox"); from("direct:archive").routeId("archive").to("file:target/chaos-archive"); + // a circuit breaker around a call to another route, which stands for the service + from("direct:check").routeId("check") + .circuitBreaker() + .to("direct:supplier") + .onFallback().setBody(constant("cached")) + .end(); + from("direct:supplier").routeId("supplier").setBody(constant("in stock")); + // an HTTP call per message, known only when it sends + from("direct:lookup").routeId("lookup") + .toD("http://stock/items/${header.sku}?throwExceptionOnFailure=false") + .toD("${header.target}"); } }; } @@ -107,6 +119,42 @@ class ChaosAdvisorTest extends ChaosTestSupport { assertThat(suggestions.get(0).why()).contains("without a circuit breaker"); } + @Test + void aCircuitBreakerAroundACallToAnotherRoute() { + List<String> rules = ChaosAdvisor.suggest(context).stream().filter(s -> s.route().equals("check")) + .map(ChaosAdvisor.Suggestion::rule).toList(); + // down for a while, so the breaker opens, and half of the calls failing, past its failure rate: the fallback + // answers, so the route does not fail + assertThat(rules).containsExactly( + "endpoint=direct://supplier error rate=burst:10s/1m until=10m expect=check:failed<=0", + "endpoint=direct://supplier error exception=java.net.ConnectException rate=0.5 until=10m" + + " expect=check:failed<=0"); + // a call to another route without a breaker is not a service to fail + assertThat(ChaosAdvisor.suggest(context)).noneMatch(s -> s.rule().startsWith("endpoint=direct://check")); + } + + @Test + void theHttpCallsOfToD() { + List<String> rules = ChaosAdvisor.suggest(context).stream().filter(s -> s.route().equals("lookup")) + .map(ChaosAdvisor.Suggestion::rule).toList(); + // by the address up to the first expression; a call to ${header.target} says nothing of where it goes + assertThat(rules).containsExactly( + "endpoint=http://stock/items/* error status=503 rate=0.25 until=10m expect=lookup:failed<=0", + "endpoint=http://stock/items/* error status=404 rate=0.25 until=10m expect=lookup:failed<=0"); + } + + @Test + void aRuleOfToDFailsTheCall() throws Exception { + chaos().addRule("endpoint=http://stock/items/* error status=404"); + Exchange out = template.request("direct:lookup", e -> { + e.getMessage().setHeader("sku", "mug"); + e.getMessage().setHeader("target", "mock:out"); + }); + // throwExceptionOnFailure=false: the response with the status, as the HTTP component gives it + assertThat(out.getException()).isNull(); + assertThat(out.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(404); + } + @Test void everySuggestedRuleIsValid() { for (ChaosAdvisor.Suggestion s : ChaosAdvisor.suggest(context)) {
