feat(gen_server,gen_statem): lifetime is the actor's — refs are addresses; inline named run

Root cause behind the "pin the endpoint" gotcha and the trapping-wrapper
pattern: a gen_server had two lifetime authorities — its refs (last one
dropped → inbox closes → exit) and, when supervised, its supervisor. OTP has
one: a process lives until it stops, is shut down, or is killed; a pid is an
address. Root exit now shutting down every forest root removes the reason the
ref-governed idiom existed (a forgotten server no longer hangs the run), so
adopt the one rule:

- The server/machine loop holds one inbox sender for its life; the inbox
  never closes. GenServerRef / GenStatemRef are addresses. Explicit close is
  `shutdown()`; a forgotten one is swept at root exit.
- `NamedGenServerBuilder::run()` / `gen_statem::run_named(name, m)` run the
  loop inline as the current actor: a server is a direct ChildSpec child,
  gets the supervisor's shutdown as handle_shutdown / a shutdown row, re-binds
  its name on restart, and is addressed by name. The wrapper in
  examples/graceful_shutdown.rs is gone.
- gen_statem gains GenStatemName + whereis_machine/send/call/shutdown by name
  (parity with gen_server); the macro gets `Sm::new`.
- Root-exit sweep records `Event::RootSweep { target, trapping }` under
  smarm-trace ("root_sweep shutdown|stopped"): unsupervised leftovers are
  visible rather than silently owned-by-refs.
- Named start() name-clash path stops the spawned actor instead of relying
  on ref drop.

Tests: tests/gen_server_lifetime.rs, tests/gen_statem_lifetime.rs,
tests/root_sweep_trace.rs (feature-gated); three existing tests that used
drop-closes-inbox now use shutdown(). Docs/README/ROADMAP/Deep Dive updated.
This commit is contained in:
Claude (sandbox)
2026-08-19 17:47:31 +00:00
parent 849a424c8e
commit 415effb2e9
15 changed files with 715 additions and 97 deletions
+4 -3
View File
@@ -1620,8 +1620,9 @@
wait: <code>select</code> priority is <strong>Down arms › Watcher arm › info channels (declaration order) ›
inbox</strong>, rebuilt each turn. A hot inbox can't starve a death notice or a system message;
conversely a hot info channel <em>can</em> starve the inbox — deliberately. A closed info arm is
silently dropped from the set; a closed <em>inbox</em> (every <code>ServerRef</code> gone) is graceful
shutdown.</p>
silently dropped from the set. The inbox never closes — the loop holds one sender for its whole
life, so a <code>GenServerRef</code> is an address, not an owner: the server ends only by
<code>StopHandle::stop</code>, a shutdown, a hard stop, or a panic.</p>
<h3>Death needs no monitor</h3>
<p>Server death detection falls out of channel closure. Already dead → the inbox is closed and
@@ -1700,7 +1701,7 @@
</div>
<div class="module-card">
<div class="module-name" style="color:var(--red)">Panics in <code>terminate()</code></div>
<p>gen_server's <code>terminate()</code> runs from a drop guard, possibly mid-unwind. A panic inside it during an unwind is a double panic → process abort, no supervision tree to save you. On the panic and hard-stop paths keep it cheap, non-blocking, non-panicking. Only the graceful path (<code>handle_shutdown → Exit</code>, <code>StopHandle::stop</code>, inbox close) runs it outside an unwind, where it may do real work.</p>
<p>gen_server's <code>terminate()</code> runs from a drop guard, possibly mid-unwind. A panic inside it during an unwind is a double panic → process abort, no supervision tree to save you. On the panic and hard-stop paths keep it cheap, non-blocking, non-panicking. Only the graceful path (<code>handle_shutdown → Exit</code>, <code>StopHandle::stop</code>) runs it outside an unwind, where it may do real work.</p>
</div>
<div class="module-card">
<div class="module-name" style="color:var(--yellow)">Cold locks are leaf locks</div>