This is an automated email from the ASF dual-hosted git repository.

gnodet 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 bd4d2ba20d6a CAMEL-24903: Fix flaky 
ThrottlingExceptionRoutePolicyOpenViaConfigTest (#26799)
bd4d2ba20d6a is described below

commit bd4d2ba20d6a268b66949712d00073069a8e333d
Author: Guillaume Nodet <[email protected]>
AuthorDate: Thu Sep 24 16:18:27 2026 +0200

    CAMEL-24903: Fix flaky ThrottlingExceptionRoutePolicyOpenViaConfigTest 
(#26799)
    
    * CAMEL-24903: Fix flaky ThrottlingExceptionRoutePolicyOpenViaConfigTest
    
    Root cause: with concurrentConsumers=20, a round-1 onExchangeDone()
    callback could fire after policy.setKeepOpen(true) was called, seeing
    keepOpenBool=true and calling openCircuit() to suspend the consumer
    BEFORE the 'MessageTrigger' message was picked up from the SEDA queue.
    This caused the expectedMessageCount(6) assertion to fail with count 5.
    
    Fix (two-pronged):
    1. ThrottlingExceptionRoutePolicy.setKeepOpen(true) now immediately
       opens the circuit via openCircuit(route) rather than waiting for the
       next onExchangeDone() callback. This makes the keepOpen toggle
       synchronous and deterministic.
    2. Test reordering: 'MessageTrigger' is now sent and awaited BEFORE
       setKeepOpen(true) is called, ensuring it is consumed while the
       circuit is still closed. setKeepOpen(true) then suspends the consumer
       immediately so no subsequent messages flow through.
    
    Co-authored-by: Claude Sonnet 4.6 <[email protected]>
    
    * CAMEL-24903: Address review feedback
    
    - Move state.get() != STATE_OPEN guard inside openCircuit() under the
      lock so concurrent callers are idempotent and spurious timer chains
      cannot accumulate (davsclaus item 1)
    - Remove now-redundant outer guard from setKeepOpen(); add Javadoc
      documenting synchronous-open semantics and single-route ownership
      (davsclaus items 1 & 2)
    - Add upgrade-guide entry for the behaviour change in setKeepOpen(true)
      (davsclaus item 3)
    - Add setAssertPeriod(500) to the round-2 keepOpen assertion so it
      actually verifies no extra messages arrive while the circuit is open
      (davsclaus blue nit)
    
    ---------
    
    Co-authored-by: Claude Sonnet 4.6 <[email protected]>
---
 ...tlingExceptionRoutePolicyOpenViaConfigTest.java | 18 ++++++++++++------
 .../throttling/ThrottlingExceptionRoutePolicy.java | 22 ++++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  9 +++++++++
 3 files changed, 43 insertions(+), 6 deletions(-)

diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyOpenViaConfigTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyOpenViaConfigTest.java
index 75d7b7b81abc..46ea90090390 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyOpenViaConfigTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyOpenViaConfigTest.java
@@ -69,12 +69,16 @@ class ThrottlingExceptionRoutePolicyOpenViaConfigTest 
extends ContextTestSupport
         result.expectedMessageCount(size);
         MockEndpoint.assertIsSatisfied(context, TIMEOUT_SECONDS, 
TimeUnit.SECONDS);
 
-        // set keepOpen to true
-        policy.setKeepOpen(true);
-
-        // trigger opening circuit
-        // by sending another message
+        // send the trigger message while the circuit is still closed so it is
+        // guaranteed to be consumed before we open the circuit
         template.sendBody(url, "MessageTrigger");
+        result.expectedMessageCount(size + 1);
+        MockEndpoint.assertIsSatisfied(context, TIMEOUT_SECONDS, 
TimeUnit.SECONDS);
+
+        // toggle keepOpen to true: setKeepOpen() immediately suspends the
+        // consumer so no late onExchangeDone() callback can race against the
+        // trigger message (CAMEL-24903)
+        policy.setKeepOpen(true);
 
         // wait for the circuit to open (consumer suspended)
         await().atMost(10, TimeUnit.SECONDS).until(consumer::isSuspended);
@@ -85,9 +89,11 @@ class ThrottlingExceptionRoutePolicyOpenViaConfigTest 
extends ContextTestSupport
             template.sendBody(url, "MessageRound2 " + i);
         }
 
-        // should not close b/c keepOpen is true
+        // should not close b/c keepOpen is true; use assertPeriod to verify 
no extra messages arrive
+        result.setAssertPeriod(500);
         result.expectedMessageCount(size + 1);
         MockEndpoint.assertIsSatisfied(context, TIMEOUT_SECONDS, 
TimeUnit.SECONDS);
+        result.setAssertPeriod(0);
 
         // set keepOpen to false
         policy.setKeepOpen(false);
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/throttling/ThrottlingExceptionRoutePolicy.java
 
b/core/camel-support/src/main/java/org/apache/camel/throttling/ThrottlingExceptionRoutePolicy.java
index 1254990c99fe..e453f4be20b3 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/throttling/ThrottlingExceptionRoutePolicy.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/throttling/ThrottlingExceptionRoutePolicy.java
@@ -308,6 +308,9 @@ public class ThrottlingExceptionRoutePolicy extends 
RoutePolicySupport implement
     protected void openCircuit(Route route) {
         try {
             lock.lock();
+            if (state.get() == STATE_OPEN) {
+                return;
+            }
             suspendOrStopConsumer(route.getConsumer());
             state.set(STATE_OPEN);
             openedAt = System.currentTimeMillis();
@@ -413,8 +416,27 @@ public class ThrottlingExceptionRoutePolicy extends 
RoutePolicySupport implement
         return this.keepOpenBool.get();
     }
 
+    /**
+     * Sets whether to keep the circuit breaker always open (never closes). 
This is only intended for development and
+     * testing purposes.
+     * <p>
+     * When set to {@code true}, the circuit is opened immediately and 
synchronously (the consumer is suspended before
+     * this method returns). Setting it back to {@code false} is deferred: the 
half-open timer will attempt to close the
+     * circuit on its next tick (after {@link #getHalfOpenAfter()} 
milliseconds).
+     * <p>
+     * Note: a single {@link ThrottlingExceptionRoutePolicy} instance is 
designed for one route. When attached to
+     * multiple routes, only the last route bound via {@link #setRoute(Route)} 
is acted upon by this setter.
+     */
     public void setKeepOpen(boolean keepOpen) {
         this.keepOpenBool.set(keepOpen);
+        // Immediately act on the new value so callers do not have to send a
+        // trigger message and wait for the next onExchangeDone() callback:
+        //  - keepOpen=true  → open the circuit right away (openCircuit is 
idempotent under its lock)
+        //  - keepOpen=false → the half-open timer will close the circuit on 
its next tick
+        if (keepOpen && route != null) {
+            LOG.debug("Opening circuit (keepOpen set to true)");
+            openCircuit(route);
+        }
     }
 
     public int getFailureThreshold() {
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index d23eb1f20856..f70b258eedc1 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -2812,3 +2812,12 @@ producer were never stopped.
 
 With `parallelProcessing`, a recipient whose task had not started yet when the 
Recipient List completed is now
 skipped instead of being sent to afterwards. As before, recipients that had 
already started keep running.
+
+== ThrottlingExceptionRoutePolicy
+
+`ThrottlingExceptionRoutePolicy.setKeepOpen(true)` now opens the circuit 
immediately and synchronously (the consumer
+is suspended before the setter returns) rather than waiting for the next 
`onExchangeDone()` callback.
+This is the intended behaviour and makes the `keepOpen` toggle deterministic, 
but callers that previously
+relied on the setter being inert until an exchange arrived must be aware of 
the change.
+Setting `keepOpen` back to `false` remains deferred: the half-open timer 
attempts to close the circuit on
+its next tick (after `halfOpenAfter` milliseconds, default 30 s).

Reply via email to