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.
This commit is contained in:
David Snelling 2026-08-18 10:11:30 -07:00
parent 314e0e6c29
commit 0991cf28e4
2 changed files with 26 additions and 2 deletions

View file

@ -1998,11 +1998,18 @@ export class FileSystemStorage extends BaseStorage {
public override async assertWriterFenceHeld(): Promise<void> {
if (!this.writerLockInfo) return
const current = await this.readWriterLock()
// Ownership is PER-PROCESS: pid + hostname, deliberately NOT startedAt.
// The documented same-process re-open path ("warn and take over" — two
// instances in one Node process, the server-restart test pattern)
// rewrites the lock with a fresh startedAt; fencing the first instance
// on that mismatch latched its background flushes dead while its own
// process held the lock (caught by the plant's integration lane, twice).
// startedAt adds nothing against pid recycling either: a recycled pid's
// victim is a DEAD process — it runs no fence checks.
if (
current &&
current.pid === this.writerLockInfo.pid &&
current.hostname === this.writerLockInfo.hostname &&
current.startedAt === this.writerLockInfo.startedAt
current.hostname === this.writerLockInfo.hostname
) {
return
}