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();
}
}