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:
@@ -0,0 +1,36 @@
|
||||
//! Under `smarm-trace`, every actor the root-exit sweep reaches is recorded
|
||||
//! as a `root_sweep` event — the way to *see* unsupervised leftovers. One test
|
||||
//! per binary: the trace file is process-global.
|
||||
#![cfg(feature = "smarm-trace")]
|
||||
|
||||
use smarm::gen_server::{self, GenServer, GenServerCtx};
|
||||
use smarm::run;
|
||||
|
||||
struct Quiet;
|
||||
impl GenServer for Quiet {
|
||||
type Call = ();
|
||||
type Reply = ();
|
||||
type Cast = ();
|
||||
type Info = ();
|
||||
type Timer = ();
|
||||
fn init(&mut self, _: &GenServerCtx<Self>) {}
|
||||
fn handle_call(&mut self, _: ()) {}
|
||||
fn handle_cast(&mut self, _: ()) {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forgotten_server_shows_up_as_root_sweep() {
|
||||
let path = std::env::temp_dir().join(format!("smarm_root_sweep_{}.json", std::process::id()));
|
||||
std::env::set_var("SMARM_TRACE_FILE", &path);
|
||||
run(|| {
|
||||
let srv = gen_server::start(Quiet);
|
||||
srv.call(()).unwrap();
|
||||
drop(srv); // forgotten: nobody supervises it, nobody holds it
|
||||
});
|
||||
let trace = std::fs::read_to_string(&path).expect("trace file written");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
assert!(
|
||||
trace.contains("root_sweep stopped"),
|
||||
"expected a root_sweep line for the non-trapping leftover; got:\n{trace}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user