Summary
E2E Tests fails intermittently on macos-latest with:
Graceful shutdown timed out after 10000ms. Use --force to kill.
[ERROR] Broker still reported as running after down command
The Relaycast presence phase is already bounded (SHUTDOWN_RELAYCAST_PHASE_TIMEOUT = 2500ms, event_loop.rs:35), so that is no longer the cause. What remains is structural: the broker cannot observe SIGTERM while it is inside an event handler, so any slow handler delays shutdown by its full duration.
It is flaky, not broken — which is why it keeps getting misattributed
Same branch, same code, three runs:
| head |
E2E |
fdd8dc485 |
pass |
158fdd987 |
pass |
a85744806 |
fail |
main alternates the same way — 31947aa03 pass, 62acd3736 fail, 87ada407c fail. Only the macOS leg fails; ubuntu passes. This has already been misattributed once: it was bisected to #1431, and a controlled A/B (both broker binaries built, SIGTERM→exit timed directly, fresh workspace per run) measured 8.3–9.5s on both, statistically indistinguishable. #1431 is exonerated. Shutdown simply sits close enough to the CLI's 10s down --timeout that runner speed decides the outcome.
Mechanism
crates/broker/src/runtime/event_loop.rs:320-372:
while !self.shutdown {
let event = tokio::select! {
_ = tokio::signal::ctrl_c() => RuntimeEvent::CtrlC,
_ = self.lease_check.tick() => RuntimeEvent::LeaseTick,
_ = self.sigterm.recv() => RuntimeEvent::Sigterm,
...
};
match event {
... => self.handle_api_request(*request).await, // :349
... => self.handle_relaycast_message(message).await, // :360
... => self.handle_fleet_control_event(event).await, // :366
... => self.handle_terminal_control_event(event).await,// :372
}
}
self.sigterm.recv() is only polled inside the select!. Each handler is then awaited in the loop body, outside it. So while a handler runs, SIGTERM is not being observed at all — the signal waits for the handler to finish before the loop comes back round.
An agent registration handled on this path performs a network round trip to Relaycast. On a slow runner, one in-flight registration can consume most of the 10s window on its own, and the bounded presence phase afterwards cannot recover time already spent.
Why bounding the presence phase did not fix it
SHUTDOWN_RELAYCAST_PHASE_TIMEOUT bounds work done after shutdown_runtime is entered. It cannot help when the delay is in reaching that function, because the loop has not yet observed the signal. The two are in series, and only the second half is bounded.
Suggested direction, to challenge rather than follow
- Make handler dispatch cancellable so
SIGTERM wins immediately — e.g. run the handler as a branch of the same select!, or select! the handler future against the shutdown signal so the loop can abandon it.
- Or bound the handlers themselves, so no single event can consume the caller's shutdown window.
- Decide explicitly what a cancelled in-flight registration should do. Abandoning one mid-flight is a real semantic choice, not a detail — the identity may already be claimed server-side.
Definition of done
- With a deliberately slow Relaycast (block or delay
/v1/agents/*), SIGTERM still reaches shutdown promptly and node down completes inside its window. Demonstrate both directions: the test must fail against today's code and pass after, or it proves nothing.
- The E2E shutdown phase stops depending on runner speed. Given the flakiness, a single green run is not evidence — state how many consecutive macOS runs were observed.
- Note in the PR what happens to an in-flight registration that is cancelled.
Notes for whoever picks this up
- Do not re-bisect to
#1431. It has been measured and exonerated; a clean bisect boundary here is coincidence, because the mechanism is ambient latency crossing a fixed threshold rather than a code change.
- Branch
fix/e2e-graceful-shutdown-regression (160d5a2c2) carries an earlier, superseded attempt at this: a per-call SHUTDOWN_RELAYCAST_CALL_TIMEOUT of 3s. main's phase-scoped 2500ms bound is strictly better — a per-call timeout applied to N calls costs up to N times as much. That branch should be closed, not merged, but its investigation notes are worth reading.
- The macOS-only pattern is a symptom of the threshold, not a macOS bug. Do not spend time looking for a platform difference.
Summary
E2E Testsfails intermittently onmacos-latestwith:The Relaycast presence phase is already bounded (
SHUTDOWN_RELAYCAST_PHASE_TIMEOUT = 2500ms,event_loop.rs:35), so that is no longer the cause. What remains is structural: the broker cannot observeSIGTERMwhile it is inside an event handler, so any slow handler delays shutdown by its full duration.It is flaky, not broken — which is why it keeps getting misattributed
Same branch, same code, three runs:
fdd8dc485158fdd987a85744806mainalternates the same way —31947aa03pass,62acd3736fail,87ada407cfail. Only the macOS leg fails; ubuntu passes. This has already been misattributed once: it was bisected to#1431, and a controlled A/B (both broker binaries built,SIGTERM→exit timed directly, fresh workspace per run) measured 8.3–9.5s on both, statistically indistinguishable.#1431is exonerated. Shutdown simply sits close enough to the CLI's 10sdown --timeoutthat runner speed decides the outcome.Mechanism
crates/broker/src/runtime/event_loop.rs:320-372:self.sigterm.recv()is only polled inside theselect!. Each handler is then awaited in the loop body, outside it. So while a handler runs,SIGTERMis not being observed at all — the signal waits for the handler to finish before the loop comes back round.An agent registration handled on this path performs a network round trip to Relaycast. On a slow runner, one in-flight registration can consume most of the 10s window on its own, and the bounded presence phase afterwards cannot recover time already spent.
Why bounding the presence phase did not fix it
SHUTDOWN_RELAYCAST_PHASE_TIMEOUTbounds work done aftershutdown_runtimeis entered. It cannot help when the delay is in reaching that function, because the loop has not yet observed the signal. The two are in series, and only the second half is bounded.Suggested direction, to challenge rather than follow
SIGTERMwins immediately — e.g. run the handler as a branch of the sameselect!, orselect!the handler future against the shutdown signal so the loop can abandon it.Definition of done
/v1/agents/*),SIGTERMstill reaches shutdown promptly andnode downcompletes inside its window. Demonstrate both directions: the test must fail against today's code and pass after, or it proves nothing.Notes for whoever picks this up
#1431. It has been measured and exonerated; a clean bisect boundary here is coincidence, because the mechanism is ambient latency crossing a fixed threshold rather than a code change.fix/e2e-graceful-shutdown-regression(160d5a2c2) carries an earlier, superseded attempt at this: a per-callSHUTDOWN_RELAYCAST_CALL_TIMEOUTof 3s.main's phase-scoped 2500ms bound is strictly better — a per-call timeout applied to N calls costs up to N times as much. That branch should be closed, not merged, but its investigation notes are worth reading.