supervisor: rewrite docs for users; relocate internals to items
This commit is contained in:
+109
-33
@@ -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
|
//! A *supervisor* is an actor whose only job is to start a fixed set of child
|
||||||
//! `Receiver<Signal>`. When a child actor terminates, the scheduler sends
|
//! actors and react when one of them terminates — restarting it (and, depending
|
||||||
//! a `Signal` on the supervisor's channel. The supervisor decides what to
|
//! on the strategy, some of its siblings) according to a policy, or giving up
|
||||||
//! do — restart, escalate, ignore.
|
//! 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
|
//! The supervisor type is [`OneForOne`]. The name is historical — the restart
|
||||||
//! lives in user code; library is mechanism only.
|
//! *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 crate::pid::Pid;
|
||||||
use std::any::Any;
|
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 {
|
pub enum Signal {
|
||||||
/// The child exited normally.
|
/// The child exited normally.
|
||||||
Exit(Pid),
|
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 crate::channel::channel;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -115,11 +192,10 @@ pub enum Strategy {
|
|||||||
|
|
||||||
/// A supervisor over a fixed set of children.
|
/// A supervisor over a fixed set of children.
|
||||||
///
|
///
|
||||||
/// Despite the name (kept for backwards compatibility), the restart strategy
|
/// Build it with [`new`](Self::new), add children with [`child`](Self::child),
|
||||||
/// is selectable via [`OneForOne::strategy`]; the default is
|
/// pick a [`strategy`](Self::strategy) and an [`intensity`](Self::intensity)
|
||||||
/// [`Strategy::OneForOne`]. Build with `new()`, add children with `child()`,
|
/// cap, then drive the loop with [`run`](Self::run) on an actor of its own. See
|
||||||
/// tune the cap with `intensity()`, then drive it with `run()` from inside an
|
/// the [module docs](self) for the full picture.
|
||||||
/// actor (typically `spawn(|| OneForOne::new()....run())`).
|
|
||||||
pub struct OneForOne {
|
pub struct OneForOne {
|
||||||
children: Vec<ChildSpec>,
|
children: Vec<ChildSpec>,
|
||||||
strategy: Strategy,
|
strategy: Strategy,
|
||||||
|
|||||||
Reference in New Issue
Block a user