fix: exception-safe aggregation backfill + generation-verified adoption + loud open-path guards
- Aggregation backfill walks build into a STAGING map and swap in atomically on completion. A mid-walk failure drops the staging map — the previous live state keeps serving, the aggregate stays flagged pending, and the storage error surfaces to the failing query. Previously the walk wiped live state before a scan that could throw, never cleared the pending flag on failure, and re-ran a full walk on every subsequent query: a silent wipe/walk/throw loop at the caller's retry rate. - Failed walks are latched: retries within a 30s cooldown rethrow the recorded error instantly instead of re-walking, so a tight caller-side retry loop costs one loud error per query, never a full store walk per query. - Persisted aggregation state is stamped with the store's committed generation at flush; reopen adoption requires stamp equality. Stale state (unclean shutdown) or over-counting state (a log truncation on a copied store pulled the watermark back) triggers exactly one loud rescan, never a silent adopt. - The backfill/adoption path narrates: adoption decisions, walk start/finish with counts and duration, and failures all log by default. - getNouns/getVerbs refuse a supplied-but-undecodable pagination cursor with a loud error instead of silently restarting the walk at offset 0 (which re-served page 1 forever to any while(hasMore) caller). - The graph cold-load verb walk aborts loudly on a missing or non-advancing cursor with hasMore=true. - A versioned index provider whose generation is AHEAD of the committed watermark (torn copy / crash-recovery truncation) is now named loudly at open, alongside the existing behind-direction message.
This commit is contained in:
parent
01a7f3dd01
commit
a77b064bd7
7 changed files with 335 additions and 36 deletions
|
|
@ -2068,11 +2068,20 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
// Cursor (8.0): resume token carrying the (shard, nounId) of the last returned
|
||||
// noun — the noun mirror of getVerbsWithPagination. When present it supersedes
|
||||
// `offset` and resumes the shard walk immediately AFTER that position, so a full
|
||||
// walk is O(N) instead of the O(N²) of offset paging. Malformed/foreign tokens
|
||||
// decode to null → offset fallback. (Previously the cursor was ignored, which
|
||||
// was latent — the only multi-page consumer used a single big page — until small
|
||||
// chunk sizes needed page 2 and an offset-0-on-every-call walk never terminated.)
|
||||
// walk is O(N) instead of the O(N²) of offset paging. (Previously the cursor was
|
||||
// ignored, which was latent — the only multi-page consumer used a single big
|
||||
// page — until small chunk sizes needed page 2 and an offset-0-on-every-call
|
||||
// walk never terminated.)
|
||||
const cursor = this.decodeNounWalkCursor(options.cursor)
|
||||
if (options.cursor && cursor === null) {
|
||||
// A supplied-but-undecodable resume token must FAIL, not silently restart
|
||||
// at offset 0 — to a while(hasMore) caller the silent fallback re-serves
|
||||
// page 1 forever: an unbounded CPU loop wearing pagination's clothes.
|
||||
throw BrainyError.storage(
|
||||
`getNouns: invalid pagination cursor '${options.cursor}' — cannot resume this walk. ` +
|
||||
`Restart it without a cursor.`
|
||||
)
|
||||
}
|
||||
const collected: Array<{ noun: HNSWNounWithMetadata; shard: number }> = []
|
||||
|
||||
// Peek one past the window so `hasMore` is decidable. Cursor mode collects one
|
||||
|
|
@ -2366,8 +2375,17 @@ export abstract class BaseStorage extends BaseStorageAdapter {
|
|||
// (shard, verbId) of the last returned verb. When present it SUPERSEDES `offset`
|
||||
// and resumes the shard walk immediately AFTER that position, so a full walk is
|
||||
// O(N) total instead of the O(N²) of offset paging (which re-scans from shard 0
|
||||
// every page). Malformed / foreign tokens decode to null → offset fallback.
|
||||
// every page).
|
||||
const cursor = this.decodeVerbWalkCursor(options.cursor)
|
||||
if (options.cursor && cursor === null) {
|
||||
// A supplied-but-undecodable resume token must FAIL, not silently restart
|
||||
// at offset 0 — to a while(hasMore) caller the silent fallback re-serves
|
||||
// page 1 forever: an unbounded CPU loop wearing pagination's clothes.
|
||||
throw BrainyError.storage(
|
||||
`getVerbs: invalid pagination cursor '${options.cursor}' — cannot resume this walk. ` +
|
||||
`Restart it without a cursor.`
|
||||
)
|
||||
}
|
||||
|
||||
// Each collected entry remembers its shard so nextCursor can point at the exact
|
||||
// (shard, id) resume position.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue