fix(locks): the fence keys ownership on pid+hostname — a same-process re-open never fences its predecessor
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:
parent
314e0e6c29
commit
0991cf28e4
2 changed files with 26 additions and 2 deletions
|
|
@ -1998,11 +1998,18 @@ export class FileSystemStorage extends BaseStorage {
|
||||||
public override async assertWriterFenceHeld(): Promise<void> {
|
public override async assertWriterFenceHeld(): Promise<void> {
|
||||||
if (!this.writerLockInfo) return
|
if (!this.writerLockInfo) return
|
||||||
const current = await this.readWriterLock()
|
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 (
|
if (
|
||||||
current &&
|
current &&
|
||||||
current.pid === this.writerLockInfo.pid &&
|
current.pid === this.writerLockInfo.pid &&
|
||||||
current.hostname === this.writerLockInfo.hostname &&
|
current.hostname === this.writerLockInfo.hostname
|
||||||
current.startedAt === this.writerLockInfo.startedAt
|
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,23 @@ describe('writer-lock fencing', () => {
|
||||||
expect(await brain.get(id)).not.toBeNull()
|
expect(await brain.get(id)).not.toBeNull()
|
||||||
}, 120000)
|
}, 120000)
|
||||||
|
|
||||||
|
it('the fence does NOT fire on a same-process re-open — the documented warn-and-take-over contract stays benign', async () => {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), 'brainy-fence-samepid-'))
|
||||||
|
dirs.push(dir)
|
||||||
|
const first = await fsBrain(dir)
|
||||||
|
await first.add({ data: 'first instance write', type: NounType.Document, metadata: { n: 1 } })
|
||||||
|
|
||||||
|
// A second instance in the SAME process takes the lock over (fresh
|
||||||
|
// startedAt) — the pattern server-restart tests use. The first
|
||||||
|
// instance's background flushes must keep working: same pid + same
|
||||||
|
// hostname IS ownership. (The plant's integration lane caught the
|
||||||
|
// startedAt-strict fence latching exactly this shape dead.)
|
||||||
|
const second = await fsBrain(dir)
|
||||||
|
await second.add({ data: 'second instance write', type: NounType.Document, metadata: { n: 2 } })
|
||||||
|
await expect(first.flush()).resolves.toBeUndefined()
|
||||||
|
await expect(second.flush()).resolves.toBeUndefined()
|
||||||
|
}, 120000)
|
||||||
|
|
||||||
it('THE FENCE: a forced-out writer fails its next flush typed and advances nothing', async () => {
|
it('THE FENCE: a forced-out writer fails its next flush typed and advances nothing', async () => {
|
||||||
const dir = mkdtempSync(join(tmpdir(), 'brainy-fence-evict-'))
|
const dir = mkdtempSync(join(tmpdir(), 'brainy-fence-evict-'))
|
||||||
dirs.push(dir)
|
dirs.push(dir)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue