feat(log): the guarded log-authority core — group-commit durable-at-ack, the per-brain switch, the verification oracle
The storage-authority adoption path, guarded shape: the canonical tree
stays authoritative by default ('tree'); a brain flips to 'log' only
through the verification oracle, and the flip is stored, per-brain,
checked at open only.
- FactLog.ensureSynced(): classic group commit — concurrent writers
append, then join ONE covering fsync (running + queued slots give the
covering guarantee: the sync a caller awaits always starts after its
append landed). Solo writer = immediate sync.
- GenerationStore.logDurability 'deferred' (default, byte-identical to
today: fact durability rides the group-commit flush, ack latency
unchanged) | 'at-ack' (log-authority mode: every single-op ack awaits a
covering log fsync — an acked write's fact survives power loss, by
contract). transact() was already durable-at-return in both modes.
- src/db/logAuthority.ts: the stored switch artifact
(_system/log-authority.json, absent = tree), readLogAuthority, and the
VERIFICATION ORACLE — replay the fact log, fold latest state per id
(digests, never bodies — memory-bounded), diff against the canonical
tree paged; verdict green iff every canonical row is exactly reproduced
AND the log claims nothing canonical denies. Divergences are NAMED by
class (pre-log-record → needs baseline backfill; state-differs;
log-live-canonical-absent; log-tombstone-canonical-present). The flip
REFUSES on red with the first divergence and the cure in the message.
- Brainy: authority read at open (log → durable-at-ack enabled);
logAuthority() / verifyLogAuthority() / adoptLogAuthority() public API.
Nothing flips by itself; nothing changes for existing brains.
This commit is contained in:
parent
9fda6d9566
commit
6595309765
4 changed files with 409 additions and 3 deletions
|
|
@ -442,6 +442,50 @@ export class FactLog {
|
|||
await this.storage.syncRawObjects(paths)
|
||||
}
|
||||
|
||||
// --- GROUP COMMIT ON THE LOG (durable-at-ack mode) ------------------------
|
||||
// Classic group commit: concurrent writers append, then join ONE fsync
|
||||
// whose completion releases every covered ack. Two slots — the running
|
||||
// sync and at most one queued behind it — give the covering guarantee:
|
||||
// an append followed by ensureSynced() is always covered, because the
|
||||
// sync it awaits STARTS after the append landed (a running sync that
|
||||
// may have snapshotted earlier is never joined; the queued one is).
|
||||
private syncRunning: Promise<void> | null = null
|
||||
private syncQueued: Promise<void> | null = null
|
||||
|
||||
/**
|
||||
* Await a sync that covers every byte appended before this call. Many
|
||||
* concurrent callers share one fsync (solo caller = immediate sync). The
|
||||
* durability contract of an acked write in log-durable mode: this promise
|
||||
* resolving means the caller's frames survive power loss.
|
||||
*/
|
||||
async ensureSynced(): Promise<void> {
|
||||
if (this.syncQueued) {
|
||||
// A sync that has NOT started yet exists — it will snapshot after our
|
||||
// append, so it covers us.
|
||||
return this.syncQueued
|
||||
}
|
||||
if (this.syncRunning) {
|
||||
// The running sync may have snapshotted before our append — queue the
|
||||
// next one behind it and join that.
|
||||
const queued = this.syncRunning
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
// Promote: the queued sync becomes the running one.
|
||||
this.syncQueued = null
|
||||
this.syncRunning = this.sync().finally(() => {
|
||||
this.syncRunning = null
|
||||
})
|
||||
return this.syncRunning
|
||||
})
|
||||
this.syncQueued = queued
|
||||
return queued
|
||||
}
|
||||
this.syncRunning = this.sync().finally(() => {
|
||||
this.syncRunning = null
|
||||
})
|
||||
return this.syncRunning
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a scan over committed facts. The scan runs against a MANIFEST
|
||||
* SNAPSHOT (sealed segments + the tail's decoded facts at open) — exactly-
|
||||
|
|
|
|||
Reference in a new issue