Expand description
§embassy-supervisor — a task-lifecycle supervisor for embassy
Application- and HAL-agnostic primitives for orchestrating a set of embassy tasks: bringing them up in dependency order, tearing them down in reverse, scaling an elastic worker pool with load, and starting/stopping/pausing/ resuming individual tasks at runtime while keeping the dependency graph consistent.
§The model
- The graph is declared once with the
supervisor_graph!macro: each managed task becomes aTaskNodestatic, and the macro bundles the node slots, dependency table, and a topological order computed at compile time (a dependency cycle is a compile error) into a singleGraph(GRAPH). Supervisor::newtakes&GRAPH(no work, no failure) and uses the order to bring tasks up in dependency order (Supervisor::start) and tear them down in reverse (Supervisor::teardown).- Each node carries a
TaskHandleof per-node atomic flags and single-consumerSignals. Every node is single-instance — no counts, no fan-out. SeeTaskHandle.
§Three lifecycles, distinguished by Mode
Mode::Terminate— the task exits its loop on shutdown and is respawned on the next bring-up. Stateless services (a network listener, a logger).Mode::Pause— the task acks the shutdown then parks onwait_resume(); it is resumed in place, never respawned. Tasks that retain a resource across the pause (an open peripheral handle, a socket).Mode::OnDemand— likeTerminate, but not started at boot and not auto-respawned; the supervisor brings it up and down at runtime to scale an elastic worker pool (ElasticPool) with load.
§What the supervisor does not do
- It does not model any power-state transition (sleep/wake): it reacts to “teardown” and “bring-up” requests; the application drives them.
- It does not allocate, and does no work at construction: the topological
sort runs at compile time (see the
supervisor_graph!macro). - It does not observe task internals. Tasks self-report their drop state via
ack_dropped(); a task that fails to ack within a timeout panics the supervisor with the offending node’s name.
§Cargo features
control(default) — the runtime control plane:ControlOp,request_control,Supervisor::apply_control.pool(default) — elastic worker pools:ElasticPool,Supervisor::run_pools, and thepoolsfield ofGraph.defmt— route the supervisor’s logs throughdefmt; without it the log macros are no-ops.
Build with default-features = false for a minimal core that only does
dependency-ordered bring-up/teardown (drops the control plane and pools,
trimming flash and a couple of statics).
§Example
supervisor_graph! declares the whole graph once — it generates the node
statics and a single Graph value GRAPH bundling the node slots, dep
table, and compile-time topological order (a dependency cycle is a compile
error), which Supervisor::new consumes.
use embassy_executor::Spawner;
use embassy_supervisor::{supervisor_graph, Supervisor, wait_control};
// `app` depends on `net`; each `spawn:` names a task fn spawned with the node.
supervisor_graph! {
node NET = Terminate, deps: [], spawn: net_task;
node APP = Terminate, deps: [NET], spawn: app_task;
}
#[embassy_executor::task]
async fn supervisor_task(spawner: Spawner) {
let sup = Supervisor::new(&GRAPH);
sup.start(spawner).expect("initial spawn"); // brings up `net`, then `app`
loop {
// Apply runtime start/stop/pause/resume requests in dependency order.
let cmd = wait_control().await;
sup.apply_control(cmd, spawner).await;
}
// With the `pool` feature you'd instead drive scaling and control together:
// `select(sup.run_pools(spawner), wait_control())` (see the `firmware` crate).
}The firmware crate in the repository
is a complete working example (USB-net, an HTTP control plane, an elastic pool,
and OTA).
Docs:
embassy_sync::signal::Signal: https://docs.embassy.dev/embassy-sync- Kahn’s algorithm (topological sort): https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm
Macros§
- supervisor_
graph - Declare a supervised task graph and compute its topological order at compile
time (single source of nodes, deps, pool, and order). See the
embassy-supervisor-macroscrate for the surface syntax. Declare a supervised task graph; see the crate docs for the surface syntax.
Structs§
- Control
Command - A runtime control request: drive
node(and, per the dependency graph and pool membership, the nodes it implies) in theopdirection. - Deferred
Shrink - Grow immediately (stay responsive), but shrink only after the idle surplus has
persisted for
cooldown— damps grow→shrink→grow flapping. Holds its pending shrink deadline in aCell<Option<Instant>>(interior mutability under&self, since the pool is astatic);None= no shrink pending. - Elastic
Pool - An elastic pool of single-instance nodes scaled by policy
P. - Graph
- The compile-time task graph produced by
supervisor_graph!: the node slots, the dependency-index table, the topological order, and the elastic pools — the single valueSupervisor::newconsumes. The macro emits onepub static GRAPHof this type. The fields are public so the application can read them directly (e.g. a status endpoint iteratingGRAPH.nodes/GRAPH.deps). - Pool
Stats - Aggregate state of a pool, handed to the policy.
- Supervisor
- Orchestrates a set of managed tasks across spawn / teardown / bring-up.
- Task
Handle - Coordination state for one task. Embedded inside
TaskNode. - Task
Node - A node in the supervisor’s task graph.
Enums§
- Control
Op - Which way to drive a node. Higher-level verbs fold onto these two:
start/resume→Activate,stop/pause→Deactivate. The concrete mechanism (respawn vs resume vs leave-to-pool) is then chosen per nodeModeby the supervisor when it applies the command (Supervisor::apply_control). - Mode
- Lifecycle policy for a managed task: what the task does on shutdown and what the supervisor does to bring it back.
- Pool
Action - What the supervisor should do for a pool this tick. The async part (start /
stop) is applied by the caller, keeping
Poolobject-safe without futures. - Scale
Action - What a policy wants done this evaluation.
Traits§
- Pool
- Object-safe, synchronous pool interface so
&dyn Poolneeds no heap: the policy decides here; the supervisor performs the async start/stop. - Scaling
Policy - Swappable scaling decision.
decideis synchronous; stateful policies use interior mutability (the pool is astatic, so&self).
Functions§
- request_
control - Enqueue a control request. Non-blocking; drops if the mailbox is full (4 outstanding), which is harmless for low-frequency manual control. Called by the application’s control surface.
- request_
scale - Fire the scale-request signal. Called by a task on a busy/idle transition.
A no-op when the
poolfeature is disabled (no pools to re-evaluate). - wait_
control - Await the next control request. Selected by the supervisor’s driver loop against pool scaling and any other application wake sources.
- wait_
scale - Await the next scale request. The supervisor’s driver loop selects this against its other wake sources and runs the scaling policy on each wake.