fix: cold-open no longer re-derives durable indexes — complete the readiness contract for all three providers
A production deployment measured ~48 seconds on EVERY reopen of an 11k-entity brain. Root cause: brainy's rebuild gate decided from in-memory size()/count, which read 0 for a durable-but-not-resident index, so it re-read every entity file to rebuild from scratch. At GA we gave only the GRAPH provider a readiness contract (init() eager cold-load + isReady() honest signal) so it would never eat that spurious rebuild; the vector and metadata providers never got it, and brainy never even eager-inited the vector provider. Complete the contract symmetrically: - plugin.ts: VectorIndexProvider gains optional init()+isReady(); MetadataIndexProvider gains isReady() — mirroring GraphIndexProvider. Additive and optional; a provider that exposes nothing keeps today's behavior. - brainy.ts: eager-init every provider that exposes init() (after metadata init() so the id-mapper is hydrated first), then decide per leg in precedence order — migrating (skip) -> epoch drift (rebuild) -> isReady() -> a per-leg empty fallback. The old instant fast-path keyed off this.index.size()>0, a dishonest proxy that skipped the metadata/graph checks whenever the vector was warm and never fired on a real cold process anyway; removed. The per-leg fallbacks differ because "empty" means different things: the JS vector's rebuild() IS its load, so size()===0 correctly triggers it; the id-mapper backs metadata, so totalEntries===0 (past the empty-store return) is a real load failure; but entities do not imply edges, so a graph size()===0 is a valid empty state, not a load failure. - The JS graph now COLD-LOADS its durable LSM instead of re-deriving from a full canonical verb scan on every boot (baseStorage._initializeGraphIndex loads the persisted SSTables via a new GraphAdjacencyIndex.init(); it self-heals from canonical only when the durable state is genuinely missing). This removes an O(E)-per-open cost every filesystem consumer paid. - LSMTree.loadManifest loads its SSTables BEFORE publishing the relationship count, and resets to an honest-empty state on load failure — a tree can no longer claim persisted relationships while holding none (the silent-empty cold-load class the query-time guards exist to prevent). Verified end-to-end against a built brain: a warm reopen (with edges and edgeless) reloads only the JS vector; the graph and metadata cold-load with no rebuild, and queries return correct results. New tests in cold-open-rebuild-gate.test.ts pin the contract (isReady() defers, self-heal still fires); migration-deference updated to drive size-based deference through the vector, the leg where empty->rebuild remains correct. Pairs with the native provider's isReady()/init() implementation — brainy's gate defers only to a signal the provider exposes.
This commit is contained in:
parent
4fde94bc2f
commit
61c247c923
8 changed files with 425 additions and 57 deletions
|
|
@ -2429,11 +2429,26 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
// invalidateGraphIndex); on first init Brainy wires it right after.
|
||||
this.graphIndex = new GraphAdjacencyIndex(this, {}, this.graphEntityIdResolver)
|
||||
|
||||
// Check if we need to rebuild from existing data
|
||||
const sampleVerbs = await this.getVerbs({ pagination: { limit: 1 } })
|
||||
if (sampleVerbs.items.length > 0) {
|
||||
prodLog.info('Found existing verbs, rebuilding graph index...')
|
||||
await this.graphIndex.rebuild()
|
||||
// Load the PERSISTED adjacency first (LSM manifests + SSTables). A warm
|
||||
// reopen must load the durable index it already built — the previous
|
||||
// "any verb exists → rebuild()" check here re-derived the whole graph
|
||||
// from a full canonical verb scan on EVERY boot, an O(E) cost that
|
||||
// dominated real deployments' startup.
|
||||
await this.graphIndex.init()
|
||||
|
||||
// Self-heal only when the durable state is genuinely missing: canonical
|
||||
// records exist but the loaded index is empty (first open on pre-index
|
||||
// data, a deleted/corrupt _graph dir, or the LSM load failing loud).
|
||||
// One O(1) probe replaces the unconditional O(E) re-derive.
|
||||
if (this.graphIndex.size() === 0) {
|
||||
const sampleVerbs = await this.getVerbs({ pagination: { limit: 1 } })
|
||||
if (sampleVerbs.items.length > 0) {
|
||||
prodLog.warn(
|
||||
'GraphAdjacencyIndex: canonical verbs exist but the persisted adjacency is empty — ' +
|
||||
'rebuilding from storage (one-time self-heal).'
|
||||
)
|
||||
await this.graphIndex.rebuild()
|
||||
}
|
||||
}
|
||||
|
||||
return this.graphIndex
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue