fix(reads): the readiness gate guards every index read surface — serving empty from a not-ready provider is unrepresentable
A production store acked writes while readback served empty for fifteen minutes. The brainy half: the lazy readiness gate had exactly one caller — find() — while related() and every VFS path served straight from providers that init had deferred (disableAutoRebuild on a large store). A false health verdict from the accelerator pulled the trigger; the unguarded read surfaces were the gun. Every index read funnels through three helpers; the gate now lives at those choke points, so any first read on a cold instance waits for the build and then serves truth — the fast path after the latch is one boolean. The lazy build's start is narrated through the production logger, never the silent-suppressible console: fifteen silent minutes taught that line. Pinned with the production shape: related() as the first-ever read on a fresh lazy instance serves the relation; a filtered find serves the row.
This commit is contained in:
parent
1e046aa115
commit
40e7119b85
2 changed files with 95 additions and 1 deletions
|
|
@ -3907,6 +3907,12 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
uuid: string,
|
uuid: string,
|
||||||
options?: { direction?: 'in' | 'out' | 'both'; limit?: number; offset?: number }
|
options?: { direction?: 'in' | 'out' | 'both'; limit?: number; offset?: number }
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
|
// READ-SURFACE READINESS GATE (the 4.2.4 blackout's brainy half): every
|
||||||
|
// index read funnels through this helper, so the gate here makes
|
||||||
|
// serve-while-not-ready UNREPRESENTABLE — a production store once acked
|
||||||
|
// writes while every non-find() read served empty from a not-ready
|
||||||
|
// provider for 15 minutes. Fast path after the latch is one boolean.
|
||||||
|
await this.ensureIndexesLoaded()
|
||||||
const entityInt = this.graphEntityInt(uuid)
|
const entityInt = this.graphEntityInt(uuid)
|
||||||
if (entityInt === undefined) return []
|
if (entityInt === undefined) return []
|
||||||
const neighborInts = await this.graphIndex.getNeighbors(entityInt, options)
|
const neighborInts = await this.graphIndex.getNeighbors(entityInt, options)
|
||||||
|
|
@ -11807,6 +11813,12 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
filter: unknown,
|
filter: unknown,
|
||||||
opts?: { limit?: number; offset?: number }
|
opts?: { limit?: number; offset?: number }
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
|
// READ-SURFACE READINESS GATE (the 4.2.4 blackout's brainy half): every
|
||||||
|
// index read funnels through this helper, so the gate here makes
|
||||||
|
// serve-while-not-ready UNREPRESENTABLE — a production store once acked
|
||||||
|
// writes while every non-find() read served empty from a not-ready
|
||||||
|
// provider for 15 minutes. Fast path after the latch is one boolean.
|
||||||
|
await this.ensureIndexesLoaded()
|
||||||
try {
|
try {
|
||||||
return await this.metadataIndex.getIdsForFilter(filter, opts)
|
return await this.metadataIndex.getIdsForFilter(filter, opts)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -14279,6 +14291,12 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
verbTypes?: Set<VerbType>,
|
verbTypes?: Set<VerbType>,
|
||||||
limit?: number
|
limit?: number
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
|
// READ-SURFACE READINESS GATE (the 4.2.4 blackout's brainy half): every
|
||||||
|
// index read funnels through this helper, so the gate here makes
|
||||||
|
// serve-while-not-ready UNREPRESENTABLE — a production store once acked
|
||||||
|
// writes while every non-find() read served empty from a not-ready
|
||||||
|
// provider for 15 minutes. Fast path after the latch is one boolean.
|
||||||
|
await this.ensureIndexesLoaded()
|
||||||
// 8.0 BigInt boundary: unmapped node → no relations.
|
// 8.0 BigInt boundary: unmapped node → no relations.
|
||||||
const nodeInt = this.graphEntityInt(nodeId)
|
const nodeInt = this.graphEntityInt(nodeId)
|
||||||
if (nodeInt === undefined) return []
|
if (nodeInt === undefined) return []
|
||||||
|
|
@ -16217,7 +16235,15 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start lazy rebuild (with mutex to prevent concurrent rebuilds)
|
// Start lazy rebuild (with mutex to prevent concurrent rebuilds).
|
||||||
|
// ALWAYS narrated (prodLog, never the silent-suppressible console): a
|
||||||
|
// read that triggers an index build must be visible to the operator —
|
||||||
|
// fifteen silent minutes of a production blackout taught this line.
|
||||||
|
prodLog.warn(
|
||||||
|
`[Brainy] first read on this instance is building the derived indexes ` +
|
||||||
|
`(deferred at open by disableAutoRebuild) — reads WAIT and then serve; ` +
|
||||||
|
`nothing serves empty. Bounded by store size; progress under [MetadataIndex]/[GraphIndex].`
|
||||||
|
)
|
||||||
this.lazyRebuildInProgress = true
|
this.lazyRebuildInProgress = true
|
||||||
this.lazyRebuildPromise = this.rebuildIndexesIfNeeded(true)
|
this.lazyRebuildPromise = this.rebuildIndexesIfNeeded(true)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
|
||||||
68
tests/integration/read-surface-readiness.test.ts
Normal file
68
tests/integration/read-surface-readiness.test.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
* @module tests/integration/read-surface-readiness
|
||||||
|
* @description THE READ-SURFACE READINESS GATE (a production blackout's
|
||||||
|
* brainy half): with `disableAutoRebuild: true`, init defers index builds —
|
||||||
|
* and before this gate, only find() waited for the lazy rebuild while
|
||||||
|
* related() and every VFS path served EMPTY from the not-ready providers
|
||||||
|
* (writes acked into canonical, readback empty — fifteen live minutes).
|
||||||
|
* The pins: on a fresh instance over a populated store, the FIRST read on
|
||||||
|
* every surface serves truth (it waits for the build), never empty.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, afterEach } from 'vitest'
|
||||||
|
import { mkdtempSync, rmSync } from 'node:fs'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { Brainy } from '../../src/index.js'
|
||||||
|
import { NounType, VerbType } from '../../src/types/graphTypes.js'
|
||||||
|
|
||||||
|
const dirs: string[] = []
|
||||||
|
const brains: Brainy[] = []
|
||||||
|
afterEach(async () => {
|
||||||
|
for (const b of brains.splice(0)) await b.close().catch(() => {})
|
||||||
|
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
async function openLazy(dir: string): Promise<Brainy> {
|
||||||
|
const brain = new Brainy({
|
||||||
|
storage: { type: 'filesystem', path: dir },
|
||||||
|
requireSubtype: false,
|
||||||
|
silent: true,
|
||||||
|
disableAutoRebuild: true
|
||||||
|
})
|
||||||
|
await brain.init()
|
||||||
|
brains.push(brain)
|
||||||
|
return brain
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('read-surface readiness gate', () => {
|
||||||
|
it('related() as the FIRST read on a fresh lazy instance serves truth, never empty', async () => {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), 'brainy-readgate-'))
|
||||||
|
dirs.push(dir)
|
||||||
|
const writer = await openLazy(dir)
|
||||||
|
const a = await writer.add({ data: 'hub row', type: NounType.Document, metadata: { n: 1 } })
|
||||||
|
const b = await writer.add({ data: 'leaf row', type: NounType.Document, metadata: { n: 2 } })
|
||||||
|
await writer.relate({ from: a, to: b, type: VerbType.RelatedTo })
|
||||||
|
await writer.flush()
|
||||||
|
await brains.pop()!.close()
|
||||||
|
|
||||||
|
// Fresh instance: indexes deferred at open. The production shape called
|
||||||
|
// related() FIRST (no find() to trigger the old, only gate).
|
||||||
|
const reader = await openLazy(dir)
|
||||||
|
const rels = await reader.related({ from: a })
|
||||||
|
expect(rels.length, 'the FIRST related() read waits for the build and serves').toBeGreaterThan(0)
|
||||||
|
expect(rels.some((r) => r.to === b || (r as { target?: string }).target === b)).toBe(true)
|
||||||
|
}, 120000)
|
||||||
|
|
||||||
|
it('a metadata-filtered read as the FIRST read serves truth, never empty', async () => {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), 'brainy-readgate2-'))
|
||||||
|
dirs.push(dir)
|
||||||
|
const writer = await openLazy(dir)
|
||||||
|
await writer.add({ data: 'tagged row', type: NounType.Document, metadata: { team: 'atlas' } })
|
||||||
|
await writer.flush()
|
||||||
|
await brains.pop()!.close()
|
||||||
|
|
||||||
|
const reader = await openLazy(dir)
|
||||||
|
const rows = await reader.find({ where: { team: 'atlas' } })
|
||||||
|
expect(rows.length, 'filtered find on a cold lazy instance serves').toBe(1)
|
||||||
|
}, 120000)
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue