pub struct Supervisor<const N: usize> { /* private fields */ }Expand description
Orchestrates a set of managed tasks across spawn / teardown / bring-up.
Owned by a single supervisor task. Concurrent access from other tasks goes
through each TaskNode’s own atomic state, not the Supervisor struct.
Implementations§
Source§impl<const N: usize> Supervisor<N>
impl<const N: usize> Supervisor<N>
Sourcepub async fn run_pools(&self, spawner: Spawner) -> ShutdownTimeout
pub async fn run_pools(&self, spawner: Spawner) -> ShutdownTimeout
Drive the registered elastic pools (from GRAPH.pools): run their
policies, then park until the next status signal (SCALE_REQ) or a pool’s
deferred deadline. Runs forever in the success case — meant to be
selected against the application’s control / teardown futures in the
supervisor task; when another arm wins this future is dropped, which is
safe: a half-applied stop is re-driven on the next pass. Returns only
on error: a pool member that missed its shutdown ack during a shrink,
as ShutdownTimeout — the app escalates (its select arm typically
panics or triggers a watchdog reset).
Source§impl<const N: usize> Supervisor<N>
impl<const N: usize> Supervisor<N>
Sourcepub const fn new(graph: &'static Graph<N>) -> Self
pub const fn new(graph: &'static Graph<N>) -> Self
Build a supervisor from a precomputed Graph — the GRAPH that
supervisor_graph! emits (node slots, dependency-index table, compile-time
topological order, and the elastic pools). A dependency cycle is a
compile error, so construction is infallible and does no work —
start / teardown / respawn_terminate just iterate.
Sourcepub async fn start(&self, spawner: Spawner) -> Result<(), SpawnError>
pub async fn start(&self, spawner: Spawner) -> Result<(), SpawnError>
Bring the graph from any quiescent state to running, in dependency
order — cold boot AND re-entry (a sub-graph supervisor is legitimately
start()/teardown()-cycled per app phase). Idempotent: running nodes
are skipped; detached nodes are skipped on re-entry (their instance
survived the teardown — the first start still spawns them, the flag is
app-set afterwards); a Pause instance parked by an earlier teardown is
resumed in place (never double-spawned; like
resume_pausable this bypasses the gate waits,
since the parked instance retains its resources and its slots are empty
by design). Mode::OnDemand nodes are skipped — they’re brought up at
runtime by start_node. A parked node (no spawn fn) is spawned
externally by main() (with hardware handles main owns); it’s still
marked running here. Disabled nodes, and #[cfg]-ed-out slots, are
skipped.
Async because an executor: NAME node first awaits its SpawnerSlot::ready
(bounded by SLOT_READY_TIMEOUT — the rendezvous with a tier or second core
that comes up asynchronously); a slot still empty at the deadline fails the
bring-up with SpawnError::Busy. A node with no executor: slot never
touches the timer.
Sourcepub async fn run(&self, spawner: Spawner) -> RunError
pub async fn run(&self, spawner: Spawner) -> RunError
The canonical driver, as one call: start the graph,
then drive elastic-pool scaling and/or runtime control forever. Returns
only on error — every arm is an app-level escalation (typically
panic! into a hardware-watchdog reset):
match sup.run(spawner).await {
RunError::Spawn(_) => defmt::panic!("supervisor: bring-up failed"),
RunError::Shutdown(e) => defmt::panic!("supervisor: {} missed ack", e.node.name),
}Apps that select extra wake sources into the driver loop (their own
signals, a wake timer) keep writing the loop by hand:
select(sup.run_pools(spawner), wait_control()) + apply_control.
Sourcepub async fn start_node(
&self,
node: &'static TaskNode,
spawner: Spawner,
) -> Result<(), SpawnError>
pub async fn start_node( &self, node: &'static TaskNode, spawner: Spawner, ) -> Result<(), SpawnError>
Start a single node at runtime — e.g. growing an elastic pool. Resets the
handle, spawns one instance via the node’s spawn fn (which must launch
exactly one), and marks it running. Returns SpawnError::Busy if the
underlying embassy task pool is exhausted (the ceiling), which the caller
treats as “can’t grow”.
Sourcepub async fn stop_node(
&self,
node: &'static TaskNode,
) -> Result<(), ShutdownTimeout>
pub async fn stop_node( &self, node: &'static TaskNode, ) -> Result<(), ShutdownTimeout>
Stop a single running node at runtime — e.g. shrinking an elastic pool.
For a Pause node this IS the single-node “pause”: the worker acks and
parks on wait_resume(), and resume_node is the
symmetric other half. Signals shutdown, waits for the ack, clears
running. No-op Ok if the node isn’t running, or is
detached (self-managed — the supervisor never
stops it). A node that misses the ack window is returned as
ShutdownTimeout and stays marked running.
Sourcepub async fn teardown(&self) -> Result<(), ShutdownTimeout>
pub async fn teardown(&self) -> Result<(), ShutdownTimeout>
Signal every running node to shut down in reverse topological
order, awaiting each node’s ack before moving to its dependency. Down
OnDemand nodes are skipped (no instance to ack). Pause-mode nodes ack
and park on wait_resume(); Terminate/OnDemand nodes exit.
Aborts on the first missed ack, returning the offending node as
ShutdownTimeout: continuing would stop dependencies out from under a
still-live dependent. After Err the graph is partially down — the sane
escalations are app-level (hardware watchdog reset, panic!, retry, or
teardown_continue when quiescing the rest
still matters before a reset).
Sourcepub async fn teardown_continue(&self) -> Result<(), ShutdownTimeout>
pub async fn teardown_continue(&self) -> Result<(), ShutdownTimeout>
Best-effort variant of teardown for the
“hardware reset next” escalation path: presses on past a non-acking node
(still in reverse topological order) so the remaining nodes get their
chance to flush and park, and returns the first timeout after
visiting every node. The wedged node’s dependencies are stopped under it
— acceptable only because the caller is about to reset anyway.
Sourcepub fn resume_node(&self, node: &'static TaskNode)
pub fn resume_node(&self, node: &'static TaskNode)
Resume ONE Pause node parked by an earlier stop_node
or teardown — the single-node partner of
resume_pausable, same sequence and the same
deliberate absence of dependency gating (the parked instance retains its
resources). Cheap and synchronous. No-op unless the node is Pause
mode, actually parked (an instance acked without exiting), and neither
disabled (a control pause sticks — clear it
with activate) nor
detached.
Sourcepub fn resume_pausable(&self)
pub fn resume_pausable(&self)
Signal every Pause-mode node to resume. Cheap and synchronous — the tasks
were parked on wait_resume() and pick up immediately. Called separately
from respawn_terminate so the application can fire resume independently
of the respawn step. Disabled (manually-paused) nodes are skipped so a
manual pause sticks, and detached (self-managed) Pause nodes are left
parked; there is intentionally no dependency gate here.
Sourcepub async fn respawn_terminate(
&self,
spawner: Spawner,
) -> Result<(), SpawnError>
pub async fn respawn_terminate( &self, spawner: Spawner, ) -> Result<(), SpawnError>
Reset and re-spawn every Terminate-mode node in dependency order.
Pause-mode nodes are untouched (use resume_pausable); OnDemand nodes
are left down — they re-grow under load via start_node. Disabled nodes
are skipped so a manual stop sticks across the bring-up. Detached nodes are
skipped too: teardown never brought them down, so they are still running
and re-spawning would double-spawn them (see TaskNode::set_detached). The
reset happens before the spawn so newly-running tasks see a clean handle.
Source§impl<const N: usize> Supervisor<N>
impl<const N: usize> Supervisor<N>
Sourcepub async fn apply_control(
&self,
cmd: ControlCommand,
spawner: Spawner,
) -> Result<(), ShutdownTimeout>
pub async fn apply_control( &self, cmd: ControlCommand, spawner: Spawner, ) -> Result<(), ShutdownTimeout>
Apply one control command, honoring pool membership and the dependency
graph — the mailbox-dispatch form of activate /
deactivate (call those directly when you hold the
supervisor). Run from the supervisor’s driver loop (never concurrently
with itself), so the cascade is atomic from the application’s
perspective. A Deactivate cascade propagates a missed shutdown ack as
ShutdownTimeout (the cascade aborts at the offending node, dependents
already stopped); Activate cannot fail this way.
Sourcepub async fn deactivate(
&self,
target: &'static TaskNode,
) -> Result<(), ShutdownTimeout>
pub async fn deactivate( &self, target: &'static TaskNode, ) -> Result<(), ShutdownTimeout>
Bring target (and its pool, and every transitive dependent) down, in
reverse-topological order so each dependent stops before the dependency it
relies on — the cascading “turn this subsystem off” verb, and the exit
half of the subordinate sub-graph pattern’s one-graph variant. Marks
the whole set disabled so the stop sticks against the elastic policy
and the wake respawn until a matching activate.
Aborts with ShutdownTimeout on a missed ack (the offending node stays
running and disabled; dependents visited before it are already down).
Contrast stop_node: ONE node, no cascade, no
disabled latch (the pool-shrink primitive). Call this directly when
you hold the supervisor; request_control +
apply_control is the same operation routed
through the mailbox from code that doesn’t.
Sourcepub async fn activate(&self, target: &'static TaskNode, spawner: Spawner)
pub async fn activate(&self, target: &'static TaskNode, spawner: Spawner)
Bring target (and its pool, and every transitive dependency) up, in
topological order so each dependency starts before its dependent — the
cascading “turn this subsystem on” verb, and the entry half of the
subordinate sub-graph pattern’s one-graph variant: activate on a
subtree’s LEAF pulls its whole dependency chain up, skipping
already-running nodes. Per-node spawn errors are deliberately swallowed
(a cascade is best-effort; a Busy member is re-driven by the pool
policy or a later activate), so this returns () — asymmetric with
deactivate on purpose. Clears
disabled across the set. OnDemand (pool) members are only re-enabled,
not force-spawned — the elastic policy re-grows them under load, which is
the whole point of the pool.