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 01b1008ee1c1 CAMEL-25115: camel-semantic - A failing retry predicate 
escalates without another attempt
01b1008ee1c1 is described below

commit 01b1008ee1c1b2a37ff7fc315d9fa623f468407d
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 29 09:41:20 2026 +0200

    CAMEL-25115: camel-semantic - A failing retry predicate escalates without 
another attempt
    
    Since CAMEL-25115 a retryWhile predicate that throws is regarded as false: 
the message is not redelivered and the
    exception clause outputs run, with the evaluation failure attached to the 
original exception as a suppressed
    exception. Update the semantic retry test and documentation to this 
behavior.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/catalog/docs/semantic-language.adoc      | 11 +++++---
 .../src/main/docs/semantic-language.adoc           | 11 +++++---
 .../semantic/SemanticRetryAndValidationTest.java   | 29 +++++++++++++++-------
 3 files changed, 34 insertions(+), 17 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
index a62eb7f92db4..f8ec5e5b7e3d 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
@@ -362,10 +362,13 @@ redeliveries after the initial attempt. Each failure 
refreshes the selected stat
 restarts at the failed processor, not at the beginning of the route. A 
negative decision or
 an exhausted budget sends the message to `direct:escalate` through normal 
exception handling.
 
-Evaluation errors are distinct from a negative decision. If the retry 
predicate throws,
-Camel reports the evaluation failure on the exchange; it does not 
automatically execute
-the exception clause's escalation route. Arrange caller or supervising-route 
handling for
-that failure. Bound provider timeouts as well as the retry count.
+If the retry predicate throws (such as a provider timeout, or a malformed or 
uncertain result),
+Camel logs the evaluation failure at `WARN` level and regards the predicate as 
`false`. The message
+is not redelivered and is sent to `direct:escalate` through normal exception 
handling, the same
+as for a negative decision. The original exception is kept as the caught 
exception, with the
+evaluation failure attached as a suppressed exception. To tell an evaluation 
failure apart from a
+negative decision in the escalation route, check `getSuppressed()` of the
+`Exchange.EXCEPTION_CAUGHT` exception. Bound provider timeouts as well as the 
retry count.
 
 === Contextual action validation
 
diff --git 
a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc 
b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
index a62eb7f92db4..f8ec5e5b7e3d 100644
--- a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
+++ b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
@@ -362,10 +362,13 @@ redeliveries after the initial attempt. Each failure 
refreshes the selected stat
 restarts at the failed processor, not at the beginning of the route. A 
negative decision or
 an exhausted budget sends the message to `direct:escalate` through normal 
exception handling.
 
-Evaluation errors are distinct from a negative decision. If the retry 
predicate throws,
-Camel reports the evaluation failure on the exchange; it does not 
automatically execute
-the exception clause's escalation route. Arrange caller or supervising-route 
handling for
-that failure. Bound provider timeouts as well as the retry count.
+If the retry predicate throws (such as a provider timeout, or a malformed or 
uncertain result),
+Camel logs the evaluation failure at `WARN` level and regards the predicate as 
`false`. The message
+is not redelivered and is sent to `direct:escalate` through normal exception 
handling, the same
+as for a negative decision. The original exception is kept as the caught 
exception, with the
+evaluation failure attached as a suppressed exception. To tell an evaluation 
failure apart from a
+negative decision in the escalation route, check `getSuppressed()` of the
+`Exchange.EXCEPTION_CAUGHT` exception. Bound provider timeouts as well as the 
retry count.
 
 === Contextual action validation
 
diff --git 
a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticRetryAndValidationTest.java
 
b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticRetryAndValidationTest.java
index 96f922aaa3d6..a5abe7d6bd71 100644
--- 
a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticRetryAndValidationTest.java
+++ 
b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticRetryAndValidationTest.java
@@ -31,6 +31,7 @@ import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.mock.MockEndpoint;
 import org.apache.camel.language.semantic.SemanticLanguage;
 import org.apache.camel.test.junit6.CamelTestSupport;
+import org.apache.camel.util.ObjectHelper;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
@@ -170,16 +171,21 @@ class SemanticRetryAndValidationTest extends 
CamelTestSupport {
 
     @ParameterizedTest
     @ValueSource(strings = { "timeout", "malformed", "uncertain" })
-    void retryEvaluationFailurePropagatesWithoutRetryOrNormalEscalation(String 
failure) throws Exception {
+    void retryEvaluationFailureEscalatesWithoutRetry(String failure) throws 
Exception {
         failEvaluation(failure);
         getMockEndpoint("mock:completed").expectedMessageCount(0);
-        getMockEndpoint("mock:escalated").expectedMessageCount(0);
+        getMockEndpoint("mock:escalated").expectedBodiesReceived("operation");
 
         Exchange exchange = template.request("direct:retry", e -> 
e.getMessage().setBody("operation"));
 
-        assertEvaluationFailure(exchange, failure);
-        assertThat(exchange.getMessage().getBody()).isEqualTo("operation");
-        assertThat(exchange.getProperty(Exchange.EXCEPTION_CAUGHT, 
Exception.class)).isInstanceOf(IOException.class);
+        // a retry predicate that fails is regarded as false, so the exception 
clause escalates without another
+        // attempt, and the evaluation failure is attached to the original 
exception as a suppressed exception
+        assertThat(exchange.getException()).isNull();
+        IOException caught = exchange.getProperty(Exchange.EXCEPTION_CAUGHT, 
IOException.class);
+        assertThat(caught).hasMessage("Failure 1");
+        assertThat(caught.getSuppressed()).hasSize(1);
+        assertEvaluationFailure(caught.getSuppressed()[0], failure);
+        assertThat(exchange.getProperty(SemanticLanguage.RESULT)).isNull();
         assertThat(attempts).hasValue(1);
         assertThat(evaluatedStates).containsExactly(retryState(1));
         MockEndpoint.assertIsSatisfied(context);
@@ -259,14 +265,19 @@ class SemanticRetryAndValidationTest extends 
CamelTestSupport {
     }
 
     private static void assertEvaluationFailure(Exchange exchange, String 
failure) {
+        assertEvaluationFailure(exchange.getException(), failure);
+        assertThat(exchange.getProperty(SemanticLanguage.RESULT)).isNull();
+    }
+
+    private static void assertEvaluationFailure(Throwable exception, String 
failure) {
         switch (failure) {
-            case "timeout" -> 
assertThat(exchange.getException(TimeoutException.class)).hasMessage("Evaluation
 timed out");
-            case "malformed" -> 
assertThat(exchange.getException(IllegalArgumentException.class))
+            case "timeout" -> 
assertThat(ObjectHelper.getException(TimeoutException.class, exception))
+                    .hasMessage("Evaluation timed out");
+            case "malformed" -> 
assertThat(ObjectHelper.getException(IllegalArgumentException.class, exception))
                     .hasMessage("Semantic result does not support the question 
and its decision policy");
-            case "uncertain" -> 
assertThat(exchange.getException(IllegalStateException.class))
+            case "uncertain" -> 
assertThat(ObjectHelper.getException(IllegalStateException.class, exception))
                     .hasMessage("Semantic boolean decision is uncertain");
             default -> throw new IllegalArgumentException(failure);
         }
-        assertThat(exchange.getProperty(SemanticLanguage.RESULT)).isNull();
     }
 }

Reply via email to