From e54c67c43132622a1941f62a51157edd6649b301 Mon Sep 17 00:00:00 2001 From: smarm-agent Date: Sat, 20 Jun 2026 19:51:32 +0000 Subject: [PATCH] supervisor: rewrite docs for users; relocate internals to items --- src/supervisor.rs | 142 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 109 insertions(+), 33 deletions(-) diff --git a/src/supervisor.rs b/src/supervisor.rs index 0f4d142..cd92709 100644 --- a/src/supervisor.rs +++ b/src/supervisor.rs @@ -1,16 +1,114 @@ -//! Supervision signals. +//! Supervision: keep a set of actors alive. //! -//! Every actor has a supervisor, which is itself just an actor with a -//! `Receiver`. When a child actor terminates, the scheduler sends -//! a `Signal` on the supervisor's channel. The supervisor decides what to -//! do — restart, escalate, ignore. +//! A *supervisor* is an actor whose only job is to start a fixed set of child +//! actors and react when one of them terminates — restarting it (and, depending +//! on the strategy, some of its siblings) according to a policy, or giving up +//! when failures arrive too fast. It is how you turn "an actor that might crash" +//! into "a service that stays up": a crash becomes a restart instead of a hole +//! in the process tree. //! -//! For v0.1 there is no built-in restart-intensity cap. That's policy and -//! lives in user code; library is mechanism only. +//! The supervisor type is [`OneForOne`]. The name is historical — the restart +//! *strategy* is selectable via [`OneForOne::strategy`], and +//! [`Strategy::OneForOne`] is merely the default. You declare the children up +//! front as [`ChildSpec`]s, each carrying a [`Restart`] policy, then hand the +//! supervision loop an actor of its own with [`OneForOne::run`]. +//! +//! ## A child that crashes and recovers +//! +//! ``` +//! use smarm::{run, spawn, ChildSpec, OneForOne, Restart}; +//! use std::sync::Arc; +//! use std::sync::atomic::{AtomicUsize, Ordering}; +//! use std::time::Duration; +//! +//! run(|| { +//! // A flaky child: it panics on its first two starts, then settles. +//! let starts = Arc::new(AtomicUsize::new(0)); +//! let s = starts.clone(); +//! let child = move || { +//! let n = s.fetch_add(1, Ordering::SeqCst) + 1; +//! if n < 3 { +//! panic!("boom {n}"); +//! } +//! // The third start returns normally. +//! }; +//! +//! // The supervisor runs on its own actor. `Transient` restarts a child +//! // that panics but treats a clean return as "done", so once the child +//! // finally succeeds the supervisor has nothing left to do and `run()` +//! // returns. smarm catches the child's panic and turns it into a restart; +//! // it never reaches the process as a real crash. +//! let sup = spawn(move || { +//! OneForOne::new() +//! .intensity(5, Duration::from_secs(60)) +//! .child(ChildSpec::new(Restart::Transient, child)) +//! .run(); +//! }); +//! sup.join().unwrap(); +//! +//! assert_eq!(starts.load(Ordering::SeqCst), 3); // one start, two restarts +//! }); +//! ``` +//! +//! ## Restart policies +//! +//! Each child carries a [`Restart`] policy that decides whether *that child* +//! comes back when it terminates: +//! +//! - [`Restart::Permanent`] restarts on any termination, normal or panic — +//! for a service that should never be down. +//! - [`Restart::Transient`] restarts only on an abnormal exit (a panic or a +//! cooperative stop); a clean return means "done" — for work that runs to +//! completion but should be retried if it crashes. +//! - [`Restart::Temporary`] never restarts; the death is simply noted. +//! +//! ## Strategies: which siblings get cycled +//! +//! When a restart is due, the [`Strategy`] decides which *other* children are +//! cycled along with the one that died. The triggering child's own policy still +//! decides whether anything restarts at all. +//! +//! - [`Strategy::OneForOne`] restarts only the child that died — the default. +//! - [`Strategy::OneForAll`] restarts every child: the survivors are stopped, +//! then the whole set is restarted. +//! - [`Strategy::RestForOne`] restarts the dead child and every child started +//! after it, leaving earlier children untouched. +//! +//! ## Stopping a sibling is cooperative +//! +//! Cycling a sibling means stopping it first, and a supervisor never tears a +//! running actor down from outside: smarm actors share a heap and rely on +//! Drop/RAII, so unwinding a peer's stack from elsewhere would be unsound. +//! Instead the supervisor *requests* the stop and the child unwinds at its next +//! observation point — a `check!()`, an allocation, or a blocking call. A child +//! wedged in a tight loop with no observation point cannot be stopped, for the +//! same reason it cannot be preempted. +//! +//! ## Giving up: the restart-intensity cap +//! +//! A child that crashes the instant it starts would otherwise restart forever. +//! [`OneForOne::intensity`] bounds that: at most `max` restarts within any +//! `period`-long sliding window. One terminating child counts as a single +//! restart event even when the strategy cycles several siblings. When the cap +//! trips, the supervisor stops restarting, cooperatively stops any survivors in +//! reverse start order, and `run()` returns. +//! +//! ## Running context +//! +//! [`OneForOne::run`] takes over the calling actor as the supervision loop, so +//! a supervisor gets an actor of its own — typically +//! `spawn(|| OneForOne::new()/* … */.run())`, all from inside +//! [`run`](crate::run). Each child is spawned beneath the supervisor's pid, so +//! every child termination funnels back to it as a [`Signal`]. use crate::pid::Pid; use std::any::Any; +/// A child-termination notice delivered to its supervisor. +/// +/// Every child a supervisor starts is spawned beneath the supervisor's pid, so +/// each child's termination funnels back to it as one of these. The variant +/// records *how* the child went — which is what its [`Restart`] policy keys off. pub enum Signal { /// The child exited normally. Exit(Pid), @@ -42,27 +140,6 @@ impl Signal { } } -// --------------------------------------------------------------------------- -// One-for-one supervisor -// -// A supervisor is itself an actor. It spawns each child under its own pid so -// that every child death funnels into one mailbox (the `supervisor_channel`), -// then loops: receive a Signal, decide per the child's `Restart` policy -// whether to restart, and enforce a restart-intensity cap so a child that -// crashes in a tight loop eventually gives up instead of spinning forever. -// -// What this does NOT do: *forcibly* terminate a running child. smarm actors -// share a heap and rely on Drop/RAII, so tearing down a peer's stack from -// outside is unsound. Stopping a sibling — whether for `one_for_all` / -// `rest_for_one`, for a propagated link death, or for the ordered shutdown -// below — is therefore *cooperative*: `request_stop` flags the child and it -// unwinds at its next observation point. A child with no observation points -// (a tight loop with no `check!()`, allocation, or blocking op) cannot be -// stopped, exactly as it cannot be preempted. When the intensity cap trips, -// this supervisor stops restarting and tears the remaining children down in -// reverse start order before returning. -// --------------------------------------------------------------------------- - use crate::channel::channel; use std::collections::{HashMap, VecDeque}; use std::sync::Arc; @@ -115,11 +192,10 @@ pub enum Strategy { /// A supervisor over a fixed set of children. /// -/// Despite the name (kept for backwards compatibility), the restart strategy -/// is selectable via [`OneForOne::strategy`]; the default is -/// [`Strategy::OneForOne`]. Build with `new()`, add children with `child()`, -/// tune the cap with `intensity()`, then drive it with `run()` from inside an -/// actor (typically `spawn(|| OneForOne::new()....run())`). +/// Build it with [`new`](Self::new), add children with [`child`](Self::child), +/// pick a [`strategy`](Self::strategy) and an [`intensity`](Self::intensity) +/// cap, then drive the loop with [`run`](Self::run) on an actor of its own. See +/// the [module docs](self) for the full picture. pub struct OneForOne { children: Vec, strategy: Strategy,