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)) {

Reply via email to