Commit graph

2 commits

Author SHA1 Message Date
0991cf28e4 fix(locks): the fence keys ownership on pid+hostname — a same-process re-open never fences its predecessor
All checks were successful
CI / Node 24 (push) Successful in 12m23s
CI / Node 22 (push) Successful in 12m33s
CI / Integration + conformance (Node 22) (push) Successful in 18m25s
CI / Bun (latest) (push) Successful in 12m20s
The plant's integration lane caught it twice: the fence's startedAt-strict
comparison turned the documented same-process warn-and-take-over path (two
instances in one Node process — the server-restart test pattern, and the
shared-default-store pattern across test files) into a flush-killer: the
first instance's background flushes latched dead while its own process held
the lock ('PID N no longer holds the lock — it is now held by PID N').

Ownership is per-process: pid + hostname. startedAt stays in the lock for
observability but not in the fence — it protects nothing (a pid-recycled
successor's victim is a dead process that runs no fence checks) and it
convicted the innocent. Pinned: a same-process re-open leaves both
instances' flushes working; the cross-process eviction pins unchanged.

Verified under the lane's exact command: 102/102 files, 850 passed, exit 0.
2026-08-18 10:11:30 -07:00
292e7c0406 fix(locks): live writers are never auto-evicted; evicted writers are fenced at every commit barrier
Some checks failed
CI / Node 24 (push) Successful in 12m21s
CI / Node 22 (push) Successful in 12m32s
CI / Integration + conformance (Node 22) (push) Failing after 13m32s
CI / Bun (latest) (push) Successful in 12m21s
The production dev-store split-brain (two live writers alternating a store's
id-mapper between two internally-consistent truths), cured at all three of
its roots. (1) STALENESS REQUIRES PID-DEATH: the old rule evicted on
heartbeat age alone, so a >60s event-loop stall (debugger pause, GC, heavy
sync work) handed the lock to a second opener while the first kept writing;
a live process is now never auto-evicted — a wedged-but-alive holder is the
operator's call via {force:true}, and the heartbeat stays for observability.
(2) THE CLAIM IS ATOMIC: writeFile(wx)'s open→write→close left an empty-file
window a concurrent opener could read as torn, unlink a LIVE claim, and take
the lock; the claim is now tmp-write + hard-link — the lock appears with its
full contents in one step. (3) THE FENCE: every flush commit and transact
barrier verifies lock ownership first (one small read per window) — a
forced-out or lock-deleted writer fails typed (BRAINY_WRITER_FENCED) before
a single staged byte or manifest advance, instead of writing on unaware.

Pinned: live-with-ancient-heartbeat refuses typed; dead-PID self-clears
narrated; a forced-out writer's flush and transact both fence, advancing
nothing. Requested by a downstream team as single-writer guard or loud
lockout — this is both.
2026-08-17 16:26:41 -07:00