fix: race-proof writer-lock acquisition + machine-readable conflict through init

- acquireWriterLock now CLAIMS with an atomic create-exclusive write (O_EXCL)
  inside a bounded retry loop: two processes racing an absent lock can never
  both succeed (the old read-then-tmp-rename flow let the loser keep running
  unlocked, silently). An EEXIST loser re-evaluates and either throws loudly
  with the winner's details or performs a verified stale-takeover (re-read
  before unlink so a lock that changed hands mid-deliberation is never
  clobbered). Exhausted contention fails loudly instead of degrading into a
  lockless open.
- BRAINY_WRITER_LOCKED passes through init() unwrapped: the error documents a
  machine-readable contract (err.code + err.lockInfo with the holder's
  pid/host/heartbeat), but init's blanket error wrapping stripped both,
  leaving consumers a message to regex against.
- Two contract tests added: stale-foreign takeover installs OUR lock via the
  atomic claim; the conflict error carries code + lockInfo at the public
  init() surface.
This commit is contained in:
David Snelling 2026-07-17 17:11:46 -07:00
parent e450e0eedf
commit 01a3b46ade
4 changed files with 188 additions and 49 deletions

View file

@ -10,6 +10,26 @@ Full auto-generated changelog: `CHANGELOG.md` · Releases: https://github.com/so
---
## v8.7.1 — 2026-07-17 (writer-lock acquisition is race-proof + machine-readable through init)
Two hardenings of the multi-process writer lock (the `locks/_writer.lock` lease that makes a
second writer on the same brain directory fail loudly):
- **Lock acquisition claims atomically.** The acquire path used read-then-write, leaving a
window where two processes racing an *absent* lock could both "succeed" — and the loser
kept running unlocked, silently. The claim is now an atomic create-exclusive write
(`O_EXCL`): exactly one racer wins; the loser re-evaluates and either fails loudly with
the winner's details or performs a verified stale-takeover. Bounded retries; contention
beyond them fails loudly rather than degrading into a lockless open.
- **`BRAINY_WRITER_LOCKED` survives `init()`.** The conflict error documents a
machine-readable contract (`err.code`, `err.lockInfo` with the holder's pid/host/
heartbeat), but init's error wrapping silently stripped both, leaving consumers a message
to regex against. The error now passes through unwrapped.
Measured while verifying (for operators sizing audits): `brain.auditGraph()` at a
production-consumer scale of ~2,600 relationships / 800 entities costs ~0.1 s warm and
~0.5 s cold, with exact scar counting across reopen.
## v8.7.0 — 2026-07-17 (bulk-transact ergonomics: scaled budgets + timeout telemetry)
The bulk-import ergonomics release, from a consumer's measured production incident (a serial