fix(close): a read-only brain writes no clean-shutdown evidence — the marker is the writer's word about itself
Some checks are pending
CI / Node 22 (push) Waiting to run
CI / Node 24 (push) Waiting to run
CI / Integration + conformance (Node 22) (push) Waiting to run
CI / Bun (latest) (push) Waiting to run
Delta Gate / Delta gate — candidate vs control (push) Waiting to run
Some checks are pending
CI / Node 22 (push) Waiting to run
CI / Node 24 (push) Waiting to run
CI / Integration + conformance (Node 22) (push) Waiting to run
CI / Bun (latest) (push) Waiting to run
Delta Gate / Delta gate — candidate vs control (push) Waiting to run
This commit is contained in:
parent
a79db434ac
commit
367ca721a5
3 changed files with 280 additions and 4 deletions
|
|
@ -20486,9 +20486,22 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
await this._aggregationIndex.flush()
|
||||
}
|
||||
})(),
|
||||
// 8.0 MVCC: detach the generation-bump hook and persist the counter
|
||||
// 8.0 MVCC: detach the generation-bump hook and persist the counter.
|
||||
// READ-ONLY GUARD: a reader's open() never sets the bump hook, never
|
||||
// buffers pending single-ops, and — since generationStore.open() also
|
||||
// leaves the clean-shutdown marker untouched for a reader — never
|
||||
// consumes it either, so there is nothing of a writer's to persist or
|
||||
// release here. Calling close() anyway would still WRITE: it
|
||||
// unconditionally re-stamps `_system/clean-shutdown.json` (and can
|
||||
// advance the fold checkpoint / counter files) at the generation this
|
||||
// session merely observed — a reader vouching for a commit it never
|
||||
// made. The marker is the writer's own evidence about the writer's own
|
||||
// process; a read-only brain must leave `_system/` exactly as it found
|
||||
// it. (Mirrors the same guard already applied to every other Phase-1
|
||||
// step below, and to the signal-path shutdown in
|
||||
// registerShutdownHooks().)
|
||||
(async () => {
|
||||
if (this.generationStore) {
|
||||
if (this.generationStore && !this.isReadOnly) {
|
||||
await this.generationStore.close()
|
||||
}
|
||||
})()
|
||||
|
|
|
|||
|
|
@ -805,7 +805,16 @@ export class GenerationStore {
|
|||
if (uncleanOpen) await this.advanceFoldCheckpointUnlocked()
|
||||
// The marker is consumed: any session that can write invalidates it
|
||||
// at first commit (see the commit paths); a clean close re-writes it.
|
||||
await this.clearCleanShutdownMarker()
|
||||
// A READER NEVER CONSUMES IT. The marker is the writer's own evidence
|
||||
// about the writer's own process — clearing it here exists so that
|
||||
// if THIS session goes on to write and then dies before its next
|
||||
// clean close, the marker's absence correctly reads as unclean. A
|
||||
// reader can never write, so it can never leave the store in a state
|
||||
// its own crash would mis-describe; clearing the marker for it would
|
||||
// only cost the store's actual writer a needless whole-log fold on
|
||||
// its next open, for a generation the reader merely observed. Leave
|
||||
// `_system/` exactly as found.
|
||||
if (!options?.readOnly) await this.clearCleanShutdownMarker()
|
||||
}
|
||||
await this.factLog.open(this.committed)
|
||||
} else {
|
||||
|
|
@ -895,7 +904,11 @@ export class GenerationStore {
|
|||
}
|
||||
}
|
||||
|
||||
/** Consume the clean-shutdown marker (every open; a clean close re-writes it). */
|
||||
/**
|
||||
* Consume the clean-shutdown marker (every WRITER open; a clean close
|
||||
* re-writes it). Callers must gate this on `!options.readOnly` — a reader
|
||||
* never consumes the marker, see the call site in {@link open}.
|
||||
*/
|
||||
private async clearCleanShutdownMarker(): Promise<void> {
|
||||
try {
|
||||
await this.storage.deleteRawObject(CLEAN_SHUTDOWN_PATH)
|
||||
|
|
|
|||
Reference in a new issue