With the unique per-writer temp suffix, a rename ENOENT can no longer mean "a concurrent idempotent writer already renamed it" — nobody else holds this writer's temp. It means our just-written temp vanished before the rename, so the bytes did NOT land. The old code returned success on that ENOENT, acknowledging a write that persisted nothing; the native provider mmaps these blobs, so a phantom-acked blob is a silent-loss. saveBinaryBlob now retries once with a fresh temp (self-healing a transient external-sweeper/crash-cleanup race), and if the temp vanishes again it throws loud rather than acknowledging a store-nothing write. Non-ENOENT rename faults propagate verbatim as before. The unique-temp suffix already eliminated the concurrent same-key collision that originally motivated the ENOENT shortcut.
76 lines
3 KiB
TypeScript
76 lines
3 KiB
TypeScript
/**
|
|
* @module tests/unit/storage/blob-save-durability
|
|
* @description Finding 13 (cortex sweep): saveBinaryBlob must never acknowledge a
|
|
* durable write that stored nothing. With the UNIQUE per-writer temp suffix, a
|
|
* rename ENOENT can no longer mean "a concurrent idempotent writer already
|
|
* renamed it" — nobody else has this writer's temp — so it means our temp
|
|
* vanished before the rename and the bytes did NOT land. The old code returned
|
|
* success on that ENOENT; the native provider mmaps these blobs, so a
|
|
* phantom-acked blob is the exact silent-loss class the registered-blob work
|
|
* chases. The fix retries once with a fresh temp, then fails loud.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
import * as fs from 'node:fs'
|
|
import * as os from 'node:os'
|
|
import * as path from 'node:path'
|
|
import { FileSystemStorage } from '../../../src/storage/adapters/fileSystemStorage.js'
|
|
|
|
const enoent = (): Error => Object.assign(new Error('temp removed'), { code: 'ENOENT' })
|
|
|
|
describe('FileSystemStorage.saveBinaryBlob durable-write honesty (finding 13)', () => {
|
|
let dir: string
|
|
let storage: any
|
|
|
|
beforeEach(async () => {
|
|
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'brainy-blob-save-'))
|
|
storage = new FileSystemStorage(dir)
|
|
await storage.init()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
fs.rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
it('persists and reads back a blob under normal conditions', async () => {
|
|
await storage.saveBinaryBlob('graph-lsm/seg-1', Buffer.from([1, 2, 3, 4]))
|
|
expect(await storage.loadBinaryBlob('graph-lsm/seg-1')).toEqual(Buffer.from([1, 2, 3, 4]))
|
|
})
|
|
|
|
it('throws (never acks) when the temp vanishes on every rename attempt', async () => {
|
|
vi.spyOn(fs.promises, 'rename').mockRejectedValue(enoent())
|
|
|
|
await expect(
|
|
storage.saveBinaryBlob('k/silent-loss', Buffer.from([9]))
|
|
).rejects.toThrow(/did NOT persist/)
|
|
|
|
// Prove the write truly did not land — and was not acknowledged. (loadBinaryBlob
|
|
// uses readFile, not rename, so it reflects the real on-disk state.)
|
|
expect(await storage.loadBinaryBlob('k/silent-loss')).toBeNull()
|
|
})
|
|
|
|
it('self-heals: a single transient temp-vanish retries and persists', async () => {
|
|
const real = fs.promises.rename.bind(fs.promises)
|
|
let calls = 0
|
|
vi.spyOn(fs.promises, 'rename').mockImplementation(async (from: any, to: any) => {
|
|
calls++
|
|
if (calls === 1) throw enoent()
|
|
return real(from, to)
|
|
})
|
|
|
|
await expect(
|
|
storage.saveBinaryBlob('k/heals', Buffer.from([7, 7]))
|
|
).resolves.toBeUndefined()
|
|
expect(calls).toBe(2) // first attempt vanished, retry landed
|
|
expect(await storage.loadBinaryBlob('k/heals')).toEqual(Buffer.from([7, 7]))
|
|
})
|
|
|
|
it('a non-ENOENT rename fault propagates verbatim (not swallowed)', async () => {
|
|
vi.spyOn(fs.promises, 'rename').mockRejectedValue(
|
|
Object.assign(new Error('disk fault'), { code: 'EIO' })
|
|
)
|
|
await expect(
|
|
storage.saveBinaryBlob('k/eio', Buffer.from([1]))
|
|
).rejects.toMatchObject({ code: 'EIO' })
|
|
})
|
|
})
|