/** * @module tests/integration/transact-durability-barrier * @description Regression for a consumer-reported durability gap: a committed * `transact()` reported success while its canonical entity writes were still * only in the page cache (tmp+rename, not yet fsync'd), whereas the generation * counter/manifest WERE fsync'd — so a hard kill could leave the counter ahead * of the persisted entity bytes ("phantom progress" for any generation-based * consumer resuming from the counter). * * Fix: `commitTransaction` opens a write barrier before running the planned * operations and flushes it (fsync of every canonical write + the parent dir of * every canonical delete) BEFORE advancing the counter and writing the manifest. * These tests assert the ordering property directly by recording the storage's * `syncRawObjects` calls: the entity writes are fsync'd in an earlier call than * the manifest, and a precommit-rejected batch opens no barrier and advances * nothing. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import * as fs from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' import { Brainy } from '../../src/brainy.js' import { NounType, VerbType } from '../../src/types/graphTypes.js' import { MemoryStorage } from '../../src/storage/adapters/memoryStorage.js' const MANIFEST_REL = '_system/manifest.json' describe('transact durability barrier — entity writes fsync before the counter advances', () => { let dir: string let brain: any let syncCalls: string[][] let beginCount: number let flushCount: number beforeEach(async () => { process.env.BRAINY_DETERMINISTIC_EMBEDDINGS = 'true' dir = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-durability-')) brain = new Brainy({ requireSubtype: false, storage: { type: 'filesystem', path: dir }, dimensions: 384, silent: true }) await brain.init() // Instrument the real filesystem storage: record every fsync batch in order, // and count barrier open/flush, delegating to the originals. syncCalls = [] beginCount = 0 flushCount = 0 const storage = brain['storage'] const origSync = storage.syncRawObjects.bind(storage) storage.syncRawObjects = async (paths: string[]) => { syncCalls.push([...paths]) return origSync(paths) } const origBegin = storage.beginWriteBarrier.bind(storage) storage.beginWriteBarrier = () => { beginCount++ return origBegin() } const origFlush = storage.flushWriteBarrier.bind(storage) storage.flushWriteBarrier = async () => { flushCount++ return origFlush() } }) afterEach(async () => { await brain.close() fs.rmSync(dir, { recursive: true, force: true }) }) /** Index of the first sync call whose paths satisfy `pred` (or -1). */ const findSync = (pred: (p: string) => boolean): number => syncCalls.findIndex((paths) => paths.some(pred)) it('a committed transact fsyncs the entity write BEFORE the manifest', async () => { const id = '00000000-0000-4000-8000-00000000ab01' await brain.transact([{ op: 'add', id, data: 'durable entity', type: NounType.Thing }]) // The barrier opened and flushed exactly once. expect(beginCount).toBe(1) expect(flushCount).toBe(1) // The entity's canonical metadata write was fsync'd… const entitySyncIdx = findSync((p) => p.includes(`/${id}/`) && p.includes('entities/nouns')) expect(entitySyncIdx).toBeGreaterThanOrEqual(0) // …and the manifest (the commit point) was fsync'd in a LATER call. const manifestSyncIdx = findSync((p) => p === MANIFEST_REL) expect(manifestSyncIdx).toBeGreaterThanOrEqual(0) expect(entitySyncIdx).toBeLessThan(manifestSyncIdx) }) it('a multi-op transact (add + relate) fsyncs both endpoints and the edge before the manifest', async () => { const a = '00000000-0000-4000-8000-00000000ab02' const b = '00000000-0000-4000-8000-00000000ab03' await brain.transact([ { op: 'add', id: a, data: 'A', type: NounType.Thing }, { op: 'add', id: b, data: 'B', type: NounType.Thing }, { op: 'relate', from: a, to: b, type: VerbType.Contains } ]) expect(flushCount).toBe(1) const aIdx = findSync((p) => p.includes(`/${a}/`)) const bIdx = findSync((p) => p.includes(`/${b}/`)) const manifestIdx = findSync((p) => p === MANIFEST_REL) expect(aIdx).toBeGreaterThanOrEqual(0) expect(bIdx).toBeGreaterThanOrEqual(0) expect(manifestIdx).toBeGreaterThanOrEqual(0) expect(Math.max(aIdx, bIdx)).toBeLessThan(manifestIdx) // Sanity: the data is actually there and traverses. expect((await brain.related(a)).map((r: any) => r.to)).toContain(b) }) it('a precommit-rejected batch opens no barrier and advances nothing', async () => { const anchor = await brain.add({ id: '00000000-0000-4000-8000-00000000ab04', data: 'anchor', type: NounType.Thing }) const genBefore = brain.generationStore.generation() syncCalls.length = 0 beginCount = 0 flushCount = 0 const never = '00000000-0000-4000-8000-00000000ab05' await expect( brain.transact([ { op: 'add', id: never, data: 'never', type: NounType.Thing }, { op: 'update', id: anchor, metadata: { poke: 1 }, ifRev: 999 } ]) ).rejects.toMatchObject({ name: 'RevisionConflictError' }) // Precommit throws before the barrier opens (it lives just before execute): // the barrier never opens, nothing is flushed, and the generation counter is // unchanged. (A manifest sync CAN appear here from transact() flushing the // anchor's previously-buffered single-op — that is the anchor's deferred // durability, not this rejected batch committing; beginCount is the clean // signal that this batch's commit path never reached execute.) expect(beginCount).toBe(0) expect(flushCount).toBe(0) expect(brain.generationStore.generation()).toBe(genBefore) expect(await brain.get(never)).toBeNull() }) it('MemoryStorage does not implement the barrier (optional-chaining no-op)', () => { const mem = new MemoryStorage() as any // In-memory has no durability seam; the generation store treats the absent // barrier methods as no-ops via optional chaining. expect(mem.beginWriteBarrier).toBeUndefined() expect(mem.flushWriteBarrier).toBeUndefined() }) })