Skip to content

fix(store-runtime): cancel store opens at daemon shutdown - #2644

Merged
ScriptedAlchemy merged 4 commits into
masterfrom
fleet/fix-unowned-issues-shutdown
Sep 29, 2026
Merged

ScriptedAlchemy merged 4 commits into
masterfrom
fleet/fix-unowned-issues-shutdown

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Owner

Refs #2430

Root cause

A daemon stopped while an admitted project open was inside its session-store mount waited for that mount to finish (measured in #2430 by the master-reds lane on #2612/b896d9f8db). The mount had no safe points: StoreRuntimeRegistry::begin_or_join_open runs resolve + publish (including the Initialize schema install) in a detached task that never observed shutdown, attach_registered then ran the whole registered-schema admission (ensure_attached_registered_schema, the 127 ms of the 165 ms mount unthrottled) without a cancellation point, and close_idle_for_shutdown skipped runtimes still Opening. Under CPU starvation the mount outlived the 2 s cooperative drain, the open was aborted, and an aborted open could not close its stores exactly.

Change

  • StoreRuntimeRegistry owns a shutdown cancellation (cancel_opens_for_shutdown). New opens are refused with typed StoreRuntimeRegistryFailure::OpenCancelled; an in-flight open stops before resolve and before publish; an open that finishes publishing after the cancel closes its unpublished runtime and fails OpenCancelled instead of publishing.
  • The registered schema install observes that token at both entry points (Initialize via RegisteredSchemaInstallationV1::cancellation, daemon attach via admit_and_attach_for_daemon): before classification, before the admission transaction, and before every statement of it. A cancel rolls the transaction back and fails with typed TraceDecayError::store_open_cancelled (store_open_cancelled, retryable), so the store keeps exactly its prior schema; a cancel after commit lets the idempotent index builds and validation finish, so it is never half-installed. An Initialize-mode cancel also aborts the prospective file.
  • close_idle_for_shutdown cancels and joins in-flight opens before it scans, so none is skipped or publishes behind the close.
  • The daemon's project_open shutdown owner cancels store opens for every mounted session registry before it drains project opens. No deadline, drain window, or test bound changed.

Evidence

Fail-before (fix neutralized: cancel_opens_for_shutdown made a no-op and both schema checkpoints disabled; on origin/master the new APIs do not exist):

shutdown_close_cancels_and_joins_an_in_flight_open ... FAILED  (shutdown close cancels in-flight opens: Elapsed(()))
cancelled_daemon_attach_leaves_the_store_empty_or_fully_installed_and_reopens ... FAILED
  cancelled at [], completed at [1, 21, 41, ..., 421] of 411 polls

Pass-after on this branch (rebased on 6bc1138): both pass. The schema test sweeps the cancel across every 20th statement boundary of a real daemon attach on an empty existing profile-sessions store: every cancel before the commit ends store_open_cancelled with zero schema objects, every later one ends with the exact full object set, and a reopen after each succeeds with the full set.

Focused suites (non-zero counts): tracedecay-global-db --lib 386 passed; tracedecay-runtime-core --lib 454 passed; tracedecay-store-runtime --lib 122 passed (1 ignored, pre-existing); tracedecay --lib -- daemon::store_runtime_tests daemon::tests::bootstrap daemon::engine project_open 138 passed; core_cli_suite -- tool_daemon_test 41 passed (includes daemon_sigterm_exits_while_authenticated_project_client_is_connected and daemon_sigterm_stops_an_in_flight_open_at_the_next_store_boundary). cargo clippy -p tracedecay-domain -p tracedecay-runtime-core -p tracedecay-global-db -p tracedecay-store-runtime -p tracedecay --all-targets --features tracedecay/test-helpers,tracedecay/test-transport -- -D warnings clean; cargo fmt --all -- --check clean.

Throttled-SIGTERM loop (the #2430 harness, real debug CLI)

daemon_sigterm_exits_while_authenticated_project_client_is_connected driven through a TRACEDECAY_TEST_BIN wrapper that runs every daemon under systemd-run --user --scope -p MemoryMax=6G -p MemorySwapMax=1G and sets CPUQuota on the test's daemon once it logs event=daemon_ready. Same test binary for both sides; "before" is the master daemon (10b4ff2), "after" this branch.

quota before pass after pass project_open join, before project_open join, after
7% 12/12 12/12 ≤202 ms ≤196 ms
5% 8/8 6/8 ≤398 ms ≤804 ms
4% 19/20 18/20 ≤303 ms ≤397 ms
3% 2/8 4/8 1 run 2095 ms (cooperative window expired, open aborted, no warmup outcome) ≤600 ms, every open ends project_server_warmup outcome=cancelled
5% (rebased final binary) — 8/8 + 8/8 — ≤203 ms

Final binary, one run (rebased-q5-1):

[tracedecay] event=daemon_ready bootstrap_elapsed_ms=328
[tracedecay] event=project_server_warmup outcome=cancelled project=/tmp/.tmporFevp
[tracedecay] event=daemon_shutdown outcome=owner_joined owner=project_open status=clean elapsed_ms=0 since_arm_ms=601
[tracedecay] event=daemon_shutdown outcome=store_close_complete elapsed_ms=701 deadline_remaining_ms=11298

Left open (why this is Refs, not Fixes)

After this change no run spends the cooperative window inside the open, but at ≤5% CPU the test still misses 3 s in some runs on both sides at the same rate: the rest of the shutdown sequence alone exceeds it. In those runs every phase advances in ~100 ms CFS quota periods and the terminal memory_graph_reconciliation store close takes 0.8–1.4 s (e.g. before-q3-2: project_open 694 ms, store close 1399 ms; after-q3-6: project_open 297 ms, store close 1102 ms). That store-close cost is a separate measured target and is not changed here.

ripwire: --edit-check on admit_and_attach_for_daemon, ensure_attached_registered_schema, install_and_commit_registered_schema, install_from_authorized_connection, close_idle_for_shutdown: all callers compatible. --quality-delta=f5bd745eed..HEAD: the close_idle_for_shutdown complexity/verbosity regression it flagged was fixed by moving the join into cancel_and_join_opens_for_shutdown; the remaining gating rows are 18–47-token if cancelled { return Err(..) } Ok(()) shape matches against unrelated crates and a test row-collection loop, and the dead-code rows are cross-crate callers ripwire does not resolve.

A daemon stopped while a project open was inside its session-store mount
waited for the mount to finish: the registry open ran detached, the schema
install could not be interrupted, and the shutdown close skipped runtimes
still opening. Under CPU starvation the mount outlived the 2 s cooperative
drain, the open was aborted, and the process missed its exit.

The store runtime registry now owns a shutdown cancellation. The project
open shutdown owner cancels it for every mounted session registry. New
opens are refused with a typed OpenCancelled; an in-flight open stops
before resolve and before publish, and one that finishes publishing after
the cancel closes its unpublished runtime instead of publishing it. The
registered schema install, both at initialization and at daemon attach,
checks the token before every statement of its admission transaction and
rolls back with a typed store_open_cancelled, so a store is left either
with its prior schema or fully installed. The shutdown close cancels and
joins in-flight opens before it closes idle runtimes, so none is skipped.
@changeset-bot

changeset-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 25ab1ce

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-29T14:03:40.352356Z f17d16e PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f17d16e7ae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +165 to +166
if let Some(registry) = entry.registry.get() {
registry.cancel_store_opens_for_shutdown();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cancel registries that are still initializing

When shutdown races with the first profile access, this OnceCell is still empty even though DaemonSessionRuntimeRegistryV1::open_with_session_maintenance has already created its StoreRuntimeRegistry and is awaiting the profile store/schema open. get() therefore skips precisely that in-flight registry, while the project-open/bootstrap future has no cancellation-aware select around initialization; shutdown can still wait for the full mount and exceed its cooperative deadline. Keep a cancellation authority in the registry entry before starting the OnceCell initializer, or otherwise propagate shutdown cancellation into an initializer whose cell is not yet populated.

Useful? React with 👍 / 👎.

…ssues-shutdown

# Conflicts:
#	crates/tracedecay-global-db/src/registered.rs
#	crates/tracedecay-global-db/src/schema_stages.rs
#	crates/tracedecay-store-runtime/src/session_registry/maintenance.rs
@ScriptedAlchemy
ScriptedAlchemy merged commit 8d4fd5e into master Sep 29, 2026
14 of 15 checks passed
@ScriptedAlchemy
ScriptedAlchemy deleted the fleet/fix-unowned-issues-shutdown branch September 29, 2026 15:13
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