feat: multi-process safety + read-only inspector mode
Filesystem storage now enforces single-writer, many-reader semantics. A second writer on the same data directory throws at init time with the holder's PID, hostname, and heartbeat — replacing the previous silent stale-reads failure mode. - New: `Brainy.openReadOnly()` — coexists with a live writer, every mutation throws clearly. - New: writer lock at `<rootDir>/locks/_writer.lock` with 10s heartbeat and stale-detection (PID liveness + heartbeat freshness). - New: cross-process flush-request RPC (filesystem-based, no signals) so inspectors can force fresh state on demand. - New: `brain.stats()`, `brain.explain(findParams)`, `brain.health()` for operator-facing introspection. - New: `brainy inspect` CLI with 13 subcommands (stats, find, get, relations, explain, health, sample, fields, dump, watch, backup, repair, diff), all read-only by default. - Same-PID re-opens allowed with a warning (preserves test "simulate restart" patterns). - Storage instances passed directly via `storage: new MemoryStorage()` are now honoured instead of silently falling through to the filesystem auto-detect path. Brainy + Cortex compose under this model — the lock covers both because they share `rootDir`, Cortex segments are immutable mmap files, and MANIFEST updates use atomic-rename.
This commit is contained in:
parent
1bc6a430c7
commit
4fcdc0fef3
12 changed files with 2342 additions and 7 deletions
|
|
@ -1549,6 +1549,42 @@ export class MetadataIndexManager {
|
|||
* @param value - Exact value to match
|
||||
* @returns Array of matching entity UUID strings
|
||||
*/
|
||||
/**
|
||||
* Report which index path a `where` clause on `field` will hit. Used by
|
||||
* `brain.explain()` so an operator can see *before* running a query whether
|
||||
* the field has any index entries at all. A `find({ where: { someField: ... } })`
|
||||
* against a field with no index entries returns `[]` silently — `explainField`
|
||||
* surfaces that as `path: 'none'` so the empty result has an explanation.
|
||||
*/
|
||||
async explainField(field: string): Promise<{
|
||||
path: 'column-store' | 'sparse-chunked' | 'none'
|
||||
notes?: string
|
||||
}> {
|
||||
if (this.columnStore && this.columnStore.hasField(field)) {
|
||||
return {
|
||||
path: 'column-store',
|
||||
notes: 'O(log n) binary search + roaring bitmap. Best path.'
|
||||
}
|
||||
}
|
||||
const sparse = await this.loadSparseIndex(field)
|
||||
if (sparse) {
|
||||
return {
|
||||
path: 'sparse-chunked',
|
||||
notes: 'Chunked sparse index with zone maps and bloom filters.'
|
||||
}
|
||||
}
|
||||
return {
|
||||
path: 'none',
|
||||
notes:
|
||||
`No index entries for field "${field}". A find({ where: { ${field}: ... } }) ` +
|
||||
`will return an empty result regardless of whether matching entities exist on disk. ` +
|
||||
`Likely causes: (1) the writer registered the field in memory but has not flushed; ` +
|
||||
`(2) the field name does not match what was written (typo or casing); ` +
|
||||
`(3) the field is genuinely absent from all entities. Call requestFlush() on the ` +
|
||||
`writer or call brain.flush() before relying on the result.`
|
||||
}
|
||||
}
|
||||
|
||||
async getIds(field: string, value: any): Promise<string[]> {
|
||||
// Track exact query for field statistics
|
||||
if (this.fieldStats.has(field)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue