Files
alkssh/tasks/implementation/w2-spawn-return.md
T
glm-5.3-flash 2e5a7c9cee feat(serve): land the spawn-and-return plumbing + exit/status mechanics (w2-spawn-return)
- 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.
2026-10-28 11:04:17 +00:00

8.7 KiB
Raw Blame History

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
w2-dispatch-decline-core
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-status sent 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.md rules 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-status is 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 --check pass

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) and report_exit_status_then_close(&Handle, channel, N) (the cloned-handle report with the mapping.md exit_status row's ordering baked in: exit-status before close). Both re-exported #[doc(hidden)] from the crate root — the SessionHandler §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-3 mapping.md material; 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_stub clones session.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 plain fns (not async) — an async fn holding &Session across the spawn makes the handler future non-Send (russh requires + Send on handler futures; Session is not Sync), so the clone is taken synchronously and only the handle moves into the spawned future. Wire shapes are unchanged (all w2-dispatch-decline-core rows 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's SessionHandler (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_path of a release-gated serving future reporting exit-status(7) + close through the cloned handle. Two rows, each with a Contract 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 observes ExitStatus{7} strictly before Close (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 a Notify-with-notify_waiters shape could miss against multiple spawned paths); the handler-side timestamps ride parking_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: the spawn_serving_path choke point (the ADR-006 decision-4 rule, in code, never awaited inline) and report_exit_status_then_close (exit-status before close through the cloned Session::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-Send shape that forcing async fn stubs 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: two Contract 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.