Skip to main content

embassy_supervisor/
lib.rs

1// `no_std` for the shipped crate and the embedded build; under `cargo test` the
2// crate is built for the host, where the test harness and the unit tests need `std`.
3#![cfg_attr(not(test), no_std)]
4#![forbid(unsafe_code)]
5#![deny(missing_docs)]
6//! # embassy-supervisor — a task-lifecycle supervisor for [embassy](https://embassy.dev)
7//!
8//! Application- and HAL-agnostic primitives for orchestrating a set of embassy
9//! tasks: bringing them up in dependency order, tearing them down in reverse,
10//! scaling an elastic worker pool with load, and starting/stopping/pausing/
11//! resuming individual tasks at runtime while keeping the dependency graph
12//! consistent.
13//!
14//! ## The model
15//!
16//!   * The graph is declared once with the [`supervisor_graph!`] macro: each
17//!     managed task becomes a [`TaskNode`] `static`, and the macro bundles the node
18//!     slots, dependency table, and a topological order computed **at compile time**
19//!     (a dependency cycle is a compile error) into a single [`Graph`] (`GRAPH`).
20//!   * [`Supervisor::new`] takes `&GRAPH` (no work, no failure) and uses the order
21//!     to bring tasks up in dependency order ([`Supervisor::start`]) and tear them
22//!     down in reverse ([`Supervisor::teardown`]).
23//!   * Each node carries a `TaskHandle` of per-node atomic flags and
24//!     single-consumer `Signal`s. Every node is single-instance — no counts, no
25//!     fan-out. See [`TaskHandle`].
26//!
27//! ## Three lifecycles, distinguished by [`Mode`]
28//!
29//!   * [`Mode::Terminate`] — the task exits its loop on shutdown and is respawned
30//!     on the next bring-up. Stateless services (a network listener, a logger).
31//!   * [`Mode::Pause`] — the task acks the shutdown then parks on
32//!     `wait_resume()`; it is resumed in place, never respawned. Tasks that
33//!     retain a resource across the pause (an open peripheral handle, a socket).
34//!   * [`Mode::OnDemand`] — like `Terminate`, but not started at boot and not
35//!     auto-respawned; the supervisor brings it up and down at runtime to scale
36//!     an elastic worker pool ([`ElasticPool`]) with load.
37//!
38//! ## What the supervisor does *not* do
39//!
40//!   * It does not model any power-state transition (sleep/wake): it reacts to
41//!     "teardown" and "bring-up" requests; the application drives them.
42//!   * It does not allocate, and does no work at construction: the topological
43//!     sort runs at compile time (see the `supervisor_graph!` macro).
44//!   * It does not observe task internals. Tasks self-report their drop state via
45//!     `ack_dropped()`; a task that fails to ack within a timeout panics the
46//!     supervisor with the offending node's name.
47//!
48//! ## Cargo features
49//!
50//!   * `control` *(default)* — the runtime control plane: [`ControlOp`],
51//!     [`request_control`], [`Supervisor::apply_control`].
52//!   * `pool` *(default)* — elastic worker pools: [`ElasticPool`],
53//!     [`Supervisor::run_pools`], and the `pools` field of [`Graph`].
54//!   * `defmt` — route the supervisor's logs through `defmt`; without it the log
55//!     macros are no-ops.
56//!
57//! Build with `default-features = false` for a minimal core that only does
58//! dependency-ordered bring-up/teardown (drops the control plane and pools,
59//! trimming flash and a couple of statics).
60//!
61//! ## Example
62//!
63//! [`supervisor_graph!`] declares the whole graph once — it generates the node
64//! `static`s and a single [`Graph`] value `GRAPH` bundling the node slots, dep
65//! table, and compile-time topological order (a dependency cycle is a compile
66//! error), which [`Supervisor::new`] consumes.
67//!
68//! ```ignore
69//! use embassy_executor::Spawner;
70//! use embassy_supervisor::{supervisor_graph, Supervisor, wait_control};
71//!
72//! // `app` depends on `net`; each `spawn:` names a task fn spawned with the node.
73//! supervisor_graph! {
74//!     node NET = Terminate, deps: [], spawn: net_task;
75//!     node APP = Terminate, deps: [NET], spawn: app_task;
76//! }
77//!
78//! #[embassy_executor::task]
79//! async fn supervisor_task(spawner: Spawner) {
80//!     let sup = Supervisor::new(&GRAPH);
81//!     sup.start(spawner).expect("initial spawn"); // brings up `net`, then `app`
82//!     loop {
83//!         // Apply runtime start/stop/pause/resume requests in dependency order.
84//!         let cmd = wait_control().await;
85//!         sup.apply_control(cmd, spawner).await;
86//!     }
87//!     // With the `pool` feature you'd instead drive scaling and control together:
88//!     // `select(sup.run_pools(spawner), wait_control())` (see the `firmware` crate).
89//! }
90//! ```
91//!
92//! The `firmware` crate in the [repository](https://github.com/cedrivard/embassy-supervisor)
93//! is a complete working example (USB-net, an HTTP control plane, an elastic pool,
94//! and OTA).
95//!
96//! Docs:
97//!   - `embassy_sync::signal::Signal`: <https://docs.embassy.dev/embassy-sync>
98//!   - Kahn's algorithm (topological sort):
99//!     <https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm>
100
101#[macro_use]
102mod fmt;
103
104use core::sync::atomic::Ordering;
105
106use embassy_executor::{SpawnError, Spawner};
107use embassy_futures::select::{Either, select};
108use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
109#[cfg(feature = "control")]
110use embassy_sync::channel::Channel;
111use embassy_sync::signal::Signal;
112use embassy_time::Timer;
113use portable_atomic::AtomicBool;
114
115// ─── Scale-request signal (task → supervisor) ──────────────────────────────
116//
117// Elastic pool workers fire this when their busy/idle status changes; the
118// supervisor's `run_pools` loop awaits it and re-runs the pool policies
119// (`ElasticPool`). Single-consumer `Signal`: many tasks may `signal()`, only the
120// supervisor `wait()`s. This is the *only* path by which task status reaches the
121// supervisor — it never polls.
122#[cfg(feature = "pool")]
123static SCALE_REQ: Signal<CriticalSectionRawMutex, ()> = Signal::new();
124
125/// Fire the scale-request signal. Called by a task on a busy/idle transition.
126/// A no-op when the `pool` feature is disabled (no pools to re-evaluate).
127pub fn request_scale() {
128    #[cfg(feature = "pool")]
129    SCALE_REQ.signal(());
130}
131
132/// Await the next scale request. The supervisor's driver loop selects this
133/// against its other wake sources and runs the scaling policy on each wake.
134#[cfg(feature = "pool")]
135pub async fn wait_scale() {
136    SCALE_REQ.wait().await;
137}
138
139// ─── Runtime control commands (app → supervisor) ───────────────────────────
140//
141// An application's control surface (e.g. a network endpoint) usually can't drive
142// the supervisor directly: the `Supervisor` and the `Spawner` live on the
143// supervisor task's stack, not in a `static`. So control is decoupled via this
144// channel — the caller `request_control()`s a (node, op) pair and returns
145// immediately; the supervisor's driver loop `wait_control()`s it and runs the
146// dependency-honoring `apply_control`. A `Channel` (not a `Signal`) so
147// back-to-back requests aren't coalesced; capacity 4 is ample for hand-driven
148// control and a full channel simply drops the surplus (`try_send`).
149
150/// Which way to drive a node. Higher-level verbs fold onto these two:
151/// `start`/`resume` → `Activate`, `stop`/`pause` → `Deactivate`. The concrete
152/// mechanism (respawn vs resume vs leave-to-pool) is then chosen per node `Mode`
153/// by the supervisor when it applies the command ([`Supervisor::apply_control`]).
154#[cfg(feature = "control")]
155#[derive(Clone, Copy, PartialEq, Eq, Debug)]
156pub enum ControlOp {
157    /// Bring the node up (start a stopped `Terminate` node, resume a `Pause` node).
158    Activate,
159    /// Take the node down (and its dependents, per the graph).
160    Deactivate,
161}
162
163/// A runtime control request: drive `node` (and, per the dependency graph and
164/// pool membership, the nodes it implies) in the `op` direction.
165#[cfg(feature = "control")]
166#[derive(Clone, Copy, Debug)]
167pub struct ControlCommand {
168    /// The node to drive.
169    pub node: &'static TaskNode,
170    /// The direction to drive it.
171    pub op: ControlOp,
172}
173
174/// App → supervisor control mailbox. `&'static TaskNode` is `Copy + Sync`, so
175/// the target rides the channel directly — no name lookup needed supervisor-side.
176#[cfg(feature = "control")]
177static CONTROL_REQ: Channel<CriticalSectionRawMutex, ControlCommand, 4> = Channel::new();
178
179/// Enqueue a control request. Non-blocking; drops if the mailbox is full (4
180/// outstanding), which is harmless for low-frequency manual control. Called by
181/// the application's control surface.
182#[cfg(feature = "control")]
183pub fn request_control(node: &'static TaskNode, op: ControlOp) {
184    let _ = CONTROL_REQ.try_send(ControlCommand { node, op });
185}
186
187/// Await the next control request. Selected by the supervisor's driver loop
188/// against pool scaling and any other application wake sources.
189#[cfg(feature = "control")]
190pub async fn wait_control() -> ControlCommand {
191    CONTROL_REQ.receive().await
192}
193
194/// Per-node timeout for `wait_dropped`. A task that doesn't ack within this
195/// window is a bug (e.g. a missing `ack_dropped()` call) and panics the
196/// supervisor with the offending node's name. 2 s comfortably exceeds a typical
197/// task's poll period and peripheral settle time.
198const SHUTDOWN_ACK_TIMEOUT_MS: u64 = 2_000;
199
200// ─── Mode ────────────────────────────────────────────────────────────────
201
202/// Lifecycle policy for a managed task: what the task does on shutdown and what
203/// the supervisor does to bring it back.
204#[derive(Clone, Copy, PartialEq, Eq, Debug)]
205pub enum Mode {
206    /// Task exits its loop on shutdown. The supervisor respawns it via the
207    /// node's `spawn` fn from `respawn_terminate`.
208    Terminate,
209    /// Task acks shutdown and parks on `wait_resume()`. The supervisor resumes
210    /// it from `resume_pausable`; the task is never respawned, so it keeps any
211    /// resource it holds (a peripheral handle, a socket) across the pause.
212    Pause,
213    /// Like `Terminate` (exits on shutdown), but **not** started at boot and
214    /// **not** auto-respawned. The supervisor brings it up and down at runtime
215    /// via `start_node` / `stop_node` in response to load — see [`ElasticPool`].
216    /// `start()` skips it; `respawn_terminate()` leaves it down (it
217    /// re-grows under demand); `teardown()` only acts on it while it is running.
218    OnDemand,
219}
220
221impl Mode {
222    /// Stable lower-case wire name, used both for serialization (e.g. a JSON
223    /// task-state view) and for `defmt` logging — the single source of these
224    /// strings.
225    pub fn as_str(&self) -> &'static str {
226        match self {
227            Mode::Terminate => "terminate",
228            Mode::Pause => "pause",
229            Mode::OnDemand => "ondemand",
230        }
231    }
232}
233
234#[cfg(feature = "defmt")]
235impl defmt::Format for Mode {
236    fn format(&self, f: defmt::Formatter) {
237        defmt::write!(f, "{}", self.as_str());
238    }
239}
240
241// ─── TaskHandle ──────────────────────────────────────────────────────────
242
243/// Coordination state for one task. Embedded inside [`TaskNode`].
244///
245/// Every node is single-instance, so each field is a per-node atomic flag or a
246/// single-consumer signal — no counts, no fan-out. Written by one side (task or
247/// supervisor) and read by the other:
248///   * `shutdown` / `shutdown_wake` — supervisor requests exit; the task parks
249///     on the signal and reads the flag.
250///   * `dropped` / `dropped_wake` — the task acks its exit; the supervisor
251///     parks on the signal (with a timeout) and reads the flag.
252///   * `resume_wake` — supervisor resumes a parked Pause-mode task.
253///   * `running` — supervisor's record that the node is spawned; `busy` — the
254///     task's active/idle status. Both read by the elastic scaling policy.
255///   * `disabled` — the node has been manually deactivated; see below.
256pub struct TaskHandle {
257    /// Set true by the supervisor when shutdown is requested.
258    /// Cleared by `reset()` before the next spawn.
259    shutdown: AtomicBool,
260    /// Wake source for `wait_shutdown()`. Fired by `signal_shutdown()`.
261    shutdown_wake: Signal<CriticalSectionRawMutex, ()>,
262    /// Set true by the instance when it acks the shutdown (a bool, not a count,
263    /// since every node is single-instance). Cleared by `reset()`.
264    dropped: AtomicBool,
265    /// Wake source for `wait_dropped()`. Fired by `ack_dropped()`.
266    dropped_wake: Signal<CriticalSectionRawMutex, ()>,
267    /// True while the supervisor has the node spawned and it hasn't exited.
268    /// Always-on nodes are set true by `start()`; `OnDemand` nodes are set
269    /// true/false by `start_node()` / `stop_node()`. `teardown()` only acts on
270    /// `running` nodes, so a down `OnDemand` node doesn't stall it.
271    running: AtomicBool,
272    /// True while the task is actively serving (its active/idle status). Set by
273    /// `mark_busy()` / `mark_idle()`; read by the scaling policy.
274    busy: AtomicBool,
275    /// Wake source for `wait_resume()` on Pause-mode tasks. Fired by
276    /// `signal_resume()`.
277    resume_wake: Signal<CriticalSectionRawMutex, ()>,
278    /// True while the node has been manually deactivated (stopped/paused) via the
279    /// runtime control interface (`Supervisor::deactivate`). Unlike the other
280    /// flags this one is **lifecycle-spanning**: it is *not* cleared by
281    /// `reset()`, so a manual stop "sticks" — the automatic bring-up paths
282    /// (`start`, `respawn_terminate`, `resume_pausable`, and the elastic pool's
283    /// grow) skip a node while it is set. Cleared only by `Supervisor::activate`.
284    /// Because it lives in a `static`, it also survives a power-state transition
285    /// that retains RAM (e.g. a warm-resume from deep sleep).
286    disabled: AtomicBool,
287    /// The node manages its own lifecycle and must NOT be torn down by a
288    /// dependency cascade. A dependency normally means "stop me when my dep stops",
289    /// but a node can declare a dep purely for *start* ordering and then intend to
290    /// outlive it — e.g. a node that needs a dependency up to initialize, then stops
291    /// that dependency to reclaim its resources, must survive that teardown. While
292    /// set, `deactivate` will not pull the node into a transitive-dependent teardown.
293    /// Self-managed, so `reset()` leaves it (the node clears it itself when done).
294    detached: AtomicBool,
295}
296
297impl TaskHandle {
298    const fn new(disabled_at_boot: bool) -> Self {
299        Self {
300            shutdown: AtomicBool::new(false),
301            shutdown_wake: Signal::new(),
302            dropped: AtomicBool::new(false),
303            dropped_wake: Signal::new(),
304            running: AtomicBool::new(false),
305            busy: AtomicBool::new(false),
306            resume_wake: Signal::new(),
307            disabled: AtomicBool::new(disabled_at_boot),
308            detached: AtomicBool::new(false),
309        }
310    }
311}
312
313// ─── TaskNode ────────────────────────────────────────────────────────────
314
315/// A node in the supervisor's task graph.
316///
317/// Designed to live in `static` memory: every field is `Sync`, all constructors
318/// are `const`. Declared by [`supervisor_graph!`], which emits one per managed
319/// task along with the [`Graph`] (`GRAPH`) that [`Supervisor::new`] consumes.
320pub struct TaskNode {
321    /// Human-readable name. Used in defmt logs and panic messages.
322    pub name: &'static str,
323    /// Lifecycle policy. See [`Mode`].
324    pub mode: Mode,
325    /// App-provided spawn function (typically an inline closure at the node's
326    /// declaration). Called once at boot from `Supervisor::start`, again from
327    /// `respawn_terminate` for Terminate nodes, and at runtime from `start_node`
328    /// for `OnDemand` nodes. `None` for a **parked** node the application spawns
329    /// itself (e.g. a `Pause` sensor holding a peripheral handle): the supervisor
330    /// tracks its lifecycle but never spawns it.
331    pub spawn: Option<fn(Spawner) -> Result<(), SpawnError>>,
332    handle: TaskHandle,
333}
334
335impl TaskNode {
336    /// A single-instance node started at boot (`Terminate`/`Pause`) or on demand
337    /// (`Mode::OnDemand`). Every node is single-instance; an elastic service is
338    /// modelled as several `OnDemand` nodes of the same pooled task fn.
339    ///
340    /// A `TaskNode` carries only its own identity and behaviour; the graph's
341    /// dependency edges live in the compile-time index table that
342    /// [`supervisor_graph!`] emits and [`Supervisor::new`] consumes.
343    /// `disabled_at_boot` seeds the node's disabled flag so a control-started node
344    /// (e.g. an OTA task) can be declared down and started later via a control op.
345    /// `spawn` is `None` for a parked node the application spawns itself.
346    pub const fn new(
347        name: &'static str,
348        mode: Mode,
349        spawn: Option<fn(Spawner) -> Result<(), SpawnError>>,
350        disabled_at_boot: bool,
351    ) -> Self {
352        Self {
353            name,
354            mode,
355            spawn,
356            handle: TaskHandle::new(disabled_at_boot),
357        }
358    }
359
360    // ── Task-side API ────────────────────────────────────────────────────
361    //
362    // Called from inside the `#[embassy_executor::task] async fn` body.
363
364    /// True iff the supervisor has requested shutdown. Checked at the loop top
365    /// alongside `wait_shutdown()` in a `select`.
366    pub fn shutdown_requested(&self) -> bool {
367        self.handle.shutdown.load(Ordering::Acquire)
368    }
369
370    /// Park until shutdown is requested. Returns immediately if shutdown has
371    /// already been requested. Use this for single-instance tasks in a `select`
372    /// against the task's main work future.
373    pub async fn wait_shutdown(&self) {
374        // Fast path — already requested. (Important because the signal is
375        // edge-triggered: if `signal()` fired before we got here, the bare
376        // `wait()` below would block forever.)
377        if self.handle.shutdown.load(Ordering::Acquire) {
378            return;
379        }
380        self.handle.shutdown_wake.wait().await;
381    }
382
383    /// Mark this instance as having shut down: clears the running flag and acks
384    /// the teardown handshake (so the supervisor's `wait_dropped` completes).
385    /// Every instance must call this exactly once on exit (Terminate/OnDemand
386    /// mode) or on each pause (Pause mode). It also covers an **autonomous** exit
387    /// the supervisor didn't request — e.g. a pool worker backing off — so the
388    /// pool sees the instance as down and can re-grow it under later demand.
389    pub fn ack_dropped(&self) {
390        self.handle.running.store(false, Ordering::Release);
391        self.handle.dropped.store(true, Ordering::Release);
392        self.handle.dropped_wake.signal(());
393    }
394
395    /// Pause-mode only: park until the supervisor signals resume.
396    pub async fn wait_resume(&self) {
397        self.handle.resume_wake.wait().await;
398    }
399
400    /// Report that this task started serving a request (active). Fires the
401    /// scale-request signal on a real idle→busy transition so the scaling policy
402    /// can react (e.g. grow the pool); a redundant call doesn't re-signal.
403    pub fn mark_busy(&self) {
404        if !self.handle.busy.swap(true, Ordering::Release) {
405            request_scale();
406        }
407    }
408
409    /// Report that this task finished serving and is idle again. Fires the
410    /// scale-request signal on a real busy→idle transition so the scaling policy
411    /// can react (e.g. shrink the pool); a redundant call doesn't re-signal.
412    pub fn mark_idle(&self) {
413        if self.handle.busy.swap(false, Ordering::Release) {
414            request_scale();
415        }
416    }
417
418    /// True while this task is actively serving. Read by the scaling policy.
419    pub fn is_busy(&self) -> bool {
420        self.handle.busy.load(Ordering::Acquire)
421    }
422
423    /// True while the supervisor has this node spawned (and it hasn't exited).
424    /// Read by the scaling policy to count live instances, and by a task-state
425    /// view.
426    pub fn is_running(&self) -> bool {
427        self.handle.running.load(Ordering::Acquire)
428    }
429
430    /// True while the node has been manually deactivated via the control
431    /// interface and not yet re-activated. Read by a task-state view and by the
432    /// automatic bring-up paths (which skip a disabled node).
433    pub fn is_disabled(&self) -> bool {
434        self.handle.disabled.load(Ordering::Acquire)
435    }
436
437    /// Mark/clear this node as **detached**: a node that manages its own lifecycle
438    /// and must not be torn down by a dependency cascade (see the field doc). Set it
439    /// before stopping a dependency the node has declared but intends to outlive.
440    pub fn set_detached(&self, detached: bool) {
441        self.handle.detached.store(detached, Ordering::Release);
442    }
443
444    /// True while this node is detached from dependency-cascade teardown.
445    pub fn is_detached(&self) -> bool {
446        self.handle.detached.load(Ordering::Acquire)
447    }
448
449    // ── Supervisor-side API ──────────────────────────────────────────────
450    //
451    // Driven by the `Supervisor` struct. Kept `pub(crate)` so app code doesn't
452    // accidentally bypass the supervisor's orchestration.
453
454    pub(crate) fn signal_shutdown(&self) {
455        self.handle.shutdown.store(true, Ordering::Release);
456        self.handle.shutdown_wake.signal(());
457    }
458
459    pub(crate) fn signal_resume(&self) {
460        self.handle.resume_wake.signal(());
461    }
462
463    pub(crate) fn set_running(&self, running: bool) {
464        self.handle.running.store(running, Ordering::Release);
465    }
466
467    /// Set/clear the manual-deactivation flag. Set by `Supervisor::deactivate`,
468    /// cleared by `Supervisor::activate`. Deliberately *not* touched by
469    /// `reset()`, so a manual stop survives respawn cycles and RAM-retaining
470    /// power-state transitions.
471    ///
472    /// Public so an application can pre-disable a `Terminate` node *before*
473    /// `Supervisor::start`, making it a stopped-at-boot task that only comes up on
474    /// an explicit `Activate` control (a node started by control rather than at boot).
475    pub fn set_disabled(&self, disabled: bool) {
476        self.handle.disabled.store(disabled, Ordering::Release);
477    }
478
479    /// Wait until the instance has called `ack_dropped()`. Single-instance, so
480    /// one ack ends the wait. The fast-path flag check handles the ack landing
481    /// before this await (the `dropped_wake` signal is edge-triggered).
482    pub(crate) async fn wait_dropped(&self) {
483        if self.handle.dropped.load(Ordering::Acquire) {
484            return;
485        }
486        self.handle.dropped_wake.wait().await;
487    }
488
489    /// Clear the shutdown flag, dropped flag, busy flag, and the shutdown /
490    /// dropped wake-signals so the next cycle starts clean. Doesn't touch
491    /// `running` (managed around spawn/stop), `resume_wake` (`resume_pausable`
492    /// fires that for Pause nodes), or `disabled` (lifecycle-spanning).
493    pub(crate) fn reset(&self) {
494        self.handle.shutdown.store(false, Ordering::Release);
495        self.handle.dropped.store(false, Ordering::Release);
496        self.handle.busy.store(false, Ordering::Release);
497        self.handle.shutdown_wake.reset();
498        self.handle.dropped_wake.reset();
499    }
500}
501
502/// Manual impl: the private `TaskHandle` (Signals + atomics) has no `Debug`, and a
503/// snapshot of the *live* flags is more useful than raw handle internals anyway.
504/// `finish_non_exhaustive` marks the elided fields (`spawn`, the handle).
505impl core::fmt::Debug for TaskNode {
506    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
507        f.debug_struct("TaskNode")
508            .field("name", &self.name)
509            .field("mode", &self.mode)
510            .field("running", &self.is_running())
511            .field("busy", &self.is_busy())
512            .field("disabled", &self.is_disabled())
513            .field("detached", &self.is_detached())
514            .finish_non_exhaustive()
515    }
516}
517
518// ─── Graph ───────────────────────────────────────────────────────────────
519
520/// The compile-time task graph produced by [`supervisor_graph!`]: the node slots,
521/// the dependency-index table, the topological order, and the elastic pools — the
522/// single value [`Supervisor::new`] consumes. The macro emits one `pub static GRAPH`
523/// of this type. The fields are public so the application can read them directly
524/// (e.g. a status endpoint iterating `GRAPH.nodes` / `GRAPH.deps`).
525///
526/// `N` is capped at 256 (graph indices are `u8`); the macro enforces this at
527/// expansion time.
528pub struct Graph<const N: usize> {
529    /// Node slots, one per declared node. `None` marks a `#[cfg]`-ed-out node.
530    pub nodes: &'static [Option<&'static TaskNode>; N],
531    /// Per-node dependency indices into `nodes` (`deps[i]` lists node `i`'s deps).
532    pub deps: &'static [&'static [u8]; N],
533    /// Topologically sorted indices into `nodes` (dependencies before dependents;
534    /// reverse iteration is the teardown order). A dependency cycle is a compile error.
535    pub order: [u8; N],
536    /// Elastic worker pools to register with the supervisor (empty when unused).
537    #[cfg(feature = "pool")]
538    pub pools: &'static [&'static dyn Pool],
539}
540
541// ─── Supervisor ──────────────────────────────────────────────────────────
542
543/// Orchestrates a set of managed tasks across spawn / teardown / bring-up.
544///
545/// Owned by a single supervisor task. Concurrent access from other tasks goes
546/// through each [`TaskNode`]'s own atomic state, not the `Supervisor` struct.
547pub struct Supervisor<const N: usize> {
548    /// Node slots, one per declared node. `None` marks a slot whose node was
549    /// `#[cfg]`-ed out of the build (feature-gated); every method skips those.
550    nodes: &'static [Option<&'static TaskNode>],
551    /// Per-node dependency indices into `nodes` (`deps[i]` lists the indices of
552    /// the nodes that node `i` depends on). The single runtime source of graph
553    /// topology, generated alongside `order` by the `supervisor_graph!` macro.
554    deps: &'static [&'static [u8]],
555    /// Topologically sorted indices into `nodes`: dependencies before their
556    /// dependents; reverse iteration is the teardown order. Precomputed at
557    /// compile time (a cycle is a compile error), so construction does no work.
558    order: [u8; N],
559    /// Elastic pools, so the control interface can co-control a whole pool from
560    /// any one member (`apply_control` expands the target through
561    /// [`Pool::members`]) — the same registry `run_pools` drives. Taken from
562    /// `GRAPH.pools` at construction (empty when no pool is declared).
563    #[cfg(feature = "pool")]
564    pools: &'static [&'static dyn Pool],
565}
566
567impl<const N: usize> Supervisor<N> {
568    /// Build a supervisor from a precomputed [`Graph`] — the `GRAPH` that
569    /// `supervisor_graph!` emits (node slots, dependency-index table, compile-time
570    /// topological `order`, and the elastic pools). A dependency cycle is a
571    /// *compile* error, so construction is infallible and does no work —
572    /// `start` / `teardown` / `respawn_terminate` just iterate.
573    pub const fn new(graph: &'static Graph<N>) -> Self {
574        Self {
575            nodes: graph.nodes,
576            deps: graph.deps,
577            order: graph.order,
578            #[cfg(feature = "pool")]
579            pools: graph.pools,
580        }
581    }
582
583    /// Spawn every boot node in dependency order. Called once at boot.
584    /// `Mode::OnDemand` nodes are skipped — they're brought up at runtime by
585    /// `start_node`. A **parked** node (no `spawn` fn) is spawned externally by
586    /// `main()` (with hardware handles main owns); it's still marked `running`
587    /// here. Disabled nodes, and `#[cfg]`-ed-out slots, are skipped.
588    pub fn start(&self, spawner: Spawner) -> Result<(), SpawnError> {
589        for i in self.order.iter() {
590            let Some(node) = self.nodes[*i as usize] else {
591                continue;
592            };
593            if matches!(node.mode, Mode::OnDemand) || node.is_disabled() {
594                continue;
595            }
596            info!("supervisor: spawning {} ({})", node.name, node.mode);
597            if let Some(spawn) = node.spawn {
598                spawn(spawner)?;
599            }
600            node.set_running(true);
601        }
602        Ok(())
603    }
604
605    /// Start a single node at runtime — e.g. growing an elastic pool. Resets the
606    /// handle, spawns one instance via the node's `spawn` fn (which must launch
607    /// exactly one), and marks it `running`. Returns `SpawnError::Busy` if the
608    /// underlying embassy task pool is exhausted (the ceiling), which the caller
609    /// treats as "can't grow".
610    pub fn start_node(&self, node: &'static TaskNode, spawner: Spawner) -> Result<(), SpawnError> {
611        node.reset();
612        if let Some(spawn) = node.spawn {
613            spawn(spawner)?;
614        }
615        node.set_running(true);
616        info!("supervisor: started {}", node.name);
617        Ok(())
618    }
619
620    /// Signal `node` to shut down, wait for its ack (panicking on timeout — a
621    /// missing `ack_dropped()` somewhere), then clear `running`. Shared by
622    /// `stop_node` and `teardown`; the caller must have checked `is_running`.
623    async fn shutdown_and_wait(&self, node: &'static TaskNode) {
624        node.signal_shutdown();
625        if let Either::Second(()) = select(
626            node.wait_dropped(),
627            Timer::after_millis(SHUTDOWN_ACK_TIMEOUT_MS),
628        )
629        .await
630        {
631            panic!(
632                "supervisor: task {} did not ack shutdown within {}ms",
633                node.name, SHUTDOWN_ACK_TIMEOUT_MS,
634            );
635        }
636        node.set_running(false);
637    }
638
639    /// Stop a single running node at runtime — e.g. shrinking an elastic pool.
640    /// Signals shutdown, waits for the ack, clears `running`. No-op if the node
641    /// isn't running. Panics if it doesn't ack within the timeout.
642    pub async fn stop_node(&self, node: &'static TaskNode) {
643        if !node.is_running() {
644            return;
645        }
646        self.shutdown_and_wait(node).await;
647        info!("supervisor: stopped {}", node.name);
648    }
649
650    /// Signal every **running** node to shut down in **reverse** topological
651    /// order, awaiting each node's ack before moving to its dependency. Down
652    /// `OnDemand` nodes are skipped (no instance to ack). Pause-mode nodes ack
653    /// and park on `wait_resume()`; Terminate/OnDemand nodes exit. Panics if a
654    /// running node fails to ack within `SHUTDOWN_ACK_TIMEOUT_MS`.
655    pub async fn teardown(&self) {
656        for i in self.order.iter().rev() {
657            let Some(node) = self.nodes[*i as usize] else {
658                continue;
659            };
660            if !node.is_running() {
661                continue;
662            }
663            info!("supervisor: tearing down {}", node.name);
664            self.shutdown_and_wait(node).await;
665        }
666    }
667
668    /// Signal every Pause-mode node to resume. Cheap and synchronous — the tasks
669    /// were parked on `wait_resume()` and pick up immediately. Called separately
670    /// from `respawn_terminate` so the application can fire resume independently
671    /// of the respawn step. Disabled (manually-paused) nodes are skipped so a
672    /// manual pause sticks; there is intentionally no dependency gate here.
673    pub fn resume_pausable(&self) {
674        for i in self.order.iter() {
675            let Some(node) = self.nodes[*i as usize] else {
676                continue;
677            };
678            if matches!(node.mode, Mode::Pause) && !node.is_disabled() {
679                node.reset();
680                info!("supervisor: resuming {}", node.name);
681                node.signal_resume();
682                node.set_running(true);
683            }
684        }
685    }
686
687    /// Reset and re-spawn every Terminate-mode node in dependency order.
688    /// Pause-mode nodes are untouched (use `resume_pausable`); `OnDemand` nodes
689    /// are left down — they re-grow under load via `start_node`. Disabled nodes
690    /// are skipped so a manual stop sticks across the bring-up. The reset happens
691    /// before the spawn so newly-running tasks see a clean handle.
692    pub fn respawn_terminate(&self, spawner: Spawner) -> Result<(), SpawnError> {
693        for i in self.order.iter() {
694            let Some(node) = self.nodes[*i as usize] else {
695                continue;
696            };
697            if matches!(node.mode, Mode::Terminate) && !node.is_disabled() {
698                node.reset();
699                info!("supervisor: respawning {}", node.name);
700                if let Some(spawn) = node.spawn {
701                    spawn(spawner)?;
702                }
703                node.set_running(true);
704            }
705        }
706        Ok(())
707    }
708}
709
710// ─── Runtime control (dependency- and pool-honoring start/stop) ────────────
711//
712// The `apply_control` entry point drives one `ControlCommand` from the
713// application's control surface. Unlike the pool's bare `start_node`/`stop_node`,
714// these honor the graph: a stop cascades through dependents (so nothing is left
715// running without a dependency), a start cascades through deps (so nothing comes
716// up before what it needs), and either expands across a whole `ElasticPool` so
717// the pool is controlled as a unit. A manual stop/pause also sets the
718// lifecycle-spanning `disabled` flag, so it sticks against the elastic policy and
719// the wake respawn.
720
721#[cfg(feature = "control")]
722impl<const N: usize> Supervisor<N> {
723    /// Position of `node` in `self.nodes` (pointer identity — every node is a
724    /// `&'static`). `None` only if the node isn't in this graph (impossible for
725    /// targets sourced from `GRAPH.nodes`; treated as a no-op by callers).
726    fn index_of(&self, node: &'static TaskNode) -> Option<usize> {
727        self.nodes
728            .iter()
729            .position(|n| n.is_some_and(|x| core::ptr::eq(x, node)))
730    }
731
732    /// Whether every dependency of `node` is currently running, resolved through
733    /// the graph's index table. The pool driver checks this before growing a
734    /// worker, so a pool member is never spawned while one of its dependencies is
735    /// down.
736    #[cfg(feature = "pool")]
737    pub(crate) fn deps_running(&self, node: &'static TaskNode) -> bool {
738        match self.index_of(node) {
739            Some(i) => self.deps[i]
740                .iter()
741                .all(|&di| self.nodes[di as usize].is_some_and(|n| n.is_running())),
742            None => false,
743        }
744    }
745
746    /// Seed a membership set with `target` plus — if `target` belongs to an
747    /// elastic pool — every member of that pool, so control is applied to the
748    /// whole pool atomically. Pool membership is read from `GRAPH.pools`; with no
749    /// pools (the `pool` feature off, or none declared) this is just `{target}`.
750    fn seed(&self, target: &'static TaskNode, set: &mut [bool; N]) {
751        if let Some(i) = self.index_of(target) {
752            set[i] = true;
753        }
754        #[cfg(feature = "pool")]
755        for pool in self.pools {
756            let members = pool.members();
757            if members.iter().any(|m| core::ptr::eq(*m, target)) {
758                for m in members {
759                    if let Some(i) = self.index_of(m) {
760                        set[i] = true;
761                    }
762                }
763            }
764        }
765    }
766
767    /// Apply one control command, honoring pool membership and the dependency
768    /// graph. Run from the supervisor's driver loop (never concurrently with
769    /// itself), so the cascade is atomic from the application's perspective.
770    pub async fn apply_control(&self, cmd: ControlCommand, spawner: Spawner) {
771        match cmd.op {
772            ControlOp::Deactivate => self.deactivate(cmd.node).await,
773            ControlOp::Activate => self.activate(cmd.node, spawner).await,
774        }
775    }
776
777    /// Bring `target` (and its pool, and every transitive dependent) down, in
778    /// reverse-topological order so each dependent stops before the dependency it
779    /// relies on. Marks the whole set `disabled` so the stop sticks against the
780    /// elastic policy and the wake respawn until a matching `activate`.
781    async fn deactivate(&self, target: &'static TaskNode) {
782        let mut set = [false; N];
783        self.seed(target, &mut set);
784
785        // Grow the set to include transitive dependents. `order` is
786        // dependency-first, so when we reach a node its deps are already decided;
787        // a node joins if any dep it declares is already in the set.
788        for i in self.order.iter() {
789            let j = *i as usize;
790            if set[j] {
791                continue;
792            }
793            let Some(node) = self.nodes[j] else {
794                continue;
795            };
796            // A detached node declares its dep only for start ordering and intends
797            // to outlive it, so it's never pulled into the cascade.
798            if node.is_detached() {
799                continue;
800            }
801            if self.deps[j].iter().any(|&di| set[di as usize]) {
802                set[j] = true;
803            }
804        }
805
806        // Tear down in reverse topo order (dependents before their deps).
807        for i in self.order.iter().rev() {
808            let j = *i as usize;
809            if !set[j] {
810                continue;
811            }
812            let Some(node) = self.nodes[j] else {
813                continue;
814            };
815            node.set_disabled(true);
816            if node.is_running() {
817                info!("supervisor: control-stop {}", node.name);
818                self.shutdown_and_wait(node).await;
819            }
820        }
821    }
822
823    /// Bring `target` (and its pool, and every transitive dependency) up, in
824    /// topological order so each dependency starts before its dependent. Clears
825    /// `disabled` across the set. `OnDemand` (pool) members are only re-enabled,
826    /// not force-spawned — the elastic policy re-grows them under load, which is
827    /// the whole point of the pool.
828    async fn activate(&self, target: &'static TaskNode, spawner: Spawner) {
829        let mut set = [false; N];
830        self.seed(target, &mut set);
831
832        // Grow the set to include transitive deps. Walk dependents-first
833        // (reverse topo); when a set member is seen, pull in its direct deps.
834        for i in self.order.iter().rev() {
835            let j = *i as usize;
836            if set[j] {
837                for &di in self.deps[j] {
838                    set[di as usize] = true;
839                }
840            }
841        }
842
843        // Bring up in topo order (deps before dependents).
844        for i in self.order.iter() {
845            let j = *i as usize;
846            if !set[j] {
847                continue;
848            }
849            let Some(node) = self.nodes[j] else {
850                continue;
851            };
852            node.set_disabled(false);
853            if node.is_running() {
854                continue;
855            }
856            match node.mode {
857                Mode::Terminate => {
858                    info!("supervisor: control-start {}", node.name);
859                    // SpawnError::Busy (pool exhausted) → can't start, skip.
860                    let _ = self.start_node(node, spawner);
861                }
862                Mode::Pause => {
863                    info!("supervisor: control-resume {}", node.name);
864                    node.reset();
865                    node.signal_resume();
866                    node.set_running(true);
867                }
868                // Pool worker — leave it down; the elastic policy regrows it on
869                // demand now that `disabled` is cleared.
870                Mode::OnDemand => {}
871            }
872        }
873    }
874}
875
876// ─── Topological sort (Kahn's algorithm, const) ───────────────────────────
877//
878// Computes the topological order at *compile time* over a per-node
879// dependency-index table; a dependency cycle is a compile error.
880
881/// Topologically sort a graph given as a per-node dependency-index table.
882///
883/// `deps[i]` lists the indices of the nodes that node `i` depends on; the result
884/// lists node indices in dependency-first order (a dependency appears before its
885/// dependents). The supervisor iterates it forward for `start` /
886/// `respawn_terminate` and in reverse for `teardown`.
887///
888/// Evaluated at compile time by the code `supervisor_graph!` generates — a
889/// dependency **cycle is a compile error** (the `panic!` fires during const
890/// evaluation). `#[doc(hidden)]`: an engine for the macro, not a user-facing API.
891///
892/// Supports at most 256 nodes: indices are `u8`, so a larger `N` would truncate.
893/// The macro rejects bigger graphs at expansion; the assert below is defense in
894/// depth for a manual caller (a const-eval panic, i.e. a compile error).
895#[doc(hidden)]
896#[must_use]
897pub const fn topo_sort_const<const N: usize>(deps: &[&'static [u8]; N]) -> [u8; N] {
898    assert!(
899        N <= 256,
900        "supervisor graph exceeds 256 node slots (indices are u8)"
901    );
902    // in_degree[i] = number of deps of node i not yet resolved.
903    let mut in_degree = [0u8; N];
904    let mut i = 0;
905    while i < N {
906        in_degree[i] = deps[i].len() as u8;
907        i += 1;
908    }
909
910    // Queue (fixed array, head/tail indices) seeded with the dependency-free nodes.
911    let mut queue = [0u8; N];
912    let mut tail = 0;
913    i = 0;
914    while i < N {
915        if in_degree[i] == 0 {
916            queue[tail] = i as u8;
917            tail += 1;
918        }
919        i += 1;
920    }
921
922    let mut order = [0u8; N];
923    let mut produced = 0;
924    let mut head = 0;
925    while head < tail {
926        let node = queue[head] as usize;
927        head += 1;
928        order[produced] = node as u8;
929        produced += 1;
930
931        // Decrement the in-degree of every node that depends on `node`.
932        let mut j = 0;
933        while j < N {
934            if in_degree[j] != 0 {
935                let mut depends = false;
936                let mut k = 0;
937                while k < deps[j].len() {
938                    if deps[j][k] as usize == node {
939                        depends = true;
940                    }
941                    k += 1;
942                }
943                if depends {
944                    in_degree[j] -= 1;
945                    if in_degree[j] == 0 {
946                        queue[tail] = j as u8;
947                        tail += 1;
948                    }
949                }
950            }
951            j += 1;
952        }
953    }
954
955    // A cycle leaves some nodes unproduced. During const eval this panic is a
956    // compile error, so cyclic graphs are rejected at build time. `core::panic!`
957    // (not the crate's defmt-shimmed `panic!`) keeps this const-evaluable.
958    if produced != N {
959        core::panic!("supervisor_graph!: dependency cycle");
960    }
961    order
962}
963
964#[cfg(feature = "pool")]
965mod pool;
966#[cfg(feature = "pool")]
967pub use pool::*;
968
969/// Declare a supervised task graph and compute its topological order at compile
970/// time (single source of nodes, deps, pool, and order). See the
971/// `embassy-supervisor-macros` crate for the surface syntax.
972#[cfg(feature = "macros")]
973pub use embassy_supervisor_macros::supervisor_graph;
974
975// ─── Tests (host-only) ─────────────────────────────────────────────────────
976//
977// Run on the host: `cargo test -p embassy-supervisor --target x86_64-unknown-linux-gnu`
978// (the workspace `.cargo/config.toml` pins the embedded target, so `--target` is
979// required to override it). These exercise the compile-time `topo_sort_const`
980// over index adjacency tables — exactly what `supervisor_graph!` generates.
981#[cfg(test)]
982mod tests {
983    use super::topo_sort_const;
984
985    /// Position of index `x` within `order`.
986    fn pos<const N: usize>(order: &[u8; N], x: u8) -> usize {
987        order.iter().position(|&y| y == x).expect("index present")
988    }
989
990    #[test]
991    fn linear_chain_orders_deps_before_dependents() {
992        // A=0, B=1 dep A, C=2 dep B.
993        const DEPS: [&[u8]; 3] = [&[], &[0], &[1]];
994        const ORDER: [u8; 3] = topo_sort_const(&DEPS);
995        assert_eq!(ORDER, [0, 1, 2]);
996    }
997
998    #[test]
999    fn diamond_puts_root_first_and_join_last() {
1000        // A=0; B=1 dep A; C=2 dep A; D=3 dep B,C.
1001        const DEPS: [&[u8]; 4] = [&[], &[0], &[0], &[1, 2]];
1002        const ORDER: [u8; 4] = topo_sort_const(&DEPS);
1003        assert_eq!(ORDER[0], 0, "root first");
1004        assert_eq!(ORDER[3], 3, "join last");
1005        assert!(pos(&ORDER, 1) < pos(&ORDER, 3), "B before D");
1006        assert!(pos(&ORDER, 2) < pos(&ORDER, 3), "C before D");
1007    }
1008
1009    #[test]
1010    fn independent_nodes_all_present() {
1011        const DEPS: [&[u8]; 2] = [&[], &[]];
1012        const ORDER: [u8; 2] = topo_sort_const(&DEPS);
1013        assert!(ORDER.contains(&0) && ORDER.contains(&1));
1014    }
1015
1016    #[test]
1017    fn unsorted_input_is_sorted() {
1018        // Declared out of dependency order: node 0 depends on 1 and 2, node 2 on 1.
1019        const DEPS: [&[u8]; 3] = [&[1, 2], &[], &[1]];
1020        const ORDER: [u8; 3] = topo_sort_const(&DEPS);
1021        assert!(pos(&ORDER, 1) < pos(&ORDER, 2), "1 before 2");
1022        assert!(pos(&ORDER, 2) < pos(&ORDER, 0), "2 before 0");
1023    }
1024
1025    #[test]
1026    fn evaluates_at_compile_time() {
1027        // The sort runs in a `const` context, proving it is const-evaluable.
1028        // (A cyclic table here would be a *compile* error, not a test failure.)
1029        const DEPS: [&[u8]; 3] = [&[], &[0], &[1]];
1030        const _: () = {
1031            let order = topo_sort_const(&DEPS);
1032            assert!(order[0] == 0 && order[1] == 1 && order[2] == 2);
1033        };
1034    }
1035
1036    // Uncommenting this must fail to compile ("dependency cycle"):
1037    //   const CYCLE: [&[u8]; 2] = [&[1], &[0]];
1038    //   const _BAD: [u8; 2] = topo_sort_const(&CYCLE);
1039}