Skip to content

A Cross-Path Reservation Arbiter Must Be ONE Primitive Atomic on Every Path

Problem

A never-block contract forces a fallback: when the lock cannot be taken, the operation must still succeed. That produces two code paths which reserve the same resource. The tempting design gives each path the reservation mechanism most natural to it — the locked path writes the shared counter it already holds the lock for, the degraded path writes a marker file because it holds no lock.

Those two media are not atomic against each other, so the design is broken by construction. Worse, the failure is not probabilistic. The sole cause of a lock timeout is another claimant holding the lock right then, so the timeout condition simultaneously guarantees:

  1. a competing claimant exists, and
  2. the loser read the pre-write value of the shared counter.

The two paths do not merely sometimes overlap — the fallback is only ever entered at the exact moment the primary path is mid-claim. Contention is not the edge case; it is the trigger.

In SPEC-165 this shipped through a first review pass. That pass correctly found the degraded path reserved nothing at all and fixed it by adding an O_EXCL claim marker — but left the locked path reserving via the counter alone. A locked claimant and a timed-out claimant then returned the same ID deterministically. The one-directional fix looked complete because each path, examined alone, now reserved something.

Resolution

Pick the single primitive that is atomic across every path, and make it the arbiter for all of them. Everything else demotes to a hint.

For files, that primitive is O_EXCL creation: it is atomic regardless of whether the caller holds any lock, so it arbitrates between a lock-holder and a lock-timeout claimant equally. In SPEC-165 the counter became a fast floor hint and every allocation — locked and degraded alike — reserves by creating claim-<n> with O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW. The verdict is FileExistsError, not the in-memory "taken" set, which is only a syscall-saving hint.

Checklist:

  • Name the arbiter explicitly in the code, as a comment at its definition. The next reader's instinct is that the lock is what prevents collisions; it is not, on the path that has no lock.
  • Both paths call the same reservation function. Mirrored copies drift, and the drift is invisible until it produces a duplicate.
  • The fallback must never write the shared state the primary path owns. SPEC-165's degraded path deliberately does not write the counter: that would be an unlocked read-modify-write against the file the locked path writes, able to lower it and destroy a live reservation.
  • Widen any parse gate to cover values the arbiter itself can produce. If the chosen value is floor + 1, a gate sized to floor makes the boundary marker unreadable, so the arbiter cannot see its own reservation.

Prevention

The discriminating test is not "do concurrent claimants get distinct values" — that passes by accidental serialization. It is a negative control that suppresses the arbiter and asserts collisions DO occur. If duplicates do not appear with the arbiter disabled, the mechanism is not what the design claims it is, and the positive test is proving nothing.

SPEC-165 pins this with VT_SPEC_NO_MARKER, a test-only env var that suppresses the marker. --no-lock alone must not duplicate (O_EXCL still arbitrates without the lock — itself the proof the arbiter is the marker and not the lock); --no-lock plus VT_SPEC_NO_MARKER must. Pair that with a mixed-mode fixture that drives one claimant through each path at once, and document its statistical power honestly if it is a weak detector — SPEC-165's mixed-mode test failed to reproduce a duplicate against known-bad code in 20 rounds and says so in the gate record rather than being trusted.

A sibling of structural-test-assertions.md: in both, a guard that looks green is green for a reason unrelated to the property it claims to check.