core/teardown: an unconfirmed stop is not a clean stop
Codex's re-review of the branch returned "approve with follow-ups" — no blocking findings, five P3s. All five are applied here rather than carried as debt, since each is a few lines. The one with user-visible consequences: `stop_host` returned a bare "was sharing" bool, so the single case where the availability-first policy gives up (SIGKILL queued, reap never confirmed) still sent `ScreenShareStopped` with nothing else. The UI would say sharing had ended while pixelpass might still be alive and fanning out — a claim the user cannot see through. `shutdown` now returns `StopOutcome`, `stop_host` returns `Option<StopOutcome>`, and an unconfirmed *user-initiated* stop raises a UI error naming the stray process. Session and viewer teardown discard the outcome deliberately: nobody is waiting on an answer there, and the residual risk is already logged. Also: the three failure diagnoses in `shutdown` (the signal never left, the child ignored it, the wait itself broke) were collapsed into one log line and are now distinct — they mean different things to whoever reads the log. The second cancellation gate is the one worth keeping. The review pointed out that all cancellation coverage sat in the *graceful* wait, so a mutant that disarmed the wrapper between the two waits would survive. It was right, with a wrinkle: the naive mutant does not compile, because the child is borrowed from `self` for the whole function — the borrow checker is doing real work here. The restructured form (`self.child.take()` once cooperation has failed) does compile, and the pre-existing mid-wait test passes it. `cancelling_shutdown_after_the_kill_leaves_the_fallback_armed` kills it. Mutation-verified, both new gates: reporting an unconfirmed stop as `Reaped` fails exactly `a_failed_wait_is_not_treated_as_a_confirmed_reap`; disarming between the waits fails the new cancellation test (and the failed-wait test, which also asserts armedness) while leaving the old mid-wait test green — which is the proof the new test is not redundant. The logging split is diagnostics only and has no gate; said plainly rather than dressed up as covered. Docs: the "four mutations" line is now an explicit table naming each target and its test, with 0c's pair counted under 0c; and the aggregate teardown latency is recorded as a deferred item with a trigger (a fourth routine child, or a measured teardown over 5 s) instead of an unwritten known cost. 638 lib tests, clippy clean, fmt clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -832,10 +832,21 @@ mutation is retired as vacuous.** Reached independently by both reviewers, then
|
||||
- **Rejected: an integration test built to preserve the number five.** It would pay for a
|
||||
full iroh core plus test-only observability and prove only that a method was called — not
|
||||
the ordering invariant, which is the thing that actually breaks.
|
||||
- The 0b gate is therefore **four mutations** (no wait · reversed field order · no reap loop ·
|
||||
plus the 0c pair below), each killed by its own named test, with 4-vs-5 verified separated:
|
||||
reversing the field order leaves the reap test green and removing the reap loop leaves the
|
||||
ordering test green.
|
||||
- The 0b gate is therefore **four mutations**, enumerated exactly (round 16 P3-5 — the earlier
|
||||
wording said "three plus the 0c pair", which reads as five and blurred what 0b owns):
|
||||
|
||||
| # | mutation | killed by | status |
|
||||
|---|----------|-----------|--------|
|
||||
| 1 (old gate 3) | remove the wait after the host kill | `explicit_shutdown_reaps_the_host_before_the_aec_can_unload` | killed now |
|
||||
| 2 (old gate 4) | reverse `ScreenshareTeardown`'s field declaration order | `the_aec_unloads_after_the_children_on_the_drop_path` | killed now |
|
||||
| 3 (old gate 5) | remove the reap loop from `ReapOnDrop::drop` | `dropping_a_guard_kills_and_then_reaps_the_child` | killed now |
|
||||
| 4 (old 1) | remove the teardown at the hoisted post-loop call site | — | **deferred to the phase-9 row** *drop the controller / close the command channel while sharing* |
|
||||
|
||||
4-vs-5 separation verified: reversing the field order leaves the reap test green, and
|
||||
removing the reap loop leaves the ordering test green. **0c's own pair (no SIGINT · no
|
||||
SIGKILL fallback) is counted under 0c, not here**, along with the round-15/16 additions
|
||||
(disarm the wrapper at entry · disarm it between the waits · treat a wait error as a reap ·
|
||||
report an unconfirmed stop as clean · zero the grace).
|
||||
|
||||
**Round 15 (2026-07-26) — review of the 0b/0c-peerspeak implementation returned two blocking
|
||||
findings, both accepted.** Recorded because both are the same shape: a defence that existed
|
||||
@@ -854,6 +865,32 @@ but was disarmed exactly when it was needed.
|
||||
elapsed-time assertion compared against `STOP_GRACE` itself, so zeroing the constant left
|
||||
it trivially true. `the_grace_is_a_real_interval` now pins the constant to a band.
|
||||
|
||||
**Round 16 (2026-07-26) — the re-review of the 0b/0c-peerspeak fixes returned *approve with
|
||||
follow-ups*: no blocking findings, five P3s, all five applied before the merge.** The two that
|
||||
carry design content:
|
||||
|
||||
- **An unconfirmed stop was reported to the user as a clean one.** `stop_host` returned a bare
|
||||
"was sharing" bool, so the one case where availability-first gives up (SIGKILL queued, reap
|
||||
never confirmed) still emitted `ScreenShareStopped` with no warning — the UI would say
|
||||
sharing ended while pixelpass might still be fanning out. `ReapOnDrop::shutdown` now returns
|
||||
`StopOutcome`, `stop_host` returns `Option<StopOutcome>`, and an `Unconfirmed` user-initiated
|
||||
stop raises a UI error naming the stray process. Session/viewer teardown discards the outcome
|
||||
on purpose: no user is waiting on an answer there and the risk is already logged.
|
||||
- **Cancellation coverage only reached the graceful wait.** The mid-wait test could not kill a
|
||||
mutant that disarmed the wrapper *between* the two waits. Verified: the naive form of that
|
||||
mutant does not compile (the child is borrowed from `self`), but the restructured form —
|
||||
`self.child.take()` once cooperation has failed — compiles, and the pre-existing test passes
|
||||
it. `cancelling_shutdown_after_the_kill_leaves_the_fallback_armed` kills it.
|
||||
|
||||
**Deferred item — aggregate teardown latency (round 16 P3-4).** Bounds are per child, not per
|
||||
teardown. Sequential drain gives `2 × STOP_GRACE` per unconfirmed child inline (≈4 s), plus
|
||||
`REAP_BUDGET` (250 ms) per child on the `Drop` path: three wedged children ≈6 s of command-loop
|
||||
stall, ≈12.75 s worst case including drop retries. Accepted as-is for 0b — one host plus one or
|
||||
two viewers is the real shape, and concurrency here would mean detaching children from the
|
||||
session that owns the AEC's lifetime. **Trigger to revisit: a fourth tracked child becomes
|
||||
routine, or a measured teardown exceeds 5 s.** The fix, when triggered, is to drain viewers
|
||||
concurrently while still owned by `shutdown_children` — not to detach them.
|
||||
|
||||
**Round 1 — 13 items, 12 accepted.** Phase reorder (AEC machine before dry-run); typed capture
|
||||
plan (accepted, moved *earlier* than proposed); Phase 3 five-part gate; tag-consumption gating;
|
||||
Phase 6 matrix mandatory; 0b unwind backstop restored **and my mutation test corrected — it
|
||||
|
||||
Reference in New Issue
Block a user