Skip to content

fix: every lock removal happens under a removal lock, so two stale breaks cannot both take the lock - #344

Merged
TheAmericanMaker merged 1 commit into
mainfrom
fix/stale-lock-break-race
Sep 14, 2026
Merged

TheAmericanMaker merged 1 commit into
mainfrom
fix/stale-lock-break-race

Conversation

@TheAmericanMaker

Copy link
Copy Markdown
Member

Summary

Closes #342. acquireLock broke a lock older than STALE_LOCK_MS with stat → rm. Two waiters that both saw the stale file both removed it; the second rm landed after the first waiter had re-created the file, so both held the lock. Every waiter polls on the same 125 ms cadence, so "two waiters see it stale in the same tick" is the normal case, not a corner.

Design (the issue's rename-then-rm idea does not close it — a rename takes whatever inode is at the path at that moment, a fresh lock included):

  • All removals are serialized under a removal lock, <lock>.break (O_EXCL, held across one stat/read and one rm).
  • Each removal re-verifies under it: a stale break re-stats the age; a release re-reads its token.
  • A lock file can only be created while the path is free (wx), so nothing can be created between a remover's check and its rm, and only removal-lock holders remove: what was verified is what is removed. This also closes the narrower race where the original holder's late release and a stale break overlapped.
  • A removal lock abandoned by a crashed process is cleared after BREAK_LOCK_STALE_MS (= LOCK_TIMEOUT_MS, 5 s; it is held for milliseconds). A release that cannot take the removal lock within LOCK_TIMEOUT_MS falls back to the token-checked removal alone — the guarantee before this change, never less.

acquireLock's API and the lock file format are unchanged; BREAK_LOCK_STALE_MS is exported beside the other constants.

Test plan

🤖 Generated with Claude Code

…eaks cannot both take the lock

acquireLock broke a stale lock with an unconditional rm after a stat. Two
waiters that both saw the stale file both removed it, and the second rm
landed after the first waiter had re-created the file: both held the lock
(#342, found by a Broad-Side defect run on this repository, confirmed by
verify). Removals — stale breaks and releases — now run under `<lock>.break`
and re-check what they remove there; since a lock file can only be created
while the path is free, what a remover verified is what it removes. An
abandoned removal lock is cleared after BREAK_LOCK_STALE_MS; a release that
cannot take it in time falls back to the token-checked removal alone.

Closes #342

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@TheAmericanMaker
TheAmericanMaker merged commit c2fe6b6 into main Sep 14, 2026
5 checks passed
@TheAmericanMaker
TheAmericanMaker deleted the fix/stale-lock-break-race branch September 14, 2026 04:27
TheAmericanMaker added a commit that referenced this pull request Sep 16, 2026
One shared lock file, broken by removing and re-creating its path, is a
race between any two waiters that saw it stale (#344 closed one level of
it and left the next), and a fixed 60 s age broke live holders — a publish
across a full reindex reopened the version race #240 fixed. Each waiter now
owns a ticket beside the lock, refreshed on a timer while it waits and
holds; the holder is the first ticket whose owner is alive and refreshing;
a dead owner's ticket is removed at once and a hung one's after
STALE_LOCK_MS, each by its own unique name. Lamport's doorway keeps a
waiter from being overtaken between taking its number and writing it. A
pre-existing plain lock file is honoured while fresh.

Closes #355

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TheAmericanMaker added a commit that referenced this pull request Sep 16, 2026
One shared lock file, broken by removing and re-creating its path, is a
race between any two waiters that saw it stale (#344 closed one level of
it and left the next), and a fixed 60 s age broke live holders — a publish
across a full reindex reopened the version race #240 fixed. Each waiter now
owns a ticket beside the lock, refreshed on a timer while it waits and
holds; the holder is the first ticket whose owner is alive and refreshing;
a dead owner's ticket is removed at once and a hung one's after
STALE_LOCK_MS, each by its own unique name. Lamport's doorway keeps a
waiter from being overtaken between taking its number and writing it. A
pre-existing plain lock file is honoured while fresh.

Closes #355

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

acquireLock: two waiters breaking the same stale lock can both take it

1 participant