Supervision — Restart Policies and Scope
Raw run_fork gives you a FiberHandle and full control. Long-lived servers usually need policies: when a child effect fails, should you retry, back off, give up, or substitute a default? Effect.ts encodes these ideas in supervisors. id_effect provides the same vocabulary wired to Scope, CancellationToken, and Schedule.
Why not only retry?
retry re-runs an Effect<A, E, R> until success or the schedule stops. Supervision adds:
- A stable shutdown channel ([
CancellationToken]) installed when a child [Scope] closes, so cooperative loops exit withCause::Interruptinstead of spinning forever. - Declarative policies (
Terminate,Restart,RestartWithLimit,Escalate,Ignore) that compose with virtual time (TestClock) for deterministic tests.
Supervisor and Scope
Supervisor::attach(parent_scope) forks a child [Scope]. When the parent closes, the child closes; a finalizer on the child cancels the supervisor token so any supervised loop observes cancellation on the next iteration header.
Use Supervisor::detached() for examples and unit tests that do not need a parent tree.
Policies in one table
| Policy | On child Ok(a) | On child Err(e) |
|---|---|---|
Terminate / Escalate | Return a | Return [Cause::Fail(e)] (no retry) |
Restart { schedule } | Return a | Sleep per schedule, run factory again |
RestartWithLimit { limit, schedule } | Return a | Retry while under limit; then fail with Cause::Then aggregating prior failures |
Ignore { recover } | Return a | Return recover |
RestartWithLimit counts retries after failure: limit == 0 means “no retries” (fail on the first Err without sleeping). A positive limit allows that many retry attempts after the initial failing run.
Typed failures vs interrupts vs defects
- A supervised child that returns [
Err] becomes [Cause::Fail]. - Token cancellation (scope teardown or explicit
cancel) surfaces as [Cause::Interrupt]. - Panics inside the interpreter are still defects at the runtime boundary; supervision does not “recover” unwinding
stdpanics—document and test the happy paths your runtime actually exposes.
supervised vs FiberHandle::scoped
FiberHandle::scoped ties one handle to one scope via a finalizer that interrupts the fiber. supervised runs the factory inline on your environment R, so it suits retry loops without an extra FiberHandle until you opt into Supervisor::spawn.
Example sketch
#![allow(unused)] fn main() { use id_effect::{ Supervisor, SupervisorPolicy, supervised, Schedule, TestClock, succeed, }; use std::time::Instant; let parent = id_effect::Scope::make(); let sup = Supervisor::attach(&parent); let clock = TestClock::new(Instant::now()); let body = supervised( &sup, SupervisorPolicy::Restart { schedule: Schedule::spaced(std::time::Duration::ZERO), }, clock, || succeed::<u32, &str, ()>(42), ); // run `body` with your environment; close `parent` to cancel via token. }
For production delays, prefer a non-zero [Schedule] and a real Clock (for example the Tokio bridge clock in server code).
See also
- Phase F parity doc — epic breakdown and acceptance notes.
supervisor.rs— full API and unit tests.