[ 
https://issues.apache.org/jira/browse/CAMEL-25300?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Claus Ibsen resolved CAMEL-25300.
---------------------------------
    Resolution: Fixed

> Throttle EIP - the clean of the total requests throttler can hand out a 
> second set of permits, so more exchanges than allowed pass in a time period
> ---------------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: CAMEL-25300
>                 URL: https://issues.apache.org/jira/browse/CAMEL-25300
>             Project: Camel
>          Issue Type: Bug
>          Components: camel-core
>            Reporter: shashank
>            Assignee: shashank
>            Priority: Minor
>             Fix For: 4.23.0
>
>
> {{TotalRequestsThrottler}} keeps a {{ThrottlingState}} (a {{DelayQueue}} of 
> permits) per correlation key. Every returned permit schedules {{clean()}} 10 
> periods later ({{cleanPeriodMillis = timePeriodMillis * 10}}, computed once 
> in the constructor), which does {{states.remove(key)}}.
> * An exchange that looked up the state just before the clean runs still polls 
> a permit from the removed state, while the next exchanges create a new state 
> with all {{maximumRequests}} permits available at once. So in that period 
> more exchanges than allowed pass (with 2 per period: 3).
> * The permit of that exchange is returned to the removed state, which 
> schedules the removed state's clean again. 10 periods later that clean does 
> {{states.remove(key)}}, which removes whatever state is mapped then, also the 
> new one while it is in use: its delayed permits are dropped, the next 
> exchange gets a fresh state with all permits, and the clean of the dropped 
> state repeats this. Under continuous traffic with concurrent senders the 
> throttler can keep resetting every 10 periods.
> * {{setTimePeriodMillis}} (JMX) does not change the clean period. After 
> increasing the period to more than 10 times its initial value, the clean 
> removes states whose permits are still delayed, and the next exchanges get 
> new permits at once.
> CAMEL-24926 fixed the same pattern in {{ConcurrentRequestsThrottler}}: its 
> clean only removes its own state when no permit is out, and an exchange that 
> took a permit from a removed state takes it from the current state instead.
> h3. Reproduction
> Test {{TotalRequestsThrottlerCleanTest}} (camel-core, the same technique as 
> {{ConcurrentRequestsThrottlerCleanTest}} from CAMEL-24926: an executor that 
> keeps the scheduled clean tasks, and the {{maximumRequests}} expression, 
> which the throttler evaluates after it looked up the state, runs them for a 
> given exchange). Throttle 2 per hour, {{rejectExecution(true)}}:
> * {{testCleanAfterExchangeLookedUpState}}: "first", then "race" (the clean 
> runs after it looked up the state, as if 10 periods had passed), then 
> "second" must pass and the next exchange must be rejected. On main it passes 
> too (3 exchanges in the period after the clean):
> {noformat}
> TotalRequestsThrottlerCleanTest.testCleanAfterExchangeLookedUpState:64 
> Expected org.apache.camel.CamelExecutionException to be thrown, but nothing 
> was thrown.
> {noformat}
> * {{testCleanFollowsTimePeriod}}: after {{setTimePeriodMillis(2 hours)}} the 
> clean is scheduled in 20 hours. On main:
> {noformat}
> TotalRequestsThrottlerCleanTest.testCleanFollowsTimePeriod:94 expected: 
> <[72000000]> but was: <[36000000]>
> {noformat}
> * {{testCleanRemovesUnusedState}} (control): the clean still removes an 
> unused state (passes on main).
> The defect was found with a TLA+ model of the throttler states, the clean 
> tasks, exchanges (lookup, poll, enqueue as separate steps) and time periods: 
> "at most maximumRequests permits are handed out per period" is violated in 12 
> steps (an exchange looks up the state, the clean removes it, the exchange 
> takes a permit, another exchange creates a new state and takes a permit), and 
> "the clean of a replaced state never removes the current state" in 13 steps. 
> With the fix below both hold (2-3 exchanges, 1-2 permits, 2-4 periods).
> h3. Proposed fix
> As in CAMEL-24926:
> * {{clean()}}: {{states.computeIfPresent(key, (k, s) -> s == this && 
> markRemovedIfUnused() ? null : s)}}; a state is unused when all its permits 
> are back in the queue (checked and marked {{removed}} under a lock of its 
> own).
> * Unlike CAMEL-24926, that lock is not the state's lock: in this mode a 
> decrease of the maximum requests (a dynamic {{maximumRequests}} expression, 
> such as a header) takes permits with {{DelayQueue.take()}} while it holds the 
> state's lock, and waits when none is available. An exchange that took a 
> permit must be able to check the flag and return its permit meanwhile, 
> otherwise the decrease and every exchange of the key wait forever. (The 
> concurrent requests throttler decreases with {{Semaphore.reducePermits}}, 
> which does not wait.)
> * An exchange that polled (or took) a permit from a state that is marked 
> removed puts the permit back and takes one from the current state.
> * The clean is scheduled with the current time period ({{10 x 
> getTimePeriodMillis()}}, saturated). This belongs to the same fix: "unused" 
> counts the permits in the queue, delayed or not, so a clean must not run 
> while a permit is still delayed; with the period fixed at the initial value 
> it could, after the period was increased.
> {{TotalRequestsThrottlerRateDecreaseTest}} (regression test for the lock 
> above): {{throttle(header("max")).timePeriodMillis(2000)}}; two exchanges 
> take both permits, two more wait in {{take()}} (the test waits until their 
> threads are parked), then an exchange with {{max=1}} decreases the maximum 
> and waits for a permit to discard. All five must get through. It passes on 
> main; with the first version of the fix (the flag under the state's lock) it 
> hung: the thread dump shows the two exchanges parked in {{isRemoved()}} on 
> the state's lock and the decreasing one in {{DelayQueue.take()}} inside 
> {{calculateAndSetMaxRequestsPerPeriod}}.
> With the fix the new tests and the throttle tests 
> ({{org/apache/camel/processor/throttle/**}}, {{Throttl*Test}}: 60 tests) pass.
> Severity: the lookup race needs an exchange at the instant the clean runs 
> (after 10 idle periods); once it happened, the reset can repeat under 
> continuous traffic; the JMX case is deterministic. Minor.
> Affected: 4.14.x, 4.18.x and main (same code).
> Duplicate check (2026-10-03): JIRA text "TotalRequestsThrottler" (CAMEL-24928 
> asyncDelayed NPE, CAMEL-24227 volatile JMX fields), summary 
> "throttle"/"throttler" since 2023 (CAMEL-24926, CAMEL-20357, CAMEL-20267, 
> ...: none about the clean of the total requests mode). GitHub pull requests 
> "TotalRequestsThrottler", "throttler clean": #26775 (CAMEL-24928), #24985, 
> #24713; the open #26871 touches {{ConcurrentRequestsThrottler}} interrupt 
> handling only.
> _Filed with Claude Code on behalf of allthingssecurity._



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to