feat(log): log authority is the fleet default — adopt-at-open, oracle-gated; plus the power-cut throw-site cures and the loud torn-record contract
THE DEFAULT FLIP (ruled on proven evidence — at-ack survived 301/301 acked-writes-through-power-cut in block-layer fault injection; deferred tree authority demonstrably loses flush-covered acks): a brain with NO stored authority artifact now ADOPTS LOG AUTHORITY AT OPEN. The oracle gates the flip exactly as the guarded adoption path always did — curable divergences baseline-backfilled, the flip lands ONLY on a green verdict — and a brain that cannot verify STAYS tree-authoritative loudly, with the refusal recorded on the switch artifact so subsequent opens are cheap. config logAuthority: 'defer' is the explicit documented opt-out (no automatic adoption; declared flush-window loss; adoptLogAuthority() flips later). A stored artifact always wins. RELEASES.md carries the posture. Two standing .fails debt pins FLIP TO HOLDING under the default: the at-ack crash-survival gap and the ack-at-log durability target — both now permanent asserted truths, not aspirations. POWER-CUT THROW SITES (fault-injection findings, brainy-alone config): - A manifest-listed-but-unloadable column segment QUARANTINES at discovery (loud once, counted always, quarantinedSegments() exposed for the heal) and the field serves its remaining segments DEGRADED — never a raw throw killing every query on the field. Real storage faults still propagate untouched. - Torn generation artifacts (NaN/garbage in manifest or counter) DISCARD with narration at the store's open and recovery re-derives — plus a defensive finite-integer guard at the init consumer. Never a RangeError killing an open. THE LOUD TORN-RECORD CONTRACT: an existing-but-unparseable stored record now surfaces as a typed, counted TornRecordError on every entity-read surface (including fifteen previously-blind per-item batch catches); ENOENT stays clean-absent; artifact readers with designed absent-recovery keep null-tolerance behind the loud floor. Disk corruption can no longer read as silent data invisibility. Suite migration: the default's pins inverted deliberately, generation baselines made relative, quarantine-contract pins rewritten to the ruled behavior. Gates: tsc 0 · unit 2065/2065 (159 files) · integration 826 (93 files) · conformance 31/31 · kill-matrix 15/15 · torn-open guards 2/2.
This commit is contained in:
parent
67c606be69
commit
214c98b4d5
23 changed files with 833 additions and 154 deletions
|
|
@ -96,11 +96,15 @@ describe('8.0 Db API — generational MVCC', () => {
|
|||
}
|
||||
|
||||
/** Open (and track) a filesystem brain rooted at a fresh temp directory. */
|
||||
async function openFsBrain(dir?: string): Promise<{ brain: Brainy; dir: string }> {
|
||||
async function openFsBrain(
|
||||
dir?: string,
|
||||
logAuthority?: 'adopt' | 'defer'
|
||||
): Promise<{ brain: Brainy; dir: string }> {
|
||||
const rootDirectory = dir ?? makeTempDir()
|
||||
const brain = new Brainy({
|
||||
requireSubtype: false,
|
||||
storage: { type: 'filesystem', path: rootDirectory }
|
||||
storage: { type: 'filesystem', path: rootDirectory },
|
||||
...(logAuthority ? { logAuthority } : {})
|
||||
})
|
||||
await brain.init()
|
||||
brains.push(brain)
|
||||
|
|
@ -647,7 +651,13 @@ describe('8.0 Db API — generational MVCC', () => {
|
|||
// ==========================================================================
|
||||
it('proof 8 — a crash before the manifest rename recovers to the exact pre-transaction state', async () => {
|
||||
const dir = makeTempDir()
|
||||
const { brain: first } = await openFsBrain(dir)
|
||||
// 'defer' (tree authority): this proof pins the TREE commit-point
|
||||
// contract — the manifest rename is the commit, so a crash before it
|
||||
// rolls back. Under the adopt-at-open default (log authority) the same
|
||||
// crash point legitimately REPLAYS the fsynced fact at reopen and the
|
||||
// transaction lands — that contract is pinned in the durability kill
|
||||
// matrix's at-ack rows, not here.
|
||||
const { brain: first } = await openFsBrain(dir, 'defer')
|
||||
|
||||
await first.transact([
|
||||
{
|
||||
|
|
@ -689,9 +699,10 @@ describe('8.0 Db API — generational MVCC', () => {
|
|||
// the realistic worst case for the recovery path.
|
||||
await first.close()
|
||||
|
||||
// Reopen: recovery rolls the uncommitted generation back and rebuilds
|
||||
// the indexes from the repaired records.
|
||||
const { brain: second } = await openFsBrain(dir)
|
||||
// Reopen ('defer' again — a reopen under the adopt default would adopt
|
||||
// and change the recovery path): recovery rolls the uncommitted
|
||||
// generation back and rebuilds the indexes from the repaired records.
|
||||
const { brain: second } = await openFsBrain(dir, 'defer')
|
||||
const recovered = await second.get(uid('crash-e'))
|
||||
expect((recovered?.metadata as { v: number }).v).toBe(1)
|
||||
expect(await second.get(uid('crash-new'))).toBeNull()
|
||||
|
|
@ -1162,13 +1173,16 @@ describe('8.0 Db API — generational MVCC', () => {
|
|||
const brain = await openMemoryBrain()
|
||||
|
||||
// Model-B: a single-op write is its OWN generation and IS logged (no meta —
|
||||
// tx metadata is a transact()-only concept). It is generation 1 on a fresh
|
||||
// brain (init-time infrastructure writes are the un-versioned gen-0 baseline).
|
||||
// tx metadata is a transact()-only concept). Relative baseline: under the
|
||||
// adopt-at-open fleet default the open-time baseline backfill is itself a
|
||||
// logged single-op generation, so the log is not empty on a fresh brain —
|
||||
// every pin below is expressed against that baseline.
|
||||
const baseGens = (await brain.transactionLog()).map((entry) => entry.generation)
|
||||
await brain.add({ id: uid('txlog-solo'), type: NounType.Document, data: 'solo', vector: vec(99), subtype: 'note' })
|
||||
const soloLog = await brain.transactionLog()
|
||||
expect(soloLog.map((entry) => entry.generation)).toEqual([1])
|
||||
const soloGen = brain.generation()
|
||||
expect(soloLog.map((entry) => entry.generation)).toEqual([soloGen, ...baseGens])
|
||||
expect(soloLog[0].meta).toBeUndefined()
|
||||
const soloGen = 1
|
||||
|
||||
const first = await brain.transact(
|
||||
[{ op: 'add', id: uid('txlog-a'), type: NounType.Document, data: 'a', vector: vec(100), metadata: {} }],
|
||||
|
|
@ -1181,12 +1195,14 @@ describe('8.0 Db API — generational MVCC', () => {
|
|||
const third = await brain.transact([{ op: 'update', id: uid('txlog-a'), metadata: { v: 3 } }])
|
||||
|
||||
const entries = await brain.transactionLog()
|
||||
// Newest first: the three transacts, then the single-op solo write (gen 1).
|
||||
// Newest first: the three transacts, then the single-op solo write, then
|
||||
// whatever the open baseline logged (the adopt-at-open backfill).
|
||||
expect(entries.map((entry) => entry.generation)).toEqual([
|
||||
third.generation,
|
||||
second.generation,
|
||||
first.generation,
|
||||
soloGen
|
||||
soloGen,
|
||||
...baseGens
|
||||
])
|
||||
expect(entries[1].meta).toEqual({ author: 'job-2' })
|
||||
expect(entries[2].meta).toEqual({ author: 'job-1' })
|
||||
|
|
@ -1238,21 +1254,24 @@ describe('8.0 Db API — generational MVCC', () => {
|
|||
const brain = await openMemoryBrain()
|
||||
const a = uid('ov-a')
|
||||
const b = uid('ov-b')
|
||||
await (
|
||||
await brain.transact([
|
||||
{ op: 'add', id: a, type: NounType.Document, data: 'a', vector: vec(1), metadata: { v: 1 } },
|
||||
{ op: 'add', id: b, type: NounType.Document, data: 'b', vector: vec(2), metadata: { v: 1 } }
|
||||
])
|
||||
).release()
|
||||
const at1 = await brain.asOf(1)
|
||||
// Pin RELATIVELY at the transact's own generation (not an absolute 1 —
|
||||
// the adopt-at-open baseline backfill owns the first generation).
|
||||
const tx = await brain.transact([
|
||||
{ op: 'add', id: a, type: NounType.Document, data: 'a', vector: vec(1), metadata: { v: 1 } },
|
||||
{ op: 'add', id: b, type: NounType.Document, data: 'b', vector: vec(2), metadata: { v: 1 } }
|
||||
])
|
||||
const txGen = tx.generation
|
||||
await tx.release()
|
||||
const at1 = await brain.asOf(txGen)
|
||||
|
||||
// A single-op REMOVE of `b` lands AFTER the pin and is NOT flushed (pending).
|
||||
await brain.remove(b)
|
||||
|
||||
const liveIds = (await brain.find({})).map((r) => r.id)
|
||||
const pastIds = (await at1.find({})).map((r) => r.id)
|
||||
// Live: `b` is gone. Historical (pinned at gen 1): the un-flushed removal is
|
||||
// overlaid out, so `b` is still present at its pinned state.
|
||||
// Live: `b` is gone. Historical (pinned at the transact's generation): the
|
||||
// un-flushed removal is overlaid out, so `b` is still present at its
|
||||
// pinned state.
|
||||
expect(liveIds).toContain(a)
|
||||
expect(liveIds).not.toContain(b)
|
||||
expect(pastIds).toContain(a)
|
||||
|
|
@ -1262,11 +1281,14 @@ describe('8.0 Db API — generational MVCC', () => {
|
|||
|
||||
it('Model-B retention — explicit caps reclaim single-op history; committed history survives reopen', async () => {
|
||||
const { brain, dir } = await openFsBrain()
|
||||
// Relative baseline: the adopt-at-open backfill holds the first
|
||||
// generation(s), so the 6 writes below land at base+1..base+6.
|
||||
const base = brain.generation()
|
||||
const a = uid('ret-a')
|
||||
await brain.add({ id: a, type: NounType.Document, data: 'a', vector: vec(1), metadata: { v: 1 } })
|
||||
for (let v = 2; v <= 6; v++) await brain.update({ id: a, metadata: { v } })
|
||||
await brain.flush() // persist the per-write generations to disk
|
||||
expect(brain.generation()).toBe(6)
|
||||
expect(brain.generation()).toBe(base + 6)
|
||||
|
||||
// Cap to the 2 most recent generations — older single-op history is reclaimed.
|
||||
const res = await brain.compactHistory({ maxGenerations: 2 })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue