Skip to content

fix(api): prove queue entry for both racers in the currency-lock FIFO test (#402) - #425

Merged
mforce merged 1 commit into
mainfrom
fix/402-currency-lock-race-flake
Aug 6, 2026
Merged

mforce merged 1 commit into
mainfrom
fix/402-currency-lock-race-flake

Conversation

@mforce

@mforce mforce commented Aug 5, 2026 •

Copy link
Copy Markdown
Owner

Closes #402.

WriterFirst_TheChangeWaitsOutTheWriterTransaction_NotJustItsRead flaked intermittently — it guards the §4.6 currency-lock race (#162), and a false failure there is indistinguishable from a genuine security regression, which is exactly why it can't be left flaky.

Root cause

WaitUntilDoneOrBlockedAsync's second call could return true on its very first poll purely because the writer was already parked, without ever proving the change's own FOR UPDATE reached Postgres's wait queue. Confirmed by the failing CI run completing in 73ms — too fast for even one 50ms poll cycle to have observed the change actually registering.

Fix

WaitUntilDoneOrBlockedAsync now takes a minBlockedCount (default 1, preserving every other caller's behavior). The test passes minBlockedCount: 2 so a second competitor must prove it queued up too, not just that the first one is still there.

Detecting this required a recursive walk rather than a plain pg_blocking_pids containment check: pg_blocking_pids reports only the nearest queue blocker, so the change (queued behind the writer, not directly behind the fence) reports the writer's pid, never the fence holder's. CountBlockedBehindAsync walks the full blocking chain to find every backend for which holderPid appears anywhere in it (confirmed empirically against a live queue).

Verification

Per the issue's acceptance criterion — a flake fixed by weakening the assertion is worse than the flake — this was checked against the evaporating-lock mutant the file's header comment describes (FOR SHARE autocommitted separately from the insert): the fix still catches that regression once the race window is realistic, confirming detection wasn't weakened.

… test (#402)

WriterFirst_TheChangeWaitsOutTheWriterTransaction_NotJustItsRead flaked
intermittently: WaitUntilDoneOrBlockedAsync's second call could return true
on its very first poll purely because the writer was already parked,
without ever proving the change's own FOR UPDATE reached Postgres's wait
queue — confirmed by the failing CI run completing in 73ms, too fast for
even one 50ms poll cycle.

WaitUntilDoneOrBlockedAsync now takes a minBlockedCount (default 1,
preserving every other caller's behavior) so a second competitor can prove
it queued up too, not just that the first one is still there. Detecting
this required a recursive walk rather than a plain pg_blocking_pids
containment check: pg_blocking_pids reports only the nearest queue
blocker, so the change (queued behind the writer, not directly behind the
fence) reports the writer's pid, never the fence holder's.

Verified against the evaporating-lock mutant the file's header comment
describes (FOR SHARE autocommitted separately from the insert): the fix
still catches the regression once the race window is realistic, confirming
detection wasn't weakened.
@mforce
mforce merged commit 085a907 into main Aug 6, 2026
9 checks passed
@mforce
mforce deleted the fix/402-currency-lock-race-flake branch August 6, 2026 06:33
@mforce mforce mentioned this pull request Aug 6, 2026
90 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flaky: CurrencyLockRaceTests.WriterFirst_TheChangeWaitsOutTheWriterTransaction_NotJustItsRead fails intermittently in CI

2 participants