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.
129 lines
3.7 KiB
Rust
129 lines
3.7 KiB
Rust
//! gen_statem lifetime parity with gen_server: a machine lives until it
|
|
//! stops, is shut down, or is killed — its refs are addresses. And
|
|
//! `gen_statem::run_named` runs a machine inline as the current actor, so it
|
|
//! is a direct `ChildSpec` child addressed by name.
|
|
|
|
use smarm::gen_statem::{self, GenStatemName, Reply};
|
|
use smarm::supervisor::{ChildSpec, OneForOne, Restart, Shutdown};
|
|
use smarm::{monitor, request_shutdown, run, sleep, spawn, DownReason};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::Duration;
|
|
|
|
#[derive(Default, Clone)]
|
|
struct Log(Arc<Mutex<Vec<&'static str>>>);
|
|
impl Log {
|
|
fn push(&self, e: &'static str) {
|
|
self.0.lock().unwrap().push(e);
|
|
}
|
|
fn get(&self) -> Vec<&'static str> {
|
|
self.0.lock().unwrap().clone()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
enum S {
|
|
On,
|
|
}
|
|
|
|
struct D {
|
|
log: Log,
|
|
trap: bool,
|
|
n: u64,
|
|
}
|
|
|
|
enum Cast {
|
|
Inc,
|
|
StopNow,
|
|
}
|
|
enum Call {
|
|
Get(Reply<u64>),
|
|
}
|
|
|
|
smarm::gen_statem! {
|
|
machine: Sm { state: S, data: D };
|
|
event: Ev { cast: Cast, call: Call, info: () };
|
|
context(data, prev, cx);
|
|
|
|
enter {
|
|
S::On => { data.log.push("enter"); if data.trap { cx.trap_exit() } },
|
|
}
|
|
|
|
on S::On => {
|
|
cast Cast::Inc => { data.n += 1; prev },
|
|
cast Cast::StopNow => stop,
|
|
call Call::Get(r) => { r.reply(data.n); prev },
|
|
shutdown => { data.log.push("shutdown"); cx.stop(); prev },
|
|
state_timeout => unhandled,
|
|
timeout _ => unhandled,
|
|
}
|
|
|
|
terminate { data.log.push("terminate"); }
|
|
}
|
|
|
|
fn d(log: &Log, trap: bool) -> D {
|
|
D {
|
|
log: log.clone(),
|
|
trap,
|
|
n: 0,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn dropping_last_ref_does_not_end_machine() {
|
|
let log = Log::default();
|
|
let l = log.clone();
|
|
run(move || {
|
|
let m = Sm::start(S::On, d(&l, true));
|
|
let pid = m.pid();
|
|
m.send(Ev::Cast(Cast::Inc)).unwrap();
|
|
assert_eq!(m.call(|r| Ev::Call(Call::Get(r))).unwrap(), 1);
|
|
let mon = monitor(pid);
|
|
drop(m);
|
|
sleep(Duration::from_millis(30));
|
|
assert!(
|
|
mon.rx.try_recv().unwrap().is_none(),
|
|
"machine must outlive its refs"
|
|
);
|
|
assert_eq!(l.get(), vec!["enter"]);
|
|
request_shutdown(pid);
|
|
assert_eq!(mon.rx.recv().unwrap().reason, DownReason::Exit);
|
|
});
|
|
assert_eq!(log.get(), vec!["enter", "shutdown", "terminate"]);
|
|
}
|
|
|
|
const SM: GenStatemName<Sm> = GenStatemName::new("lifetime-sm");
|
|
|
|
#[test]
|
|
fn run_named_is_a_direct_supervised_child() {
|
|
let log = Log::default();
|
|
let l = log.clone();
|
|
run(move || {
|
|
let l2 = l.clone();
|
|
let sup = spawn(move || {
|
|
let l3 = l2.clone();
|
|
OneForOne::new()
|
|
.child(
|
|
ChildSpec::new(Restart::Permanent, move || {
|
|
gen_statem::run_named(SM, Sm::new(S::On, d(&l3, true))).expect("name free");
|
|
})
|
|
.shutdown(Shutdown::Infinity),
|
|
)
|
|
.run();
|
|
});
|
|
sleep(Duration::from_millis(10));
|
|
gen_statem::send(SM, Ev::Cast(Cast::Inc)).unwrap();
|
|
assert_eq!(gen_statem::call(SM, |r| Ev::Call(Call::Get(r))).unwrap(), 1);
|
|
// Normal self-exit → Permanent restart → fresh data, same name.
|
|
gen_statem::send(SM, Ev::Cast(Cast::StopNow)).unwrap();
|
|
sleep(Duration::from_millis(30));
|
|
assert_eq!(gen_statem::call(SM, |r| Ev::Call(Call::Get(r))).unwrap(), 0);
|
|
request_shutdown(sup.pid());
|
|
sup.join().unwrap();
|
|
assert!(gen_statem::whereis_machine(SM).is_none());
|
|
});
|
|
assert_eq!(
|
|
log.get(),
|
|
vec!["enter", "terminate", "enter", "shutdown", "terminate"]
|
|
);
|
|
}
|