fix(coordinator): make multi-daemon stop best-effort instead of aborting on first failure - #2896
Conversation
|
😎 Merged successfully - details. |
|
Automated review by Claude — fully automated, no human in the loop. The best-effort rework looks correct: each daemon's stop runs in its own One gap worth addressing: there is no regression test. This changes coordinator stop semantics from short-circuit to best-effort, and the central guarantee — that a daemon appearing after a failing one in Generated by Claude Code |
|
Best-effort is the right call here, and the caller does still learn which daemons failed: Two things: No test. This is a behavior change in distributed teardown with no RED test at any tier. Nothing pins the actual property — that daemon B's failure still lets daemon A stop. The restart path widens the blast radius. |
|
Thanks — both points are fair. Test. Added Restart blast radius. Confirmed — Generated by Claude Code |
|
@phil-opp the Trunk merge queue failed for this PR. See the Trunk merge-status comment for details. Posted as a new comment so GitHub sends an email — Trunk's sticky comment is edited in place and won't trigger a notification. |
…ing on first failure stop_dataflow iterated over dataflow.daemons and used `?` on each send/receive, so the first daemon that errored (e.g. a disconnected daemon whose connection lookup returns None) returned early from the whole function. Daemons later in iteration never received StopDataflow, leaving their nodes running as orphans while the CLI only saw the first error. Attempt the stop on every daemon, aggregate per-daemon failures, and return a combined error only after all have been attempted. This mirrors the established best-effort pattern in run::rollback_spawned_daemons. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CCyEn8YqaYQYwh3RLjEZ7B
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CCyEn8YqaYQYwh3RLjEZ7B
… the rest Adds a RED-capable regression test for stop_dataflow: two daemons where the one iterated first (a BTreeSet ordered by machine id) is unreachable. Under the old first-failure `?` code the healthy daemon was never reached and its nodes were orphaned; the test asserts the healthy daemon still receives a StopDataflow and that the error aggregates the per-daemon outcome. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CCyEn8YqaYQYwh3RLjEZ7B
d2651a8 to
dd5fb6c
Compare
|
Pushed a fix — this is ready to re-queue. The merge-queue run failed to compile with Fix: rebased onto the latest Sorry for the batch disruption to #2897/#2898 — both are fine on their own and should pass once re-tested without the stale build of this PR in the batch. Generated by Claude Code |
Re-reviewed after the new commit. It adds the regression test the earlier pass asked for: The best-effort loop itself reads correctly (per-daemon work scoped in its own future, failures aggregated and surfaced only after every daemon is attempted). Leaving restart-under-partial-failure ( Generated by Claude Code |
Issue
stop_dataflow(binaries/coordinator/src/handlers.rs) iterates overdataflow.daemonsand uses?on each step — the connection lookup,send_and_receive, and the reply match. The first daemon that errors returnsErrfrom the whole function; the CLIStop/StopByNamehandlers just forward that error and do nothing else. Daemons later in iteration never receiveStopDataflow.Failure scenario
A 3-daemon dataflow on daemons
A < B < C(dataflow.daemonsis a sortedBTreeSet). DaemonBdisconnects. User runsdora stop:AgetsStopDataflowand stops its nodes.B,daemon_connections.get_mut(B)returnsNone→bail!→ function returns early.Cnever gets the stop — its nodes keep running as orphans, consuming resources, untilCindependently disconnects.The CLI only sees the error about
B.Fix
Make the loop best-effort: attempt the stop on every daemon, collect per-daemon failures, and return an aggregated error only after all daemons have been attempted. This mirrors the established pattern already used by
run::rollback_spawned_daemons, whose comment explicitly states "one daemon's failure must not abort rollback for the remaining daemons."The success path is unchanged (all daemons succeed → same
Ok); only the failure path changes from short-circuit to best-effort with an aggregated error listing each failing daemon.Known limitation — restart widens the blast radius (follow-up)
Best-effort stop interacts with the restart path (
initiate_restart,binaries/coordinator/src/lib.rs). Step 2 of a restart stops the old dataflow and, onErrfromstop_dataflow, replies with the error and returns without registering aPendingRestart. With best-effort stop, adora restartacross daemonsA, B, CwhereBis unreachable now actively stopsAandCand then abandons the restart — leaving the dataflow half-dead, where the old first-failure abort touched fewer daemons before giving up. Same failure class, wider reach.This is a pre-existing restart-under-partial-failure semantics question (should a partial stop still proceed to a
PendingRestart, or roll forward?), not something this PR should decide unilaterally — flagging it here for a maintainer call / follow-up rather than changing restart behavior in a stop-teardown fix. Thanks to @phil-opp for catching this.Validation
stop_dataflow_is_best_effort_when_a_daemon_fails(handlers.rs): two daemons where the one iterated first is unreachable; asserts the healthy daemon still receives aStopDataflowand that the error aggregates the per-daemon outcome. RED under the old?code (the healthy daemon is never reached).cargo test -p dora-coordinator— all pass (incl. the new test).cargo clippy -p dora-coordinator --all-targets -- -D warnings— clean.cargo fmt --all -- --check— clean.