Builds on the existing supervisor_channel funnel (one mailbox per
supervisor; better than N monitor channels given there is no select).
- supervisor.rs: Restart{Permanent,Transient,Temporary}, ChildSpec (an
Fn factory so children can be re-instantiated), and OneForOne with a
builder (child/intensity) and run() supervision loop. Restart decision
keys off whether the Signal was a panic; a sliding-window intensity cap
stops crash loops and returns instead of spinning.
- Does NOT forcibly terminate children: shared-heap + Drop make async
teardown of a peer unsound, so one_for_all/rest_for_one and true links
wait on a cooperative-cancellation primitive. Cap-trip just stops
restarting; live children are left as-is.
tests/supervisor.rs: transient restart-then-settle, transient/temporary
no-restart, permanent crash-loop hitting the cap, and one-for-one
isolation across two children. Full suite green.
131 lines
4.3 KiB
Rust
131 lines
4.3 KiB
Rust
//! One-for-one supervisor tests.
|
|
//!
|
|
//! A `OneForOne` runs as an ordinary actor: it spawns its children under
|
|
//! itself, collects their termination `Signal`s on a single mailbox, and
|
|
//! restarts them per policy until either every child is in a terminal,
|
|
//! non-restartable state (then `run()` returns) or the restart intensity cap
|
|
//! is tripped (then `run()` gives up and returns).
|
|
//!
|
|
//! Policies:
|
|
//! - `Permanent` — restart on any termination (normal or panic).
|
|
//! - `Transient` — restart only on panic; a normal exit is "done".
|
|
//! - `Temporary` — never restart.
|
|
//!
|
|
//! All cases below are deterministic under the single-thread runtime: the
|
|
//! supervisor parks in `recv()` while a child runs to completion, so signals
|
|
//! arrive one at a time in a fixed order.
|
|
|
|
use smarm::supervisor::{ChildSpec, OneForOne, Restart};
|
|
use smarm::{run, spawn};
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
fn counting_child(
|
|
counter: &Arc<AtomicUsize>,
|
|
behavior: impl Fn(usize) + Send + Sync + 'static,
|
|
) -> impl Fn() + Send + Sync + 'static {
|
|
let c = counter.clone();
|
|
move || {
|
|
let n = c.fetch_add(1, Ordering::SeqCst) + 1;
|
|
behavior(n);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn transient_child_is_restarted_on_panic_then_settles() {
|
|
let runs = Arc::new(AtomicUsize::new(0));
|
|
let r = runs.clone();
|
|
run(move || {
|
|
let child = counting_child(&r, |n| {
|
|
if n < 3 {
|
|
panic!("crash {n}");
|
|
}
|
|
// 3rd run returns normally -> Transient does not restart.
|
|
});
|
|
let sup = spawn(move || {
|
|
OneForOne::new()
|
|
.intensity(10, Duration::from_secs(60))
|
|
.child(ChildSpec::new(Restart::Transient, child))
|
|
.run();
|
|
});
|
|
sup.join().unwrap();
|
|
});
|
|
assert_eq!(runs.load(Ordering::SeqCst), 3, "two restarts then a clean exit");
|
|
}
|
|
|
|
#[test]
|
|
fn transient_child_normal_exit_is_not_restarted() {
|
|
let runs = Arc::new(AtomicUsize::new(0));
|
|
let r = runs.clone();
|
|
run(move || {
|
|
let child = counting_child(&r, |_| { /* return normally */ });
|
|
let sup = spawn(move || {
|
|
OneForOne::new()
|
|
.child(ChildSpec::new(Restart::Transient, child))
|
|
.run();
|
|
});
|
|
sup.join().unwrap();
|
|
});
|
|
assert_eq!(runs.load(Ordering::SeqCst), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn temporary_child_is_not_restarted_on_panic() {
|
|
let runs = Arc::new(AtomicUsize::new(0));
|
|
let r = runs.clone();
|
|
run(move || {
|
|
let child = counting_child(&r, |_| panic!("once"));
|
|
let sup = spawn(move || {
|
|
OneForOne::new()
|
|
.child(ChildSpec::new(Restart::Temporary, child))
|
|
.run();
|
|
});
|
|
sup.join().unwrap();
|
|
});
|
|
assert_eq!(runs.load(Ordering::SeqCst), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn intensity_cap_stops_a_permanent_crash_loop() {
|
|
let runs = Arc::new(AtomicUsize::new(0));
|
|
let r = runs.clone();
|
|
run(move || {
|
|
let child = counting_child(&r, |n| panic!("always {n}"));
|
|
let sup = spawn(move || {
|
|
OneForOne::new()
|
|
.intensity(3, Duration::from_secs(60))
|
|
.child(ChildSpec::new(Restart::Permanent, child))
|
|
.run();
|
|
});
|
|
sup.join().unwrap();
|
|
});
|
|
// 1 initial run + 3 restarts, then the 4th failure trips the cap.
|
|
assert_eq!(runs.load(Ordering::SeqCst), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn one_for_one_restarts_only_the_failed_child() {
|
|
let a = Arc::new(AtomicUsize::new(0));
|
|
let b = Arc::new(AtomicUsize::new(0));
|
|
let (ra, rb) = (a.clone(), b.clone());
|
|
run(move || {
|
|
let child_a = counting_child(&ra, |n| {
|
|
if n < 2 {
|
|
panic!("a crash {n}");
|
|
}
|
|
});
|
|
let child_b = counting_child(&rb, |_| { /* runs once, normal */ });
|
|
let sup = spawn(move || {
|
|
OneForOne::new()
|
|
.intensity(10, Duration::from_secs(60))
|
|
.child(ChildSpec::new(Restart::Transient, child_a))
|
|
.child(ChildSpec::new(Restart::Temporary, child_b))
|
|
.run();
|
|
});
|
|
sup.join().unwrap();
|
|
});
|
|
assert_eq!(a.load(Ordering::SeqCst), 2, "A restarts once then settles");
|
|
assert_eq!(b.load(Ordering::SeqCst), 1, "B is untouched by A's restart");
|
|
}
|