stdiod: deterministic child shutdown — terminal report precedes any respawn ack (T-74) - #39
Conversation
|
ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing |
There was a problem hiding this comment.
1 issue found across 5 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/stdiod/crates/edison-stdiod/src/daemon_tests.rs">
<violation number="1" location="crates/stdiod/crates/edison-stdiod/src/daemon_tests.rs:8">
P2: Windows test runs fail before exercising the supervisor because every spawned fixture hard-codes `/bin/sh`; use a platform-specific shell fixture or gate these Unix process tests with `#[cfg(unix)]`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| DesiredServer { | ||
| server_id: server_id.into(), | ||
| name: server_id.into(), | ||
| command: "/bin/sh".into(), |
There was a problem hiding this comment.
P2: Windows test runs fail before exercising the supervisor because every spawned fixture hard-codes /bin/sh; use a platform-specific shell fixture or gate these Unix process tests with #[cfg(unix)].
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/stdiod/crates/edison-stdiod/src/daemon_tests.rs, line 8:
<comment>Windows test runs fail before exercising the supervisor because every spawned fixture hard-codes `/bin/sh`; use a platform-specific shell fixture or gate these Unix process tests with `#[cfg(unix)]`.</comment>
<file context>
@@ -0,0 +1,152 @@
+ DesiredServer {
+ server_id: server_id.into(),
+ name: server_id.into(),
+ command: "/bin/sh".into(),
+ args: vec!["-c".into(), script.into()],
+ env: Default::default(),
</file context>
df3e0e0 to
9537c3d
Compare
|
Latest push addresses the two ordering findings:
Declining the Generated by Claude Code |
9537c3d to
f1657b4
Compare
…wn ack ChildServer::shutdown now kills the child, drops the stdin channel, and JOINS the stdout pump (2s bound, abort fallback) instead of aborting it blindly - so the old child's terminal server_offline is queued on the outbound channel before shutdown returns, and every respawn path in the supervisor awaits shutdown before try_spawn sends the replacement's server_spawn_result. This turns the backend's spawn-ack error clearing from a heuristic into a rule, specced as PROTOCOL.md T-74. Two new tests pin the guarantee at both seams (pump join, offline-before-ack on the outbound channel) and fail against the old blind-abort behavior. Also moves daemon.rs's inline test module to a sibling daemon_tests.rs per crate convention. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NgN7YdfLT41kWnWqe1MPZa
Review follow-ups on the shutdown-ordering work. - shutdown joins both frame pumps under one shutdown budget instead of joining stdout and aborting stdin. Either pump can hold the one-shot terminal report: stdout takes it at EOF, stdin takes it when a write to the child fails. Aborting the stdin pump while it was mid-send dropped the report and left the respawn ack with nothing in front of it, which is the hang T-42 exists to prevent. Sequential joins share one deadline so the total wait is unchanged; stderr is still aborted outright. - When the budget does run out, shutdown produces the terminal report itself rather than leaving it stranded in the aborted pump. The latch that hands out the report is consumed by the pump that took it, so nothing else can ever send it once that pump is gone. Semantics of the fallback, since the interesting case is a pump that was cancelled with a send in flight: - report_terminal flips a report_sent flag immediately after the send completes, with no await in between, so cancellation cannot land between queueing the frame and recording it. An aborted pump has therefore either sent the report or left it unsent, never an unknown. - abort only requests cancellation, so shutdown waits for the pump to actually stop (200ms grace) before deciding. A pump that will not cancel in that window is left alone and no fallback is sent: at most once is preserved, and the backend's staleness teardown fails the in-flight calls. - The fallback send is try_send, so a wedged outbound channel cannot block shutdown. A full channel means the report is dropped, which the same staleness teardown covers. PROTOCOL.md T-74 said the ordering guarantee rested on joining the stdout pump. It now states the guarantee as ordering rather than delivery, names the fallback, and records why T-43's at-most-once still holds across it. Tests: two shutdown tests wedge the outbound channel so a pump parks mid-report, one with stdout holding the report and one with stdin, and assert exactly one server_offline reaches the wire. The stdin one fails against a shutdown that joins only stdout; both fail without the fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NgN7YdfLT41kWnWqe1MPZa
d04f313 to
cee91eb
Compare
f1657b4 to
8404f81
Compare
Stack 3/3 (base: #38). The highest-risk PR of the stack — concurrency change on the reconcile hot path. Please review the shutdown sequence closely.
ChildServer::shutdown()previously killed the child then blindlyabort()ed the pump tasks, making it a coin flip whether the old child's terminalserver_offlinereached the wire before, after, or never relative to a respawn'sserver_spawn_result. Downstream this forced a receiver-side clearing heuristic and test workarounds in edison-watch.New sequence: kill the process group (lock released before joining — the pump's
child_exit_statustakes the same lock), drop the stdin channel, then join the stdout pump with a 2s bound (abort fallback on timeout), then abort stdin/stderr pumps. Every respawn path in the supervisor already awaitsshutdown()beforetry_spawn, so end to end: the old child'sserver_offlineis enqueued before the new child's spawn ack. Specced as PROTOCOL.md T-74; the backend's spawn-ack error clearing becomes a rule instead of a heuristic.Two new tests pin the guarantee at both seams (pump-join in
proc_tests.rs, offline-before-ack on the outbound channel indaemon_tests.rs) — both verified load-bearing by reverting to blind-abort and watching them fail. Also movesdaemon.rs's inline test module to siblingdaemon_tests.rsper crate convention.Review checklist / known limits:
daemon_tests.rsis#[cfg(unix)]-gated and stdiod CI is ubuntu-only. A Windows smoke of kill+respawn (or a windows-latest CI job) before release would close the gap.Verified: fmt/clippy/
cargo test --workspace(114 tests), AI-writing check. Cross-repo: the full edison-watchtests/stdio_tunnelsuite (80 tests) ran green against this binary, including 4 consecutive runs of the formerly-flaky pair with the workarounds deleted.Merge order: edison-watch's fault-e2e PR asserts the T-74 ordering strictly — merge this before it.
🤖 Generated with Claude Code
https://claude.ai/code/session_01NgN7YdfLT41kWnWqe1MPZa
Generated by Claude Code
Summary by cubic
Makes child shutdown deterministic so the old child's terminal
server_offlineis always queued before any respawnserver_spawn_result. This enforcesPROTOCOL.mdT-74 and removes receiver-side heuristics for spawn-ack error clearing.Bug Fixes
ChildServer::shutdownto kill the process, mark exit, drop the outbound frame channel to end stdin cleanly, and join both frame pumps under a shared 2s budget; aborts are followed by a 200ms grace; stderr is still aborted.shutdownemits the terminal error viaOutgoingHandle::try_send(non-blocking), so ordering holds even when the WS writer is wedged.report_sentflag set immediately after a completed send; fallback only triggers when no send was recorded.Supervisorrespawn paths awaitshutdown()beforetry_spawn, ensuringserver_offlineprecedes the replacement’s spawn ack; added the ordering contract intry_spawndocs.PROTOCOL.mdT-74 to state ordering (not delivery) and document the fallback.Refactors
daemon.rstodaemon_tests.rsper crate convention.Written for commit 8404f81. Summary will update on new commits.