Skip to content

feat: /status + MCP status/projects always answer via a background snapshot - #342

Merged
flupkede merged 5 commits into
developfrom
features/status-snapshot
Oct 16, 2026
Merged

flupkede merged 5 commits into
developfrom
features/status-snapshot

Conversation

@flupkede

Copy link
Copy Markdown
Owner

Why

Field case 2026-10-16: while a full SCIP rebuild wave ran (dotnet restore + scip-csharp children starving the machine), GET /status stopped responding — the one endpoint that must always answer. Root cause: it measured inline in the request path (28 repos × filesystem probes + SCIP LMDB env opens + a sysinfo refresh, synchronous on OS-starved async workers); the MCP status tool and the projects list shared the same defect and timed out with it.

What

  • One measurer, background only: a serial background task (spawn_blocking; sleep starts after the pass completes, so under load the cadence stretches instead of piling measuring tasks onto a starved machine) computes a StatusSnapshot — repo statuses, projects payload, CPU, helper availability — into a slot whose lock is held only to swap an Arc. Cadence: CODESEARCH_STATUS_REFRESH_SECS (default 2s).
  • O(1) answers everywhere: GET /status, MCP status(kind=index) and status(kind=projects) read the snapshot; before the first pass they answer "starting" immediately (projects: empty list + explicit warming warning through the terminating warnings channel). No request-path filesystem access remains on any of the three.
  • Load value for calling agents: every /status answer carries snapshot_age_secs and a load object (text_indexing / symbol_indexing / errored / refresh_interval_secs).
  • Compatibility: all legacy /status JSON fields preserved (remote TUI keeps working); scoped (project/group) DB-backed status paths untouched; stdio-mode projects keeps its disk probe (no background task exists there).

Tests

6 new: handler contract (starting shape; stored pass = legacy fields + load counters + numeric age), MCP starting answers for index and projects, handle roundtrip, empty-fleet compute, refresh-interval env (override/unparseable/zero/empty, #[serial] + EnvRestore).

Validation

cargo fmt --check clean; clippy --all-targets -D warnings clean; cargo test --lib --bins 891+887 green.

GET /status ran repo_statuses_lightweight() + a sysinfo refresh in the
request path: across 28 repos that is dozens of filesystem probes and a
SCIP LMDB env open, synchronous on async workers that a heavy rebuild
wave (dotnet restore + scip-csharp children) OS-starves. Field case
2026-10-16: /status and the project list timed out for minutes while
symbol indexing ran - the one endpoint that must always answer was the
one that could not.

Now a single background task measures (spawn_blocking, one pass at a
time - under load the cadence stretches instead of piling measuring
tasks onto a starved machine) and stores a StatusSnapshot in a slot
whose lock is only ever held to swap an Arc. The handler is O(1): it
answers the stored snapshot with snapshot_age_secs for staleness and a
load summary (text_indexing / symbol_indexing / errored /
refresh_interval_secs) so calling agents can throttle themselves. Before
the first pass completes it answers "starting" immediately. Cadence:
CODESEARCH_STATUS_REFRESH_SECS, default 2s.
The scope-free serve branch of index_status called
repo_statuses_lightweight() inline - the same measuring-on-request-path
problem that made GET /status time out under a rebuild wave. It now
reads the background snapshot via ServeState::latest_status_snapshot()
and answers "starting" before the first pass completes, so the MCP
status tool responds in microseconds at all times.
Three unit tests over the snapshot logic: handle None-before-first-pass
+ roundtrip + age, compute on an empty fleet (incl. latest_status_snapshot
being None pre-store), and the CODESEARCH_STATUS_REFRESH_SECS cadence
env (7s override; unparseable/zero/empty all fall back to the 2s
default - zero would busy-loop the measuring task). Env var name lives
in constants.rs (STATUS_REFRESH_SECS_ENV). CHANGELOG under pending
[1.5.33] + highlights line; plan ticked.

Validation: cargo fmt --check clean, clippy --all-targets -D warnings
clean (stage 2), cargo test --lib --bins 889+885 all green.
Final-review findings: (1) list_projects (MCP status kind=projects) was
the last read endpoint still measuring inline - metadata reads, try_read
stats and lock probes per repo on the request path, the exact second
timeout the user reported on 2026-10-16. The per-repo probe pass moved
into compute_projects_snapshot() (the only measurer, called from the
background task); list_projects clones the stored payload and answers
immediately; pre-first-pass it answers an empty list with an explicit
warming warning (channel terminates - never a silent empty answer).
RepoInfo gains Clone for the slot-out clone. (2) Handler-contract tests:
status_handler starting-shape + full stored pass (legacy fields, load
counters, numeric age) and MCP status/projects pre-first-pass answers.

Review-fixes:
- [Important][Test] changed handler behavior had no test -> direct async
  calls of status_handler and the status tool pin the starting shapes,
  legacy JSON fields and the load counters.
- [Design] list_projects still measured inline -> snapshot-ized via
  compute_projects_snapshot; user's second timeout path now covered.
@flupkede
flupkede merged commit 65e8408 into develop Oct 16, 2026
6 checks passed
@flupkede
flupkede deleted the features/status-snapshot branch October 16, 2026 13:54
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