Skip to content

fix(pipeline,mcp,daemon): fail an over-budget index attempt whole with a named reason (#1997, #832) - #2112

Merged
DeusData merged 3 commits into
mainfrom
fix/decision-a-memory-budget-fail-whole
Sep 9, 2026
Merged

DeusData merged 3 commits into
mainfrom
fix/decision-a-memory-budget-fail-whole

Conversation

@DeusData

@DeusData DeusData commented Sep 8, 2026

Copy link
Copy Markdown
Owner

The indexing memory budget stops being advisory (decision A). Addresses #1997 and #832, and the OOM half of #1973/#1864.

Today the budget is enforced only inside parallel extract: after 40×3 ms backpressure spins still over budget, it latches bp_futile, logs soft_overshoot, and keeps admitting until the kernel OOM-kills the worker and the supervisor blames files. Now:

  1. Fail whole after one confirmation cycle. On the futility latch the pipeline runs one more collect+nap cycle; if still over budget it aborts the attempt with a new CBM_PIPELINE_ABORT_OVER_BUDGET (its own atomic flag, not the shared cancel token). Staging is discarded through the existing rc≠0 path, so the previously serving index is byte-identical and no partial graph is published. The worker exits 0 with a status:"error" response, so the daemon passes it through in one attempt with no recovery loop or file quarantine.
  2. Name the cause (index_repository reports an abort that preserved the index as an indistinguishable error #2020). index_repository renders reason:"over_memory_budget", previous_index:"preserved", budget_mb, peak_rss_mb, and a hint naming CBM_MEM_BUDGET_MB; distinct aborted_previous_preserved / persist_failed statuses replace the generic "Pipeline failed".
  3. Slice the worker budget by ACTIVE jobs, not the job limit (decision A follow-up): a lone worker may use the whole aggregate budget; concurrent jobs split it. This keeps fail-whole from biting a single-job host whose real peak exceeds aggregate/4.

Local verification (macOS): daemon_application 55, pipeline 276, mcp 314 (+4 skips), mem 53, index_supervisor 8, all green; Colima CI-lint (cppcheck + clang-format-20) passed; RED→GREEN→RED-on-revert on the new tests. CLI on the final binary prints the exact JSON, the DB is byte-identical after a fail-whole, and index_status still serves the old graph. Windows PR #2030 (Job Object cap) is complementary and must sit above this cooperative gate.

🤖 Generated with Claude Code

DeusData and others added 3 commits September 8, 2026 01:29
…t-overshooting

The extraction memory gate was advisory: when a full back-pressure cycle
ended still over budget it latched "futile" and kept admitting files, so a
host that could not hold the graph ran on until the kernel killed the
worker (#1997, #832) and the supervisor then quarantined innocent files.

Now the worker that latches futility runs ONE confirmation cycle (collect
+ bounded naps). If resident memory drained, the gate re-arms as before.
If it is still over budget, the attempt fails whole: every worker stops
pulling, extract returns the new CBM_PIPELINE_ABORT_OVER_BUDGET, the
orchestrator discards the staging DB on that non-zero code like any other,
and the previously serving generation is left byte-identical. The verdict
is a plain return value so it travels as a clean-exit worker response,
never a kill. The abort flag is its own atomic, not the shared cancel
token, which belongs to client cancellation.

The abort/persist codes move to pipeline.h alongside the new one (callers
legitimately distinguish them); FORCE_FULL_REINDEX stays internal.

Tests: the futility test now asserts the new code (the confirmation adds
one cycle to its bound), and a new test proves fail-whole preserves the
live DB byte-for-byte, keeps its node count, and leaves no stage residue.

Refs #1997 #832 #1973 #1864

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Martin Vogel <[email protected]>
index_repository rendered every non-zero pipeline code as "Pipeline
failed. Check repo_path exists and contains source files", which sent
people debugging a path that was fine (#2020). Three outcomes now say
what actually happened:

- CBM_PIPELINE_ABORT_OVER_BUDGET (#1997, #832): status "error" with
  reason "over_memory_budget", previous_index "preserved", the budget and
  peak RSS in MB, and a hint naming CBM_MEM_BUDGET_MB / CBM_WORKERS /
  excludes. The handler logs index.abort reason=over_memory_budget. The
  CLI prints the same JSON and exits 1; a supervised worker still exits 0
  with the response written, so the daemon passes it through verbatim
  instead of entering skip-and-continue recovery.
- CBM_PIPELINE_ABORT_PRESERVE_DB: status "aborted_previous_preserved"
  (retry; the previous index still serves).
- CBM_PIPELINE_PERSIST_FAILED: status "persist_failed" (disk/permissions).

The incremental test helper goes fail-closed on error responses so a
transient pre-publication abort surfaces once at the source instead of
cascading into dozens of downstream count failures.

Test: an in-process index over 64 files at a 1 MiB budget seam returns
the named error, leaves the project DB the same size on disk, and
index_status still reports the previous node count.

Refs #2020 #1997 #832

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Martin Vogel <[email protected]>
…e job limit

The daemon handed every index worker aggregate/physical_job_limit (a
fixed quarter by default) regardless of how many jobs were running, so a
lone worker on a host like #1864 was capped at a quarter of the memory
the daemon itself was allowed — and with the budget now failing whole
(decision A) that fixed slice would have turned an idle host into a
guaranteed over_memory_budget. Decision 2 (#1997, #832): the slice is
decided at spawn time as aggregate / jobs active right then (the job
being spawned included), so a lone worker may use the whole aggregate
and concurrent jobs split it. The explicit CBM_MEM_BUDGET_MB override
keeps its precedence inside the worker (bf46b91); the sub-byte-per-slot
guard is unchanged. The spawn line logs project, active_jobs,
aggregate_mb and slice_mb so a fail-whole verdict can be read against
the budget the worker really had.

Tests: sequenced admission proves first=aggregate, second=aggregate/2;
the racing 4-way admission test accepts any aggregate/1..4 slice (the
divisor is scheduling-dependent there); a scripted CLEAN worker
returning the over_memory_budget error response is passed through
verbatim on the first attempt — no recovery loop, no quarantine.

Refs #1997 #832 #1864

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Martin Vogel <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant