feat: flush() never compacts — history maintenance moves to close() with bounded passes
flush() is durability work: it must cost what the current window's deltas cost, never what the history backlog costs. Under adaptive retention the byte budget derives from free memory, so bulk-load pressure shrank the budget exactly at peak write volume and flush paid actual reclaim inline — a production deployment measured single writes blocked 25-191s behind reclaim-on-flush. - flush() no longer calls autoCompactHistory(); close() is THE auto-compaction site (already ran there; now alone). - Every auto pass is time-bounded (CLOSE_COMPACTION_BUDGET_MS = 5s): reclamation is oldest-first, so an early stop is a consistent prefix and the next pass resumes. Explicit compactHistory() gains an optional timeBudgetMs for caller-chosen maintenance windows. - Documented trade stated where operators read: a long-lived writer that never closes accumulates history until its next explicit compactHistory() — predictable writes, explicit maintenance. Pins: flush-never-reclaims + close-reclaims-durably (db-mvcc), bounded pass stops-then-resumes as a consistent prefix (generationStore unit).
This commit is contained in:
parent
a16567d626
commit
300d9f2a16
7 changed files with 100 additions and 31 deletions
|
|
@ -396,6 +396,15 @@ export type IndexFamily = 'vector' | 'metadata' | 'graph'
|
|||
*/
|
||||
const AGGREGATION_BACKFILL_RETRY_COOLDOWN_MS = 30_000
|
||||
|
||||
/**
|
||||
* Time budget for the auto-compaction pass at close() (8.9.0). Bounds how long
|
||||
* a clean shutdown spends reclaiming history backlog — an early stop is a
|
||||
* consistent prefix and the next close/explicit pass resumes. Explicit
|
||||
* `compactHistory()` calls are unbounded unless the caller passes their own
|
||||
* `timeBudgetMs` (maintenance windows choose their own budgets).
|
||||
*/
|
||||
const CLOSE_COMPACTION_BUDGET_MS = 5_000
|
||||
|
||||
/**
|
||||
* The main Brainy class - Clean, Beautiful, Powerful
|
||||
* REAL IMPLEMENTATION - No stubs, no mocks
|
||||
|
|
@ -8362,19 +8371,24 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
|
||||
/**
|
||||
* @description Run history compaction under the resolved `retention` policy
|
||||
* when `autoCompact` is on (the default). Invoked from `flush()` and
|
||||
* `close()` so generational record-sets cannot accumulate unbounded across a
|
||||
* long-lived writer's lifetime.
|
||||
* when `autoCompact` is on (the default). Invoked from `close()` ONLY
|
||||
* (8.9.0) — flush() is durability work and never pays maintenance costs; a
|
||||
* production deployment measured reclaim-on-flush blocking single writes
|
||||
* for 25-191s under memory pressure. Long-lived writers that never close
|
||||
* accumulate history until their next explicit `compactHistory()` — the
|
||||
* documented trade: predictable writes, explicit maintenance.
|
||||
*
|
||||
* - `'all'` → returns without reclaiming (index compaction for speed still
|
||||
* runs elsewhere; history is decoupled and kept).
|
||||
* - `'adaptive'` → reclaim oldest-unpinned history down to the byte budget.
|
||||
* - `'explicit'` → apply the supplied `maxGenerations`/`maxAge`/`maxBytes` caps.
|
||||
*
|
||||
* Every auto pass is TIME-BOUNDED ({@link CLOSE_COMPACTION_BUDGET_MS}) so a
|
||||
* large backlog can never stall a clean shutdown; the next pass resumes.
|
||||
* Read-only instances and an explicit `autoCompact: false` skip silently.
|
||||
* Pinned generations are never reclaimed ({@link GenerationStore.compact}).
|
||||
* Failures are logged and swallowed — compaction is housekeeping and must
|
||||
* never fail a flush or a clean shutdown.
|
||||
* never fail a clean shutdown.
|
||||
*/
|
||||
private async autoCompactHistory(): Promise<void> {
|
||||
// Nothing to compact on a read-only instance or before init wired up the
|
||||
|
|
@ -8390,12 +8404,16 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
if (policy.mode === 'adaptive') {
|
||||
const budget = this.adaptiveHistoryBudgetBytes(policy.budgetBytes)
|
||||
if (budget === Infinity) return // no pressure signal → keep everything this pass
|
||||
await this.generationStore.compact({ maxBytes: budget })
|
||||
await this.generationStore.compact({
|
||||
maxBytes: budget,
|
||||
timeBudgetMs: CLOSE_COMPACTION_BUDGET_MS
|
||||
})
|
||||
} else {
|
||||
await this.generationStore.compact({
|
||||
maxGenerations: policy.maxGenerations,
|
||||
maxAge: policy.maxAge,
|
||||
maxBytes: policy.maxBytes
|
||||
maxBytes: policy.maxBytes,
|
||||
timeBudgetMs: CLOSE_COMPACTION_BUDGET_MS
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -10540,12 +10558,14 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
this.generationStore.persistCounterNow()
|
||||
])
|
||||
|
||||
// 6. Auto-compact generational history per config.retention (default on).
|
||||
// Runs after the flush so durable state is in place; respects live
|
||||
// Db pins and an explicit autoCompact: false.
|
||||
await this.autoCompactHistory()
|
||||
// NOTE (8.9.0): flush() no longer compacts history. Flush is DURABILITY
|
||||
// work — it must cost what this window's deltas cost, never what the
|
||||
// history backlog costs. Auto-compaction (a MAINTENANCE concern) runs at
|
||||
// close() and via explicit compactHistory(); a production deployment
|
||||
// measured reclaim-on-flush stalling single writes for 25-191s under
|
||||
// memory pressure, which is exactly the class this separation ends.
|
||||
|
||||
// 7. Stamp the entity tree: which source generation the canonical tree
|
||||
// 6. Stamp the entity tree: which source generation the canonical tree
|
||||
// reflects + the rollup invariants that verify it whole (the counters
|
||||
// persisted in step 1). Written at flush boundaries — the tree tracks
|
||||
// every commit by construction, so the stamp is a durable checkpoint,
|
||||
|
|
@ -16173,8 +16193,10 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
}
|
||||
|
||||
// Phase 0b: Auto-compact generational history per config.retention (default
|
||||
// on) BEFORE the generation store closes below. Respects live Db pins and
|
||||
// an explicit autoCompact: false; no-op on read-only instances.
|
||||
// on) BEFORE the generation store closes below. This is THE auto-compaction
|
||||
// site (8.9.0 — flush() never compacts): time-bounded per pass, respects
|
||||
// live Db pins and an explicit autoCompact: false; no-op on read-only
|
||||
// instances.
|
||||
await this.autoCompactHistory()
|
||||
|
||||
// Phase 1: Flush ALL components in parallel to persist buffered data
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue