fix(bulkhead): release permits granted to abandoned waiters - #400
Open
Empatixx wants to merge 1 commit into
Open
fix(bulkhead): release permits granted to abandoned waiters#400Empatixx wants to merge 1 commit into
Empatixx wants to merge 1 commit into
Conversation
tryAcquirePermit(Duration) enqueues a waiter via acquirePermitAsync() but, on timeout, returns false without removing it from the queue. The next releasePermit() polls that abandoned waiter, decrements permits and completes a future nobody reads, so the permit is destroyed rather than returned. Capacity therefore ratchets downwards and never recovers. When more callers time out than there are permits, the bulkhead drops to zero in a single burst and every subsequent acquisition fails for the lifetime of the instance. Completing the abandoned waiter removes it from the queue (FutureLinkedList unlinks on completion). If completing fails, releasePermit() had already granted it a permit, which is then released so it is not lost. The same leak applied to InterruptedException in both acquirePermit() and tryAcquirePermit(Duration). Fixes failsafe-lib#393
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bulkhead#tryAcquirePermit(Duration)permanently destroys a permit every time it gives up. Once enough acquisitions have timed out, the bulkhead reaches zero capacity and never recovers for the lifetime of the instance — every subsequent acquisition fails even when nothing is executing.Fixes #393.
Root cause
tryAcquirePermit(Duration)enqueues a waiter throughacquirePermitAsync(), but on timeout it just returns:The abandoned waiter is still first in the queue, so the next
releasePermit()hands it the permit:Nobody will ever call
releasePermit()for that grant, so the permit is gone for good.Reproducer
No test framework — just
failsafe-3.3.2.jaron the classpath. Eight concurrent callers time out against a bulkhead of capacity 8:On 3.3.2 and on current
master:With this patch it stays at
8every round.A single burst is enough — the capacity does not decay gradually, it collapses.
Why this is critical
A
Bulkheadis normally built once and reused for the life of the process. Once wedged it cannot be recovered through the public API:releasePermit()only feeds the next zombie, and there is no reset. The only remedy is to recreate the bulkhead, which callers have no way of knowing they need to do.The failure is also silent and delayed: it is triggered by a brief overload spike, but it manifests afterwards, when load is back to normal and nothing is in flight. The symptom is a component that permanently rejects all work while looking idle.
We hit this in production. One traffic burst briefly pushed concurrent requests past the configured 64 permits; from that point on every request through that bulkhead failed at the acquire timeout, with zero threads actually executing, until the process was restarted.
Fix
Complete the abandoned waiter, which removes it from the queue (
FutureLinkedListunlinks on completion). If completing fails,releasePermit()had already granted it a permit, so release it back rather than lose it — this closes the race between the timeout and a concurrent release.The same leak applied to
InterruptedException, in bothacquirePermit()andtryAcquirePermit(Duration); both are handled.Tests
Added
BulkheadImplTestwith two cases. Againstmasterthey fail:With the fix they pass, and the full
coresuite is green (308 tests, 0 failures).