feat(daemon): a stop that names its reason, and an inventory that cannot drift
§7's shutdown choreography, taking the shape from exonum's `ApiManager` rather
than transliterating `serve.zig`.
**Teardown moves to the outer function.** `ApiManager::run` splits into `run`
and `run_inner` so the servers stop "in any case" — every early return from the
loop still tears down. `Supervisor::shutdown` consuming `self` already made the
ordering unforgeable at the TYPE level; nothing made it unforgeable at the call
site, where one `?` on an error path is enough to skip it. `Daemon::run` now
performs the whole sequence and there is no other way to stop.
**Three ways to stop, not one.** `serve.zig` models a signal arriving. exonum
selects over the signal AND the server's own termination; habitat's supervisor
returns a `shutdown_mode` from its loop. Both are right, and a daemon that waits
only for a signal HANGS when its listener dies of something else — a lost bind,
an accept loop that returned. That process is unkillable except by SIGKILL and
explains nothing on the way out. `StopCause` names which happened, and
`Outcome::is_clean` is false for a server that fell over however clean the join
was.
**The two Zig shutdown flags are retired, not ported.** They exist because a
watcher thread polling every 100ms cannot tell "the signal arrived" from "the
server stopped" without a second flag. In `Daemon::run` they are statements in
order, so there is nothing to race. The `select!` is `biased` so the answer is a
fact about the futures rather than about tokio's branch order —
`test_boot_window_sigterm` fails if the arms are swapped, which is the mutation
that proves the assertion is about the ordering and not about luck. It also
asserts the server was NEVER POLLED: the signal had already arrived, and that
window is precisely what the two flags protected.
**The task inventory is code.** Dimension 7.5 asks that every `concurrency.md`
thread-map row be supervised or explicitly deferred to a named milestone.
`inventory.rs` carries all eleven with a `Disposition`, and the suite asserts
the row count and that no row is silent — a deferral without a milestone is an
omission wearing a label. Two are supervised (`hub_pump`, `otlp_export`), seven
are deferred with milestones, and two are RETIRED because the thread existed
only to work around the absence of async:
- the signal watcher, above;
- the M139 deadline scheduler — a treap-backed registration map and a worker
thread so one thread could interrupt another's blocked socket. `timeout` at
the call site is the same guarantee with no shared map to keep consistent and
no generation check to get wrong.
`docs/architecture/concurrency.md` gains the Rust task map beside the Zig thread
map, and the doc and the table are checked against each other rather than one
being prose about the other.
Measured, `cargo llvm-cov -p agentsfleetd --all-features`:
lines 100.00% · functions 100.00% · regions 99.12%
Four uncovered regions, all `?` paths in `Display` impls that cannot be taken
because writing to a `String` cannot fail. The `log`-bridge trap bit once more
and is fixed at the source: an `error_code` field calling `.as_str()` inline was
hoisted to a `let`.
make lint-rustd → ✓ [rustd] Lint passed
bash audits/ufs.sh → OK: no violations across 2369 file(s)
cargo test -p agentsfleetd --all-features → 8 suites, 0 failed
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> K
Kishore Kumar committed
fc8093b3f32f7f89d24674bcd0b9bac4d38cc2a1
Parent: c6d3899