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
+6 -6
View File
@@ -88,7 +88,7 @@ impl GenServer for Lifecycle {
}
}
// init -> handle_call -> (drop last ref closes inbox) -> terminate.
// init -> handle_call -> shutdown -> terminate.
#[test]
fn init_and_terminate_run() {
let log = Arc::new(Mutex::new(Vec::new()));
@@ -96,9 +96,9 @@ fn init_and_terminate_run() {
run(move || {
let server = start(Lifecycle { log: log2 });
server.call(()).unwrap();
// Dropping the only ref closes the inbox; the server breaks out of its
// recv loop and runs terminate. run() will not return until it has.
drop(server);
// Refs are addresses: dropping one does not end the server. The
// explicit close does, and waits for terminate.
server.shutdown();
});
assert_eq!(*log.lock().unwrap(), vec!["init", "call", "terminate"]);
}
@@ -702,8 +702,8 @@ fn no_timer_survives_exit() {
let _ = server.call(()).unwrap(); // sync: periodic armed
smarm::sleep(Duration::from_millis(45)); // a couple of ticks
let mon = smarm::monitor(server.pid());
drop(server); // inbox closes → loop exits → guard drains timers
// Clean Down ⇒ the loop returned without the no-leak assert aborting.
server.shutdown(); // loop exits → guard drains timers
// Clean Down ⇒ the loop returned without the no-leak assert aborting.
assert!(mon.rx.recv().is_ok());
let at_exit = f_read.lock().unwrap().len();
smarm::sleep(Duration::from_millis(90)); // would be several more ticks