Repository navigation
Part of #3736: T2-F2 daemon run on the native runtime - #4003
Merged
Merged
Conversation
|
ScriptedAlchemy
enabled auto-merge
October 15, 2026 06:54
ScriptedAlchemy
disabled auto-merge
October 15, 2026 06:55
ScriptedAlchemy
marked this pull request as draft
October 15, 2026 06:55
Contributor
There was a problem hiding this comment.
🔍 Devin Review: 1 flag
Not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
The three project_server_lifecycle hops ran as spawned Tokio tasks that a dropped caller only aborted, so their work (a cloned StoreAdministration, the carried admission lease) was dropped later on a Tokio worker, possibly after the clients owner reported drained. on_tokio now polls and drops the work in the caller's task with the Tokio runtime entered.
NativeTask now implements Future, and NativeTaskSet joins native tasks in completion order and aborts the rest on drop, as JoinSet does. The daemon's ClientTasks becomes an alias of it, and NativeTask::wait goes back to borrowing its handle.
A registration that missed the Tokio runtime had already aborted the running consumer of the data root, then released its own entry, leaving the root with no consumer. The handle is now resolved first, and the return names why no consumer started.
Both production callers already hold the handle. The inner lookup returned silently on a miss, closing the listener unserved.
The installed handle now sits in a clearable slot. main uninstalls it before shutdown_background, so a late tokio_handle call is a counted miss instead of a handle to a dead runtime. A separate test binary covers the fallback from a native root and the uninstall.
The native root test now asserts native_thread_writer_waits is 0 and drops the tokio_runtime_missing assertion, which could not fail with the handle installed. The receipt is logged as daemon_runtime_hazards so it no longer reads like a shutdown fault.
NativeTask is a Future now, so clippy's async_yields_async flagged the test that returned one from block_on.
A native worker has no ambient Tokio runtime, so tokio::spawn, spawn_blocking, Handle::current, JoinSet::spawn and the tokio::time free functions are now lint errors. Test modules, which run on an ambient Tokio runtime, carry a module allow; project_server_lifecycle carries one for its Tokio-polled timers.
The 242 module allows also switched off the #3829 tracing set_default guard in those modules. A repository check replaces the lint.
scripts/check-ambient-tokio.py resolves each production file's tokio imports and fails on tokio::spawn, spawn_blocking, Handle::current, JoinSet::spawn and the tokio::time free functions outside test code. An exception names its reason in an ambient-tokio comment; the project_server_lifecycle module declaration carries the one exception. The Repository gates job runs the check and its self-test, and Bazel runs the self-test as //scripts:ambient_tokio_check_test.
ScriptedAlchemy
marked this pull request as ready for review
October 15, 2026 13:48
ScriptedAlchemy
enabled auto-merge
October 15, 2026 13:51
This was referenced Oct 15, 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 #3736
T2-F2 of the native runtime cutover:
tracedecay daemon runnow drives its root future and every client connection on the Asupersync runtime. The daemon still owns one multi-thread Tokio runtime. The Tokio sites that remain reach it only through an explicitnative_runtime::tokio_handle(site)handle, never through an ambientHandle::current()from a native thread.What changed
tracedecay-rusqlite-runtimenative_runtime:install_tokio_runtime(handle)puts the Tokio runtime in a clearable slot, anduninstall_tokio_runtime()empties it.tokio_handle(site)returns the runtime entered on the calling thread, else the installed one, and counts a miss as before.on_tokio(site, work)returnsNoneon a counted miss, else the work wrapped inInTokioContext. The wrapper polls and drops the work with the Tokio runtime entered, in the caller's own task, so Tokio timers and spawns inside it work from a native thread and dropping the caller drops the work at once.in_tokio_context(handle, work)does the same with an explicit handle.NativeTaskimplementsFuture.NativeTaskSet<T>joins native tasks in completion order and aborts the rest on drop, asJoinSetdoes.tracedecay-climain.rs: every command runsnative.block_on(run(cli)). Fordaemon run(CommandRuntime::NativeWithTokio),mainbuilds the Tokio runtime with the same settings as before and installs it. Afterrunreturns, which is after the shutdown coordinator has logged every owner's receipt, it uninstalls the handle and callsshutdown_background(), so a latetokio_handlecall is a counted miss rather than a handle to a dead runtime.daemon/bootstrap.rs:ClientTasks, an alias ofNativeTaskSet, replaces the three clientJoinSets. A refused spawn is logged asdaemon_client outcome=runtime_unavailable.daemon_client outcome=error error=<message>, as the Unix loop does. It used to logdaemon_client outcome=<message>.process_runtime()calls are deleted.event=daemon_runtime_hazardsreceipt withtokio_runtime_missing_total()andnative_thread_writer_waits().daemon/project_server_lifecycle_hops.rs(new): threeproject_server_lifecycleentry points that native code awaits run underon_tokio. They arecancel_retained_session_history(bootstrap root),schedule_user_profile_host_admission_replay_for_identity(connection handshake) andawait_user_profile_host_admission_replay_for_identity(retained profile reads). That file still polls Tokio timers, and this PR does not read or edit it. The hops run in the caller's task, so the connection admission task-local stays in scope and a dropped client releases everything the hop holds before the clients owner reports. A miss on the last hop is a non-retryabletokio_runtime_unavailable. The file's other entry points are awaited only inside Tokio tasks: project open, retirements, and the shutdown runner.hook_v2_replay_consumer::register_hook_v2_replay_consumerresolves the Tokio handle before it touches the replay registry, and returnsReplayConsumerStart(Started,AlreadyRunning,NoTokioRuntime,RegistryPoisoned) instead of abool. A miss used to abort the data root's running consumer first.serve_loopbacktakes the Tokio handle as a parameter. Its inner lookup returned silently on a miss.scripts/check-ambient-tokio.py(new) fails the Repository gates job when production code callstokio::spawn,tokio::task::spawn,tokio::task::spawn_blocking,tokio::runtime::Handle::current,JoinSet::spawnorspawn_blocking, or atokio::timefree function (sleep,sleep_until,timeout,timeout_at,interval,interval_at). It readscrates/*/src, resolves each file'suse tokio::...imports (aliases, groups, globs), and skips test files,#[cfg(test)]and test-feature items and the files they load. An exception needs an// ambient-tokio: <reason>comment at the call, or above amod x;line to cover that file. The one exception is theproject_server_lifecycledeclaration, for its 3 Tokio timeouts that run on Tokio tasks or inside theon_tokiohops.//scripts:ambient_tokio_check_testruns its self-test. On origin/master the check reports 22 ambient calls, the same sites this PR moves. It replaces the earlierclippy.tomlentries, whose 258 test-module allows also switched off the test: scoped tracing subscribers miss callsites cached by other tests #3829 tracingset_defaultguard there.Remaining Tokio sites
project_retirement.rsspawn_and_track,spawn_fallible_retirement;remote_recovery_lifecycle.rs(track_tokio_retirement)project_server_lifecycleproject_open_admission.rsproject-open taskproject_server_lifecycleProjectOpenTaskClaim::Failedinvocation_executor.rsin-process invocation (outer and inner spawn)spawn_for_worktreeDaemonInvocationError::Unavailablehttp_application.rsloopback listener, dashboard-apiserve_loopbackconnections/remote-deletionscallsproject_server_lifecyclemcp/tools/handlers/dashboard.rshosted dashboardConfigerror before bindhook_v2_replay_consumer.rsconsumer,spool_opener.rsopenerNoTokioRuntime, registry unchanged; opener not startedmcp/server/hook_dispatch.rs×2, dashboard-apistate.rsscheduler reconcilelifecycle.rstask owner,settlement.rsdispatch set,project_host_admission_replay.rstool_dispatch_runtime_unavailable; no replay workertokio_handlesites (7, unchanged)project_server_lifecycle_hopsNone; non-retryabletokio_runtime_unavailablescripts/check-ambient-tokio.pyfinds no ambient Tokio call in production code.Tests
mcp_suitenative_root_daemon_test::daemon_on_the_native_root_serves_tool_mcp_and_dashboard_then_stops_clean(new) runs the real binary. It covers an MCP session (initialize and a readiness read), a CLI tool call, the hosted dashboard started through MCP, and an authenticated/api/capabilitiesrequest. It then sends SIGTERM and asserts a clean exit,native_thread_writer_waits=0on the hazards receipt, a completed store close, and no unfinished owner, client drain timeout, incomplete project-server shutdown, missing reactor or panic in the log.daemon/tests/lifecycle.rsa_client_dropped_mid_tokio_hop_releases_the_hop_before_the_drain_receipt(new): a client is dropped while its hop parks on a Tokio timer and the Tokio worker is busy. The hop's resource and the client's admission slot must be released when the clients owner reports.native_runtimetests (new):a_native_task_uses_tokio_timers_and_spawns_in_tokio_context,dropping_a_tokio_hop_drops_its_work_at_once,dropping_a_native_task_set_aborts_its_running_tasks.tokio_runtime_slot(new test binary, because the slot is process-global): anative_runtime::block_onroot with no Tokio entered misses, reaches the installed runtime, and misses again after the uninstall.hook_v2_replay_consumera_registration_without_tokio_leaves_the_running_consumer_in_placeand dashboard-apiloopback_server_serves_from_a_native_task_with_an_explicit_handle(new).daemon/tests/lifecycle.rsclient shutdown and panicking-connection tests driveClientTasks.Mutation checks (each reverted afterwards):
Gates
At
b14a8a2acb(origin/master503059beddmerged, with #4011, #4013, #4014, #4017 and #4018):bazel build --config=clippy --keep_going //crates/...: Build completed successfully.//:rustfmt_checkand//scripts:bazel/gen_builds.py -- --check: completed successfully.python3 scripts/check-ambient-tokio.py: checked 2489 production source files; no ambient Tokio call.//scripts:ambient_tokio_check_testPASSED.//crates/tracedecay:unit_test,:daemon_suite,:mcp_suite,:session_suite,:transport_acceptance_suite,:runtime_acceptance_suite,:dashboard_api_testand:hooks_lsp_suitePASSED.//crates/tracedecay-cli:core_cli_suiteand:host_journeys_suitePASSED.//crates/tracedecay-rusqlite-runtime:unit_test,:tokio_runtime_slot,//crates/tracedecay-mcp:unit_testand//crates/tracedecay-dashboard-api:unit_testPASSED.Latency diagnosis
The previous ABBA (8 runs each at host load 20 to 292) put the flip's short-read p50s 13 to 15 % behind base. The measurements below do not depend on host load, and none of them shows a per-request cost on the flip. The gap came from host load, not from the flip.
Probe: one private daemon over the small corpus. 6 client workers run a fixed mix (3 × callers, grep and search, then 1 context), one CLI process per call. Release builds: base
42489e6324against fliped907e0cc6.perf stat -p <daemon>over 1200 calls, 4 runs each, net of the daemon's idle rate measured just before, with the operator's real HOME as the perf gate uses it. Base used 103 to 109 M user and 22 to 24 M kernel instructions per call. The flip used 103 to 111 M user and 21 to 24 M kernel. That is the same work, so theon_tokiohops, the per-connectionNativeTaskand theNativeTaskSetadd no measurable CPU.sched:sched_wakingcounted wakeups made by daemon threads, over 600 calls with an isolated HOME. Base made 326 to 332 per call, about 84 of them with a Tokio worker on one side. The flip made 260 to 318 per call and none on Tokio: this request mix never leaves the native runtime. Handoffs are where host load turns into latency, and the flip has no more of them.The perf gate does not isolate HOME, so its private daemon ingests the operator's real
~/.claudeand~/.hermessessions while it measures. That kept the daemon 0.7 to 1.4 cores busy between requests, in some runs for more than 6 minutes after init. This PR does not change that. It is a source of noise between runs.Peak RSS and latency (ABBA)
Release builds (
--config=release): base503059bedd(the master merged here) against this branch atb14a8a2acb. Harness and knobs as before:scripts/perf-gate.sh, small corpus, 6 workers for 60 s, private daemon. Four rounds of A B B A, 8 runs per binary, all PASS. Each run started only once the 1-minute load average was below 50. The load average at each run boundary was 17.5 to 47.1. Three runs started above 30: base-1 at 43.6, flip-8 at 47.1 and base-8 at 45.1.Request latency, startup latency and peak RSS are inside the 10 % bound. The read p50s differ by 0 to 2 ms, about 1 ms on the 33 ms callers read. That is the size of one step in a run's millisecond-rounded percentile, and the paired probe shows no gap at the same load. Index time is not a section 7 measurement, and its runs spread widely: base 21.1 to 46.0 s, flip 23.5 to 35.2 s. The index phase polls readiness once a second. The previous ABBA had index time at −0.5 %.