Skip to main content

Supervisor

Struct Supervisor 

Source
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>

Source

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>

Source

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.

Source

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.

Source

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.

Source

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”.

Source

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.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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>

Source

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.

Source

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.

Source

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.

Auto Trait Implementations§

§

impl<const N: usize> !RefUnwindSafe for Supervisor<N>

§

impl<const N: usize> !UnwindSafe for Supervisor<N>

§

impl<const N: usize> Freeze for Supervisor<N>

§

impl<const N: usize> Send for Supervisor<N>

§

impl<const N: usize> Sync for Supervisor<N>

§

impl<const N: usize> Unpin for Supervisor<N>

§

impl<const N: usize> UnsafeUnpin for Supervisor<N>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.