Files
smarm/examples/graceful_shutdown.rs
Claude (sandbox) 415effb2e9 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.
2026-08-19 17:47:31 +00:00

140 lines
5.3 KiB
Rust

//! Graceful shutdown, end to end: a supervised app tree, a server that
//! drains before it exits, and the two ways the whole thing winds down.
//!
//! Stopping an actor comes in two strengths, as in OTP:
//! - `request_stop(pid)` = `exit(Pid, kill)`: cooperative hard stop,
//! unwinds at the next observation point.
//! - `request_shutdown(pid)` = `exit(Pid, shutdown)`: a trapping target gets
//! an `ExitSignal { reason: Shutdown }` and winds
//! down on its own terms; a non-trapping one is
//! stopped outright.
//!
//! A supervisor traps exits. `request_shutdown(sup)` runs its ordered
//! shutdown — children in reverse start order, each per its `ChildSpec`
//! `Shutdown` policy (`Timeout(d)` default 5s, `Infinity`, `BrutalKill`) —
//! and the supervisor then returns normally.
//!
//! Two triggers are shown:
//! 1. **Root exit.** The run's root actor returning means "the program is
//! done": the runtime delivers `request_shutdown` to every top-level actor
//! (here: the supervisor). Trapping actors may keep running to drain and
//! end the run when they stop themselves; non-trapping ones are stopped.
//! 2. **An outside thread** (e.g. a signal handler) driving it via
//! `RuntimeHandle::request_shutdown` on the supervisor — the root then
//! just waits for the tree to come down.
use smarm::gen_server::{
GenServer, GenServerBuilder, GenServerCtx, GenServerName, ShutdownAction, StopHandle,
TimerHandle,
};
use smarm::supervisor::{ChildSpec, OneForOne, Restart, Shutdown};
use smarm::{sleep, spawn};
use std::thread;
use std::time::Duration;
/// A server with in-flight work: on shutdown it stops accepting, finishes what
/// it has (simulated with a ticking timer), then ends itself.
struct Drainer {
pending: u32,
stop: Option<StopHandle<Drainer>>,
timer: Option<TimerHandle<Drainer>>,
}
impl GenServer for Drainer {
type Call = ();
type Reply = ();
type Cast = ();
type Info = ();
type Timer = ();
fn init(&mut self, ctx: &GenServerCtx<Self>) {
ctx.trap_exit(); // opt in: shutdown arrives as handle_shutdown
self.stop = Some(ctx.stop_handle());
self.timer = Some(ctx.timer());
}
fn handle_call(&mut self, _: ()) {}
fn handle_cast(&mut self, _: ()) {}
fn handle_shutdown(&mut self) -> ShutdownAction {
println!(
"drainer: shutdown requested, {} items pending",
self.pending
);
self.timer
.as_ref()
.unwrap()
.tick_every(Duration::from_millis(20), ());
ShutdownAction::Continue // keep serving until drained
}
fn handle_timer(&mut self, _: ()) {
self.pending -= 1;
if self.pending == 0 {
println!("drainer: drained, stopping");
self.stop.as_ref().unwrap().stop(); // normal exit
}
}
fn terminate(&mut self) {
// Graceful path: this runs on the normal path and may block.
println!("drainer: terminate");
}
}
/// The server's name: how the rest of the app reaches it (and the only handle
/// that survives a restart).
const DRAINER: GenServerName<Drainer> = GenServerName::new("drainer");
fn app_tree() -> OneForOne {
OneForOne::new()
.child(
ChildSpec::new(Restart::Permanent, || {
// A plain worker that does not trap: stopped outright on shutdown.
loop {
sleep(Duration::from_millis(10));
}
})
.shutdown(Shutdown::Timeout(Duration::from_millis(100))),
)
// A gen_server is a direct child: `named(N).run()` runs the loop as
// the child actor itself, so the supervisor's shutdown arrives as
// `handle_shutdown` and a restart re-binds the name.
.child(
ChildSpec::new(Restart::Permanent, || {
GenServerBuilder::new(Drainer {
pending: 3,
stop: None,
timer: None,
})
.named(DRAINER)
.run()
.expect("drainer name is free");
})
.shutdown(Shutdown::Infinity),
)
}
fn main() {
println!("--- 1. root exit drives the shutdown ---");
smarm::run(|| {
spawn(|| app_tree().run());
sleep(Duration::from_millis(50)); // the app "runs" for a while
// Returning here asks the supervisor to shut down; the run ends when
// the tree — drainer included — is gone.
});
println!("--- 2. an outside thread drives the shutdown ---");
let rt = smarm::init(smarm::Config::default());
let handle = rt.handle(); // Send + Sync; grab it before run
rt.run(move || {
let sup = spawn(|| app_tree().run());
let sup_pid = sup.pid();
// Stand-in for a SIGTERM handler thread.
thread::spawn(move || {
thread::sleep(Duration::from_millis(50));
println!("signal thread: requesting shutdown");
handle.request_shutdown(sup_pid);
});
sup.join()
.expect("supervisor returns normally after ordered shutdown");
println!("supervisor down; root returns");
});
}