docs,examples: graceful shutdown — new examples/graceful_shutdown.rs, README 'Stopping actors', named_genserver uses shutdown(), Deep Dive terminate note, ROADMAP open items

This commit is contained in:
Claude (sandbox)
2026-08-19 16:28:42 +00:00
parent 6ceb138f5f
commit 849a424c8e
5 changed files with 179 additions and 1 deletions
+15
View File
@@ -54,6 +54,21 @@ run(|| {
});
```
## Stopping actors
Two strengths, as in OTP. `request_stop(pid)` is `exit(Pid, kill)`: a cooperative
hard stop, unwinding at the actor's next observation point. `request_shutdown(pid)`
is `exit(Pid, shutdown)`: an actor that traps exits (`trap_exit()`, or
`ctx.trap_exit()` in a gen_server / `cx.trap_exit()` in a gen_statem) receives it
as a signal — `handle_shutdown` / a `shutdown` row — and may drain before stopping
itself; one that does not trap is stopped outright. Supervisors trap:
`request_shutdown(sup)` tears the tree down top-down, each child per its
`ChildSpec` `Shutdown` policy (`Timeout(d)`, `Infinity`, `BrutalKill`). The run's
root actor returning means "the program is done": every top-level actor gets a
`request_shutdown`, and `run()` returns when they are gone. From outside the
runtime (a signal thread), `Runtime::handle().request_shutdown(pid)` does the
same. `examples/graceful_shutdown.rs` shows all of it.
## Layout
```
+15
View File
@@ -272,6 +272,21 @@ outright — `join` what you need finished. No forcing sweep follows.
---
### Open items from the graceful-shutdown work (not scheduled)
- **gen_server / gen_statem as a direct supervised child.** A `ChildSpec` start
fn is `Fn()`, and `GenServerBuilder::start` spawns a *new* actor and hands
back a ref, so a supervised server today needs a trapping wrapper actor that
starts it `under(self_pid())`, forwards the shutdown and waits
(`examples/graceful_shutdown.rs::drainer_child`). An inline
`GenServerBuilder::run()` (loop as the current actor; ref handed out via a
name or a start callback) would make the primary OTP use case direct. Needed
by urus's endpoint child.
- Supervisor `Live` drop-guard sweep is `request_stop` (kill propagates as
kill); OTP would deliver a trappable `killed`. Chosen for boundedness.
- A root-exit shutdown reaches only actors live *at that instant*; a
non-trapping forest root that spawns before it unwinds leaves that spawn
to itself (Erlang: an unlinked spawn is nobody's child).
## Invariants & gotchas (respect these across all cycles)
- **Shared mutex is non-reentrant.** `Sender::send` can call `unpark` →
+1 -1
View File
@@ -1700,7 +1700,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. Keep it cheap, non-blocking, non-panicking.</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>, inbox close) 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>
+142
View File
@@ -0,0 +1,142 @@
//! 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, ShutdownAction, StopHandle, TimerHandle,
};
use smarm::supervisor::{ChildSpec, OneForOne, Restart, Shutdown};
use smarm::{monitor, request_shutdown, self_pid, sleep, spawn, trap_exit, DownReason};
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");
}
}
/// A supervised child wrapping the server. (A gen_server is not yet directly
/// usable as a `ChildSpec` start fn; the wrapper traps, forwards the shutdown,
/// and waits for the server to finish. See ROADMAP "open items".)
fn drainer_child() {
let inbox = trap_exit();
let srv = GenServerBuilder::new(Drainer {
pending: 3,
stop: None,
timer: None,
})
.under(self_pid())
.start();
let mon = monitor(srv.pid());
// Wait for our shutdown, forward it, wait for the server.
while let Ok(sig) = inbox.recv() {
if sig.reason == DownReason::Shutdown {
request_shutdown(srv.pid());
let _ = mon.rx.recv();
return;
}
}
}
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))),
)
.child(ChildSpec::new(Restart::Permanent, drainer_child).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");
});
}
+6
View File
@@ -66,6 +66,12 @@ fn main() {
let svc: Option<GenServerRef<Counter>> = whereis_server(COUNTER);
if let Some(svc) = svc {
let _ = svc.call(Query::Get);
// A named server is pinned alive by the registry, so dropping refs
// does not end it. Stop it explicitly: `shutdown()` asks politely
// (a trapping server drains first; this one is stopped outright)
// and waits until it is gone. Left running, the root's return
// would shut it down the same way — see examples/graceful_shutdown.rs.
svc.shutdown();
}
});
}