perf(runtime-core): close idle stores concurrently at shutdown - #2669
Merged
Merged
Conversation
|
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…ssues-shutdown-close
This was referenced Sep 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #2430
Root cause
The terminal
memory_graph_reconciliationstore close ends inStoreRuntimeRegistry::close_idle_for_shutdown, which closed every idle SQLite runtime one after another inside a singlespawn_blocking. Each close drains its readers, joins its writer, and runs the writer's shutdownTRUNCATEcheckpoint. The stores are independent: separate files, writer threads, and reader pools. So the shutdown waited for the sum of the checkpoints and thread hand-offs.Measured on the #2430 harness (details below, temporary scaffolding since removed). In the test, every close copies the WAL that the fixture's SIGKILLed
initdaemon left behind. That WAL is the whole store: 634 frames (2.5 MB) forglobal.db,user-sessions.db, and the projectsessions.db. Per store, the thread CPU was:TRUNCATEcheckpoint: 5–6 ms, with 2 fsyncsAt 3–5% quota every blocking hand-off waits for the next 100 ms CFS period, so the serial chain stacked periods. At 5%, one run took 500 ms for store 0 and then 400 ms for store 1. The drain quiesce poll never looped (
polls=0in every run), so the 5 ms poll interval is not a factor. Neither checkpoint fsync is redundant, because aTRUNCATEcan only drop the WAL after the database file is durable.Change
close_idle_for_shutdowngives each reserved runtime its own blocking close and joins all of them. The first failure is kept in reservation order, a join error is typed per close, and dropped handles detach rather than abort, as before. No deadline, drain window, or bound changed.Evidence
Behavior test, fail-before/pass-after:
shard_runtime::registry::tests::attachment::shutdown_close_drains_every_idle_runtime_concurrently. It holds both idle runtimes' drains until both have entered. Withorigin/master'sclose.rsswapped in, it fails:every idle runtime drains while the others are still draining left: 1 right: 2. With this branch, it passes. The production-path testterminal_shutdown_truncates_every_released_session_store_walstill passes: two real session stores now close concurrently, and each keeps a 0-byte WAL, so the checkpoint still returns every committed frame.Throttled SIGTERM loop (the #2430 / #2644 harness, real debug CLI). The test is
daemon_sigterm_exits_while_authenticated_project_client_is_connected, driven through aTRACEDECAY_TEST_BINwrapper:systemd-run --user --scope -p MemoryMax=6G -p MemorySwapMax=1GCPUQuotaonce it logsevent=daemon_readycpu.statis sampled every 5 ms from outside the scopeBefore and after use the same
core_cli_suitetest binary. "master" is 8d4fd5e and "fix" is this change on the same base.The 5% miss on the final binary spent 1000 ms in
background_drain(thegit_watcherandsession_syncowners) and never reached store close.Store-close phase at 3%, over all 22 runs of each:
In passing runs, SIGTERM-to-exit CPU was 82 ms median on master and 62 ms on the fix.
Hotpath (
--features hotpath, 5%, 4 runs each). On master,runtime_core.registry.close_idle_for_shutdownequals the sum of itsrusqlite.attachment.draincalls: 1.00/1.40/1.50/2.02 s against 1.00/1.40/1.50/1.92 s, i.e. serial. On the fix, the drain sum exceeds the enclosing wall time: 1.80/2.00/2.10/3.09 s of drains inside 1.00/1.00/1.20/1.59 s, i.e. concurrent. Thedaemon.shutdown.store_closemedian dropped from 1550 to 1150 ms.At 3% the Hotpath build does not separate the two: its own collector threads (
hp-threads,hp-functions,hp-cpu-baseline, …) used about 22 ms of the store-close window's CPU. For that reason the plain binary's pass rates and cgroup CPU are the 3% evidence.A final one-run journey on the merged binary (
final-q3-1, 3%): client drain 302 ms, store close 901 ms (28.1 ms CPU), exited within the 3 s bound.Focused suites (merged with origin/master 154a0eb):
tracedecay-global-db --lib: 387 passedtracedecay-runtime-core --lib: 455 passedtracedecay-store-runtime --lib: 124 passed (1 ignored, pre-existing)tracedecay --lib -- daemon::store_runtime_tests daemon::tests::bootstrap daemon::engine project_open: 138 passedcore_cli_suite -- tool_daemon_test: 41 passedcargo clippy -p tracedecay-runtime-core -p tracedecay-store-runtime --all-targets -- -D warnings: cleancargo fmt --all -- --check: cleanAfter merging origin/master 410e8f8, these re-ran and passed:
daemon_suite -- store_shutdown_checkpoint_test: 3 passed. This is the production stop journey: graceful stop, and stop during a project open, both truncate the profile and project store WALs.tracedecay-runtime-core --lib shard_runtime::registry: 65 passedcore_cli_suite -- tool_daemon_test: 41 passedripwire:
--edit-check=close_idle_for_shutdown: unchanged, 3 callers compatible.--quality-delta=origin/master..HEAD: gating=0. One minor verbosity row onclose_idle_for_shutdown(64→68 LOC; it was already over the bar). One dead-code row on the new#[tokio::test], which ripwire does not see as registered.Why this is
Refs, notFixesThe issue's measurement used 5% and 3% quotas. 5% holds (31/32 on the fix across both binaries; the one miss was in
background_drain). 3% is 85% (29/34), not reliable. Two things remain.initdaemon.client_drainat 1.2–2.0 s, where the in-flight project open's join costs 37–54 ms CPU, andbackground_drainat 1.0 s. The exit tail afterstore_close_completealso takes 0.4–1.0 s.The whole SIGTERM-to-exit path needs about 60–80 ms of CPU against the 90 ms that 3 s at 3% allows. Any of those phases can push a run past 3 s.