open-brainy/tests/unit/brainy/persistence-policy.test.ts
David Snelling a50726e6a8
Some checks failed
CI / Node 22 (push) Successful in 12m14s
CI / Node 24 (push) Successful in 12m7s
CI / Bun (latest) (push) Has been cancelled
fix(persistence): the idle flush trigger debounces under load — deferred to the floor, never dropped, never a flush-per-gap amplifier
An internal report from cross-engine write-path instrumentation: with
individual writes slower than the idle window (a contended disk), every
inter-write gap looked idle and fired a background full flush — 15 extra
flushes during 100 contended adds, amplifying the very pressure that
slowed the writes. The law now: an idle fire landing within the spacing
floor of the last flush DEFERS to the floor boundary instead of flushing;
the floor is min(interval, 10× the CONFIGURED idle window) — scaled to
caller intent (a tiny idle window keeps fast idle-driven durability;
default 2s/30s config gets a 20s floor), derived from the configured
idle, never from a deferred re-arm delay (which would compound into
runaway deferral). Deferred is never dropped: a lone write on a
then-quiet store still persists at the floor without any further write
arriving.

Pins: the contended-shape pin (six slow-spaced writes fire ≤2 idle
flushes, not one per gap; then still persist) + the original quiet-store
idle pin unchanged. Unit 2055/2055.
2026-08-10 12:15:02 -07:00

121 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @module tests/unit/brainy/persistence-policy
* @description THE ENGINE-OWNED FLUSH CADENCE pins (A4,
* SELF-ENGINE-LIFECYCLE-SPRINT, David-directed: callers NEVER call flush()
* in hot paths). The production disease: 829 caller-scheduled per-write
* flushes convoying into 4566 second write walls — cadence hand-rolled a
* layer above the only layer that can see dirty state and IO pressure.
*
* Pinned here: (1) the write-count trigger fires a BACKGROUND flush without
* any caller flush(); (2) the idle trigger; (3) `'manual'` restores
* caller-owned cadence exactly; (4) THE ACK LAW — a write acknowledges
* without awaiting any background flush, even one that never resolves.
*/
import { describe, it, expect, afterEach, vi } from 'vitest'
import { Brainy } from '../../../src/index.js'
import { NounType } from '../../../src/types/graphTypes.js'
const brains: Brainy[] = []
async function mk(persistence?: {
policy?: 'auto' | 'manual'
flushEveryWrites?: number
flushIntervalMs?: number
flushOnIdleMs?: number
}): Promise<Brainy> {
const b = new Brainy({
storage: { type: 'memory' },
requireSubtype: false,
...(persistence && { persistence })
})
await b.init()
brains.push(b)
return b
}
afterEach(async () => {
for (const b of brains.splice(0)) await b.close().catch(() => {})
vi.restoreAllMocks()
})
describe('persistence policy — the engine owns its flush cadence', () => {
it('write-count trigger: N committed writes fire ONE background flush, no caller flush()', async () => {
const brain = await mk({ flushEveryWrites: 5, flushOnIdleMs: 60_000, flushIntervalMs: 600_000 })
const flushSpy = vi.spyOn(brain, 'flush')
for (let i = 0; i < 5; i++) {
await brain.add({ data: `w${i}`, type: NounType.Document, metadata: { i } })
}
await vi.waitFor(() => expect(flushSpy).toHaveBeenCalled(), { timeout: 5000 })
// Single-flight: the threshold crossing kicks exactly one.
expect(flushSpy.mock.calls.length).toBe(1)
})
it('idle trigger: a quiet store with dirty writes flushes itself', async () => {
const brain = await mk({ flushEveryWrites: 10_000, flushIntervalMs: 600_000, flushOnIdleMs: 60 })
const flushSpy = vi.spyOn(brain, 'flush')
await brain.add({ data: 'lone write', type: NounType.Document, metadata: {} })
await vi.waitFor(() => expect(flushSpy).toHaveBeenCalled(), { timeout: 5000 })
})
it('idle debounce under load: slow writes never fire a flush per inter-write gap', async () => {
// The contended-disk amplifier: writes slower than the idle window make
// every gap look idle — without the spacing floor this fired a full
// flush per write (measured 15 background flushes in 100 contended adds
// on a production-shaped box). The floor (min(interval, 10×idle)) caps
// idle fires; deferred, never dropped.
const brain = await mk({ flushEveryWrites: 10_000, flushIntervalMs: 600_000, flushOnIdleMs: 50 })
const flushSpy = vi.spyOn(brain, 'flush')
// Six writes spaced wider than the idle window (50ms) with the whole
// span inside ~one floor window (500ms): the old behavior fires ~an
// idle flush per gap (≈6); the debounced behavior fires at most two
// (one immediate boot-window fire + one at the floor boundary).
for (let i = 0; i < 6; i++) {
await brain.add({ data: `slow ${i}`, type: NounType.Document, metadata: {} })
await new Promise((r) => setTimeout(r, 70))
}
expect(flushSpy.mock.calls.length, 'no flush-per-gap amplifier').toBeLessThanOrEqual(2)
// Deferred, never dropped: the dirty writes still persist once the
// floor elapses on the now-quiet store.
await vi.waitFor(() => expect(flushSpy).toHaveBeenCalled(), { timeout: 5000 })
})
it("'manual' policy: the engine NEVER flushes on its own", async () => {
const brain = await mk({ policy: 'manual', flushEveryWrites: 2, flushOnIdleMs: 30 })
const flushSpy = vi.spyOn(brain, 'flush')
for (let i = 0; i < 6; i++) {
await brain.add({ data: `m${i}`, type: NounType.Document, metadata: { i } })
}
await new Promise((r) => setTimeout(r, 150))
expect(flushSpy).not.toHaveBeenCalled()
})
it('THE ACK LAW: writes acknowledge without awaiting the background flush — even a hung one', async () => {
const brain = await mk({ flushEveryWrites: 2, flushOnIdleMs: 60_000, flushIntervalMs: 600_000 })
// A flush that NEVER resolves: if any write ack awaited it, the test
// would time out. (The engine's background flight must be fire-and-log.)
vi.spyOn(brain, 'flush').mockImplementation(() => new Promise<void>(() => {}))
for (let i = 0; i < 6; i++) {
const id = await brain.add({ data: `a${i}`, type: NounType.Document, metadata: { i } })
expect(id).toBeTruthy()
}
// All six writes acked while the "flush" hangs forever.
const rows = await brain.find({ type: NounType.Document, limit: 10 })
expect(rows.length).toBe(6)
// Un-hang before afterEach close(): restore the method AND drop the
// never-resolving in-flight promise (close() awaits the flight — with a
// real flush that is correct; here it is the test's own artifact).
vi.restoreAllMocks()
;(brain as unknown as { _persistBackgroundFlight: Promise<void> | null })._persistBackgroundFlight =
null
})
})