shashank created CAMEL-26291:
--------------------------------

             Summary: camel-core - ThrottlingExceptionRoutePolicy counts the 
failures since the circuit last closed instead of those within failureWindow: 
old failures open the circuit, and a successful exchange in the half open state 
opens it again
                 Key: CAMEL-26291
                 URL: https://issues.apache.org/jira/browse/CAMEL-26291
             Project: Camel
          Issue Type: Bug
          Components: camel-core
            Reporter: shashank
            Assignee: shashank


{{ThrottlingExceptionRoutePolicy}} (camel-support) documents 
{{failureThreshold}} as "How many failed messages within the window would 
trigger the circuit breaker to open" and {{failureWindow}} as the "Sliding 
window for how long time to go back (in millis) when counting number of 
failures". It counts every failure since the circuit last closed (or since the 
route started) instead, and only checks that the *last* failure is within the 
window: {{failures.get() >= failureThreshold && lastFailure >= 
System.currentTimeMillis() - failureWindow}} (main a090e27eb02a, 
{{isThresholdExceeded}} line 304; the counter is only reset in 
{{closeCircuit}}, line 390).

Two consequences:

# *Old failures open the circuit.* With {{failureThreshold=2}} and 
{{failureWindow=100}}, a failure, then a second failure more than 100 ms later, 
opens the circuit and suspends the consumer, although only one failure is 
within the window. With the defaults (50 failures, 60 s), a route with an 
occasional failure is suspended on a single failure once it has collected 50 
since it started.
# *A successful exchange in the half open state opens the circuit again.* In 
the half open state ({{calculateState}}, line 262) the policy opens the circuit 
when the threshold is exceeded and closes it otherwise. The failures that 
opened the circuit are still counted, so as long as the last of them is within 
{{failureWindow}}, the successful trial exchange opens the circuit again (the 
consumer is suspended again). With the defaults ({{halfOpenAfter}} 30 s, 
{{failureWindow}} 60 s) a successful trial within 30 s of the first half open 
check therefore opens the circuit again: the route stays suspended for about 60 
s instead of 30 s, and with a larger window (say 10 minutes, retry every 30 s) 
until the failures leave the window, after the backend has recovered, while 
each check resumes the consumer until the first exchange is done. The existing 
half open tests all use a window (30 ms) shorter than {{halfOpenAfter}}, where 
the old failures have left the window.

Reproduced with a route {{from("direct:...").routePolicy(policy)}} and 
{{ProducerTemplate}} (latches, no sleeps): {{new 
ThrottlingExceptionRoutePolicy(1, 60000, 100, null)}}: a failure opens the 
circuit, the timer half opens it, a successful exchange: state "opened", 
consumer suspended. {{new ThrottlingExceptionRoutePolicy(2, 1000, 60000, 
null)}}: failure, wait until the window has passed, failure: state "opened".

*Fix*: the policy keeps the times of the last {{failureThreshold}} failures; 
the threshold is exceeded when the oldest of them is within {{failureWindow}} 
(a sliding window, as documented). In the half open state the exchange decides: 
a failure opens the circuit again, a success closes it. The half open check of 
the timer leaves a half open circuit to the next exchange: an exchange that was 
in flight when the circuit opened can be done when the check is due, find the 
circuit still open and half open it itself, and the timer's check must then not 
open it again because of the failure that opened it. The check of an open 
circuit and {{keepOpen}} are unchanged. The counters shown by JMX and the dev 
console ({{failures}}, {{success}}, {{lastFailure}}) keep their meaning. The 
failure times have their own lock, as the policy lock is held while the 
consumer is suspended or resumed. The class javadoc, which said the half open 
state checks the failure threshold, now says that the next exchange decides. 
Upgrade guide entry for 4.24. The existing tests that open the circuit with two 
failures used a 30 ms window; as both failures must now be within it, they use 
60 s.

*Tests*: new {{ThrottlingExceptionRoutePolicyFailureWindowTest}} (camel-core, 
latches): {{testSuccessInHalfOpenStateClosesTheCircuit}} and 
{{testFailuresOutsideTheWindowDoNotOpenTheCircuit}} fail on main and pass with 
the fix; the second also checks that two failures within the window still open 
the circuit. {{testHalfOpenTimerDoesNotReopenACircuitHalfOpenedByAnExchange}} 
holds the timer's check with a latch until the straggler has half opened the 
circuit; it fails on main and on the first version of the fix (state "opened") 
and passes with the fix. The other tests of the policy (camel-core, 
camel-management, camel-console) pass.

*How it was found*: first by reading the class against the documented meaning 
of {{failureThreshold}}/{{failureWindow}} while writing a driver for the Rocq 
JVM model (a JVM semantics in Rocq that executes the real class files). Its 
thread layer (threads, locks, atomics, a clock, {{java.util.Timer}}) now runs 
the production class with its own timer, and kernel-checked theorems on the 
class files of main a090e27eb02a state both consequences: 
{{main_failures_outside_window_open}} (threshold 2, window 100 ms: two failures 
201 ms apart open the circuit) and {{main_half_open_success_reopens}} 
(threshold 1, window 60 s, half open after 30 s: the successful trial opens the 
circuit again). The same theorems on the fix show the documented behaviour. A 
schedule search over the model then found the timer race above in the first 
version of the fix ({{fix_straggler_first_timer_reopens}}, a two-thread 
schedule replayed in the kernel); the fix handles it 
({{fix2_straggler_first_stays_half_open}}). Every model result was also checked 
on HotSpot with the same class files, and the race with the test above.

*Affected*: main, camel-4.22.x, camel-4.18.x and camel-4.14.x (same 
{{isThresholdExceeded}} and half open decision in {{calculateState}}; read, not 
run). Users see the change (upgrade guide), so main only is suggested; 4.18.x 
and 4.14.x do not have CAMEL-25014 ({{reopenCircuit}}).

*Related*: PR #28382 (CAMEL-25911, {{TimeSource}}) changes the same lines of 
{{onExchangeDone}} and {{isThresholdExceeded}} (small rebase for whichever is 
merged second); CAMEL-26104 (the half open timer through the 
ExecutorServiceManager) touches the timer task.

_Filed with Claude Code on behalf of allthingssecurity._



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to