- src/serve.rs (new): the ADR-006 decision-4 mechanics in code — spawn_serving_path (the one choke point; nothing awaited inline) and report_exit_status_then_close (exit-status before close via the cloned Session::handle()), #[doc(hidden)] contract-test re-exports; a unit row pins spawn-returns-while-pending. - src/handler.rs: the four session-row seam stubs became spawned serving paths (failure-then-close unchanged; stubs are plain fns — an async stub holding &Session across the spawn is non-Send); open- level declines and the tunnel seam stub stay inline by class, per the new module-doc section. - tests/spawn_return.rs (new): two end-to-end wire rows — the client observes exit-status(7) strictly before close and after the handler returned; the handler-returns-fast row routes follow-up traffic while the serving path is still gated (POC-3 deadlock inversion). - tasks/implementation/w2-spawn-return.md — completed with notes. Verification (all green): cargo test; per-feature dispatch matrices (--features git/session/tunnel/sftp, --all-features); cargo clippy --all-targets/-all-features with -D warnings; cargo fmt --check; cargo doc --no-deps (no warnings); taskgraph validate.
8.7 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| w2-spawn-return | Wave 2 — spawn-and-return plumbing + exit/status reporting mechanics | completed |
|
narrow | medium | phase | implementation |
Description
Land the structural constraint in code (ADR-006 decision 4): russh at this pin runs server handler callbacks inline inside the session's packet loop — a blocking handler suspends all packet routing for the connection and deadlocks against the peer's immediate follow-up bytes (POC-3 proved by construction with receive-pack). Therefore:
- Every serving path is
tokio::spawned; handlers return fast. The dispatch's routing stubs become spawned serving paths (still declining stubs — the seams are wave 3), so the pattern is in place before any real mapping hangs on it. - Exit/status reporting mechanics — reporting from spawned tasks
rides the cloned
Session::handle():exit-statussent before channel close, from the spawned task. The derivation contract's rules (which code → which SSH exit shape) are wave-3 session-mapping material (mapping.mdrules 1–6); this task lands the mechanics — the handle clone, the exit-status send, the ordering-before-close — proven end to end. - The proof — a stub serving path that spawns, completes
asynchronously (after the handler has returned), and reports
exit-status(N)+ close through the cloned handle; the loopback harness client observes the exit-status arriving after the handler already returned. A handler-returns-fast assertion (the handler completes while the spawned task is still running) pins the constraint as a test, not a convention.
This is a load-bearing correctness constraint of the whole spine, not an implementation choice — the wave-3 mappings inherit the pattern mechanically from the stubs this task leaves behind.
Acceptance Criteria
- Every serving path (the dispatch stubs) runs in a
tokio::spawn; no handler hook awaits a serving path inline - Exit/status reporting from spawned tasks rides the cloned
Session::handle();exit-statusis sent before channel close - A harness test proves the mechanics end to end: the client
observes
exit-status(N)from a spawned stub that completed after the handler returned - A handler-returns-fast assertion pins the structural constraint as a test
cargo test,cargo clippy --all-targets -- -D warnings,cargo fmt --checkpass
References
- docs/architecture/decisions/006-mapping-bridge-and-dispatch.md (decision 4 — the structural constraint)
- docs/architecture/mapping.md §The structural constraint: spawn-and-return; §exit_status row (sent before close, from the spawned task)
- docs/plans/implementation.md §Wave 2 (the spawn-and-return bullet)
- docs/research/poc-3-mapping-bakeoff.md (the deadlock-by-construction evidence)
Notes
src/serve.rs(new): the spawn-and-return plumbing as mechanics, not convention —spawn_serving_path(serving future)(the one choke point for the ADR-006 decision-4 spawn; nothing it takes is ever awaited inline) andreport_exit_status_then_close(&Handle, channel, N)(the cloned-handle report with themapping.mdexit_statusrow's ordering baked in: exit-status before close). Both re-exported#[doc(hidden)]from the crate root — theSessionHandler§Test-surface pattern — as the contract-test surface the mechanics rows compose over the real wire. A unit row (spawn_returns_while_the_serving_path_is_still_pending— latch-gated future, spawn call returns first) pins spawn-then-return directly. The derivation contract's rules (which exit code → which SSH exit shape) stay wave-3mapping.mdmaterial; no rule logic landed here.src/handler.rs: the four session-row seam stubs (git_row_stub,pipe_row_stub,pty_row_stub,shell_row_stub) are now spawned serving paths —spawn_seam_stubclonessession.handle(),seam_stub_decline(handle, channel)runs the failure-then-close decline on the spawned task, and the hooks return without awaiting anything. A compile-level fact coded around: the row stubs are plainfns (notasync) — anasync fnholding&Sessionacross the spawn makes the handler future non-Send(russh requires+ Sendon handler futures;Sessionis notSync), so the clone is taken synchronously and only the handle moves into the spawned future. Wire shapes are unchanged (allw2-dispatch-decline-corerows pass unmodified with the spawned decline); the tunnel seam stub and every open-level/capability decline stay inline by class — they answer the open request before any channel is owned, so nothing spawns yet (documented in the new handler module-doc section §The spawn-and-return structural constraint; wave 3's accept-only-after-fabric-open flow is where the tunnel row gains its spawn).tests/spawn_return.rs(new): the end-to-end mechanics proof over the real wire — the russh server stack (duplex transport, harness client/harness transport posture) mounted over a minimal proof handler that is not the crate'sSessionHandler(the auth and dispatch hooks have their own suites; the proof subject is the mechanics) and that serves execs by composing the exported plumbing:spawn_serving_pathof a release-gated serving future reportingexit-status(7)+ close through the cloned handle. Two rows, each with aContract stamp::handler_returns_fast_while_the_spawned_serving_path_is_pending— the structural constraint as a wire test: after the exec hook has provably returned (a bounded wait on the handler-side timestamp; the client's exec returns when the request is on the wire, not when the hook ran) and with the serving path still gated, the client's follow-up traffic (a second session channel + second exec) is routed and dispatched normally — POC-3's follow-up-bytes deadlock shape inverted — and the still-gated no-report state is asserted before the release.spawned_stub_reports_exit_status_before_close_after_the_handler_returned— the client observesExitStatus{7}strictly beforeClose(message-by-message, no draining that could hide ordering), and the recorded timestamps prove the exit-status left after the handler had already returned (the literal criterion).
- The proof-handler design note: the gate is
tokio::sync::watch- based (no lost-wakeup race — a late subscriber sees the release, which aNotify-with-notify_waitersshape could miss against multiple spawned paths); the handler-side timestamps rideparking_lot::Mutex. - Feature independence: the mechanics are feature-independent (the
helpers exist in every build and the proof rows run in every build
they were verified under — default,
git,session,tunnel,sftp,--all-features); no feature-gated code was added, so no new decline-path coverage obligations arise.
Summary
Landed the spawn-and-return plumbing + exit/status reporting
mechanics (w2-spawn-return) in three files:
src/serve.rs—cargo test-pinned mechanics: thespawn_serving_pathchoke point (the ADR-006 decision-4 rule, in code, never awaited inline) andreport_exit_status_then_close(exit-status before close through the clonedSession::handle()), both#[doc(hidden)]contract-test re-exports; a unit row pins spawn-returns-while-pending directly.src/handler.rs— the four session-row seam stubs became spawned serving paths (handle clone taken synchronously — the non-Sendshape that forcingasync fnstubs would produce is coded around and recorded); failure-then-close wire shapes unchanged (all dispatch-core rows pass unmodified); open-level declines and the tunnel seam stub stay inline by class, documented; a new module-doc section carries the constraint and the wave-3 inherit-the-pattern note.tests/spawn_return.rs— the end-to-end mechanics proof: twoContract stamp:rows over the real wire (handler-returns-fast under follow-up traffic with the spawn still pending; exit-status before close, timestamps proving the report outlived the handler's return), driven by a proof handler composing the exported plumbing.
Verification (all green): cargo test · cargo test --features {git,session,tunnel,sftp} --test dispatch_core ·
cargo test --all-features · cargo clippy --all-targets -- -D warnings · cargo clippy --all-features --all-targets -- -D warnings · cargo fmt --check · cargo doc --no-deps (no
warnings). 93 lib/unit rows + the per-build dispatch matrices + the
two mechanics rows across 7 test binaries.