feat(runtime): root exit is graceful shutdown of the forest roots

The RFC 014 root-exit sweep hard-stopped every live slot once nothing was
runnable. That deferral privileged queued work over parked-with-a-pending-
wake work (a sleeper was killed, a queued cast was drained) and any attempt
to widen the notion of pending wake (timers, fd readiness) re-wedges the
run on the periodic-timer daemon the sweep exists to end.

Root exit now means "the program is done": finalize_actor delivers
request_shutdown to every forest root — each live actor whose parent is
the run (ROOT_PID) or is dead — synchronously, before the live-count
decrement. Supervisors cascade per child Shutdown policy; trapping actors
may Continue/drain with working timers and end the run when they stop
themselves; non-trapping actors are stopped outright. No forcing sweep.

Removes root_exited/root_swept, Pop::RootDrain and the idle-verdict
condition; adds tests/root_exit.rs.
This commit is contained in:
Claude (sandbox)
2026-08-19 07:11:46 +00:00
parent 9c8f59ca53
commit 250f31265b
4 changed files with 359 additions and 58 deletions
+14 -4
View File
@@ -77,10 +77,10 @@ Delivered surface:
`ServerBuilder::start` untouched; free `call` / `cast` / `whereis_server`;
`ServerRef::shutdown` + free `shutdown` as the sys-style synchronous stop.
- **Root-exit teardown** (final phase): the run's initial actor is the root;
when it exits, the scheduler's idle verdict stops the parked-forever remainder
(deferred past the queue drain, so actors with in-flight work finish rather
than unwinding on the stop). Closes the "app actor blocks AllDone" stall — see
Look into, below.
when it exits the run winds down. *(Reworked with the graceful-shutdown work:
root exit now delivers `request_shutdown` to every forest root — see
"Root exit" below and `tests/root_exit.rs`.)* Closes the "app actor blocks
AllDone" stall — see Look into, below.
Extends — does not retire — the "select exists; a unified per-process mailbox
still does not" invariant: 014 adds addressable *delivery*, not a unified inbox;
@@ -260,6 +260,16 @@ path the atomic-bool workaround stood in for. Re-check the urus crud repro to
confirm the workaround can be retired (the teardown is cooperative — an actor in
a tight loop with no observation point still can't be stopped).
**Update (graceful shutdown):** the RFC 014 sweep was a hard `request_stop` of
every live slot, deferred until nothing was runnable — which killed a sleeping
actor (timer pending) but drained a queued one, for no principled reason. It is
now the OTP semantics: root exit = "the program is done" = `request_shutdown`
to every **forest root** (live actor whose parent is the run or is dead), run
synchronously on the root's finalize path. Supervisors cascade with their child
`Shutdown` policies; trapping actors may `Continue`/drain (timers keep working)
and end the run when they stop themselves; non-trapping actors are stopped
outright — `join` what you need finished. No forcing sweep follows.
---
## Invariants & gotchas (respect these across all cycles)