# Cycle 17: Supervision Trees with handle_down and Supervisor Actor — Development History > Commit: `a70bd86` · 4 files · 754 insertions, 7 deletions --- ## Motivation With monitoring (Cycle 13), lifecycle hooks (Cycle 9), and factory-based restart (Cycle 7) in place, swactor had all the building blocks for supervision trees — the signature feature of Erlang/OTP. Supervision trees provide structured fault tolerance: a parent actor (supervisor) monitors children and restarts them according to configurable policies when they fail. ## Competitor Analysis | Framework | Supervisor Model | Strategies | Child Spec | Meltdown Protection | |-----------|-----------------|------------|------------|---------------------| | Erlang/OTP | Built-in `supervisor` behaviour | one_for_one, one_for_all, rest_for_one, simple_one_for_one | `{Id, MFA, Restart, Shutdown, Type}` | Intensity/period limits | | Akka | SupervisorStrategy | Resume, Restart, Stop, Escalate + BackoffSupervisor | N/A (inline) | MaxNrOfRetries/withinTimeRange | | Ractor | `ractor-supervisor` crate | External crate, event-based | SupervisionEvent callback | N/A | | Bastion | Built-in hierarchy | Redundancy groups | Structural (parent-child) | N/A | | CAF | No built-in supervisor | Monitor-based (manual) | N/A | N/A | | **Swactor** | **User-space `Supervisor` actor** | **OneForOne** (Cycle 17), **OneForAll/RestForOne** (Cycle 18) | **`ChildSpec`** | **max_restarts budget** | ### Key Findings - Swactor has all the building blocks: monitor (Cycle 13), `spawn_restartable` (Cycle 7), lifecycle hooks (Cycle 9), `Down` messages (Cycle 13) - **Decision**: Supervisor as a user-space actor built on existing primitives (like Ractor's `ractor-supervisor` crate), not a special runtime construct - **`handle_down` callback** enables any actor to react to monitored deaths without requiring `Incoming = Down` — this is the key API gap that needed filling ## Implementation ### 1. `handle_down` Callback on ActorInterface The core API addition enabling supervision: - `fn handle_down(&mut self, ctx: &Ctx, down: Down)` — default no-op, called when a monitored actor dies and the actor's `Incoming` type is NOT `Down` - Implemented via second downcast attempt in `handle_any`: if the message is `Down` and the actor's `Incoming` type doesn't match, call `handle_down` instead of `handle` - Fully backward-compatible: actors with `Incoming = Down` still receive via `handle()` as before - This decouples supervision logic from the actor's primary message type ### 2. `ctx.stop_actor(addr)` — Stop Another Actor - Sends graceful stop to another actor from handler context - Uses `StopSignal` through normal message routing (PoisonPill semantics) - Enables supervisor-controlled shutdown of children ### 3. `Supervisor` Actor A user-space actor managing child actors: - **`SupervisorStrategy::OneForOne`** — only the failed child is restarted (Cycle 17) - **`RestartPolicy`**: `Permanent` (always restart), `Transient` (restart only on panic, not normal stop), `Temporary` (never restart) - **`ChildSpec`** — `{ id: String, restart: RestartPolicy, factory: Fn(&Ctx) -> Result }` - Children spawned in `on_start`, monitored via `ctx.monitor()` - Death detected via `handle_down`, restart policy consulted, factory invoked for replacement - **Meltdown detection**: stops itself when `total_restarts > max_restarts` - **Cascading shutdown**: `on_stop` sends stop signals to all living children ### ActiveChild Struct - Tracks `addr: ActorAddress` and `monitor_ref: MonitorRef` per child - Reused by Router (Cycle 19) **Key files modified:** `src/actor.rs`, `tests/runtime_api.rs`, `docs/runtime.md` ## Design Decisions - **User-space actor (not runtime primitive)** — the Supervisor is just an actor that uses existing APIs (monitor, spawn, stop). No special runtime support needed. This validates the composability of the monitoring and lifecycle systems. - **`handle_down` as opt-in callback** — adding `handle_down` to `ActorInterface` with a default no-op means existing actors don't need to change. Actors that want to react to deaths override it. The alternative (requiring `Incoming = Down`) would force actors to handle `Down` as their primary message type. - **Factory takes `&Ctx`** — the factory closure receives the context so it can use `ctx.spawn`, `ctx.monitor`, etc. during child creation. This enables the supervisor to monitor new children immediately. - **Meltdown protection** — if children keep crashing faster than they can be restarted, the supervisor stops itself rather than looping forever. Matches Erlang's intensity/period limits. - **Cascading shutdown** — when the supervisor stops, all living children receive stop signals. This prevents orphaned actors. ## Tests Added 10 new tests (127 → 138 total, counting 130 behavioral + 7 proptest + 1 doctest): - `handle_down_receives_death_notification` — handle_down callback fires on monitored death - `handle_down_skipped_when_incoming_is_down` — backward compat: Incoming=Down uses handle() - `ctx_stop_actor_stops_target` — one actor stops another via ctx.stop_actor() - `supervisor_restarts_permanent_child_on_panic` — panic → restart (OneForOne + Permanent) - `supervisor_does_not_restart_transient_child_on_normal_stop` — Normal stop → no restart - `supervisor_restarts_transient_child_on_panic` — Panicked → restart (Transient) - `supervisor_never_restarts_temporary_child` — Temporary → never restart - `supervisor_meltdown_after_max_restarts` — exceeding max_restarts stops supervisor - `supervisor_one_for_one_only_restarts_failed_child` — multi-child, only crashed child restarted - `supervisor_on_stop_kills_children` — supervisor shutdown cascades to children ## Result - 138 tests pass (130 behavioral + 7 proptest + 1 doctest) - Zero warnings, full workspace compiles - Supervisor validates the composability of Cycles 7 (recovery), 9 (lifecycle), and 13 (monitoring)