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:
David Snelling 2026-07-19 12:04:39 -07:00
parent a16567d626
commit 300d9f2a16
7 changed files with 100 additions and 31 deletions

View file

@ -2220,6 +2220,11 @@ export class GenerationStore {
const maxAge = options?.maxAge
const maxBytes = options?.maxBytes
const ageCutoff = maxAge !== undefined ? Date.now() - maxAge : undefined
// Bounded maintenance pass (8.9.0): stop reclaiming once the budget is
// spent. Safe mid-loop — reclamation is oldest-first, so an early stop
// leaves a consistent contiguous prefix and the next pass resumes.
const deadline =
options?.timeBudgetMs !== undefined ? Date.now() + options.timeBudgetMs : undefined
const noCaps =
maxGenerations === undefined && maxAge === undefined && maxBytes === undefined
@ -2233,6 +2238,7 @@ export class GenerationStore {
for (const gen of [...this.committedGensAsc()]) {
// Pins are always exempt: never reclaim a generation a live pin needs.
if (gen > minPinned) break // committedGensAsc ascending → nothing newer is eligible either
if (deadline !== undefined && Date.now() >= deadline) break // budget spent — resume next pass
const delta = await this.getDelta(gen)
if (!noCaps) {
const violatesCount = maxGenerations !== undefined && remainingCount > maxGenerations