embassy_supervisor/lib.rs
1//! Dependency-ordered task-lifecycle supervision for [embassy](https://embassy.dev) firmware.
2//!
3//! `embassy-supervisor` brings tasks up in dependency order, supervises their
4//! lifecycle (`Terminate`, `Pause`, `OnDemand`), tears dependents down before
5//! the things they depend on, and verifies declared dataflow against live
6//! behaviour. The graph is declared through the [`supervisor_graph!`] macro
7//! (re-exported from `embassy-supervisor-macros`) and checked at compile time.
8//!
9//! The crate is HAL-agnostic, `no_std`, and has no allocator or board-specific
10//! dependencies.
11
12#![cfg_attr(not(test), no_std)]
13#![forbid(unsafe_code)]
14#![deny(missing_docs)]
15
16#[macro_use]
17mod fmt;
18
19#[cfg(all(
20 feature = "log",
21 not(target_os = "none"),
22 not(all(target_arch = "wasm32", target_os = "unknown"))
23))]
24mod host_log;
25#[cfg(all(
26 feature = "log",
27 not(target_os = "none"),
28 not(all(target_arch = "wasm32", target_os = "unknown"))
29))]
30pub use host_log::init_host_logging;
31
32use core::cell::Cell;
33use core::future::Future;
34use core::pin::Pin;
35use core::sync::atomic::Ordering;
36use core::task::{Context, Poll};
37
38use embassy_executor::{SendSpawner, SpawnError, Spawner};
39use embassy_futures::select::{Either, select};
40use embassy_sync::blocking_mutex::Mutex as BlockingMutex;
41use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
42#[cfg(any(feature = "control", feature = "liveness-monitor"))]
43use embassy_sync::channel::Channel;
44use embassy_sync::signal::Signal;
45use embassy_time::{Timer, with_timeout};
46#[cfg(feature = "liveness")]
47use portable_atomic::AtomicBool;
48#[cfg(any(feature = "liveness-monitor", feature = "fault-inject"))]
49use portable_atomic::AtomicU8;
50use portable_atomic::AtomicU16;
51#[cfg(any(
52 feature = "trace",
53 feature = "liveness",
54 feature = "epochs",
55 feature = "fault-inject"
56))]
57use portable_atomic::AtomicU32;
58
59#[cfg(feature = "pool")]
60static SCALE_REQ: Signal<CriticalSectionRawMutex, ()> = Signal::new();
61
62/// Signal that an elastic pool should re-evaluate its scaling decision.
63///
64/// This is a no-op when the `pool` feature is disabled.
65pub fn request_scale() {
66 #[cfg(feature = "pool")]
67 SCALE_REQ.signal(());
68}
69
70#[cfg(feature = "pool")]
71/// Wait until something requests a pool scaling re-evaluation.
72pub async fn wait_scale() {
73 SCALE_REQ.wait().await;
74}
75
76static GATE_EVT: Signal<CriticalSectionRawMutex, ()> = Signal::new();
77
78static STOP_EVT: Signal<CriticalSectionRawMutex, ()> = Signal::new();
79
80#[doc(hidden)]
81#[inline]
82pub fn __sv_gate_event() {
83 GATE_EVT.signal(());
84}
85
86#[cfg(feature = "control")]
87#[non_exhaustive]
88#[derive(Clone, Copy, PartialEq, Eq, Debug)]
89/// A control request issued to the supervisor for a node.
90pub enum ControlOp {
91 /// Request that the node be activated.
92 Activate,
93 /// Request that the node be deactivated.
94 Deactivate,
95 /// Request that the node be restarted.
96 #[cfg(feature = "restart")]
97 Restart,
98}
99
100#[cfg(feature = "control")]
101#[derive(Clone, Copy, Debug)]
102/// A control request addressed to a specific node.
103pub struct ControlCommand {
104 /// The target node.
105 pub node: &'static TaskNode,
106 /// The direction to drive it.
107 pub op: ControlOp,
108}
109
110/// App → supervisor control mailbox. `&'static TaskNode` is `Copy + Sync`, so
111/// the target rides the channel directly — no name lookup needed supervisor-side.
112#[cfg(feature = "control")]
113static CONTROL_REQ: Channel<CriticalSectionRawMutex, ControlCommand, 4> = Channel::new();
114
115/// The control mailbox was full (4 outstanding requests) and the request was
116/// not enqueued. Returned by [`try_request_control`]; retry after the
117/// supervisor's driver loop has drained a command, or use the awaiting
118/// [`request_control`] from async contexts.
119#[cfg(feature = "control")]
120#[derive(Clone, Copy, PartialEq, Eq, Debug)]
121pub struct ControlQueueFull;
122
123#[cfg(all(feature = "control", feature = "defmt"))]
124impl defmt::Format for ControlQueueFull {
125 fn format(&self, fmt: defmt::Formatter) {
126 defmt::write!(fmt, "control queue full");
127 }
128}
129
130/// Enqueue a control request, waiting for mailbox capacity if it is full.
131/// Lossless — the request is delivered once the supervisor's driver loop drains
132/// an earlier command. Called by the application's control surface.
133#[cfg(feature = "control")]
134pub async fn request_control(node: &'static TaskNode, op: ControlOp) {
135 CONTROL_REQ.send(ControlCommand { node, op }).await;
136}
137
138/// Non-blocking variant of [`request_control`] for sync contexts (ISRs,
139/// callbacks). Fails with [`ControlQueueFull`] instead of dropping the request
140/// when the mailbox is full — the caller decides whether to retry or surface it.
141#[cfg(feature = "control")]
142pub fn try_request_control(node: &'static TaskNode, op: ControlOp) -> Result<(), ControlQueueFull> {
143 CONTROL_REQ
144 .try_send(ControlCommand { node, op })
145 .map_err(|_| ControlQueueFull)
146}
147
148/// Await the next control request. Selected by the supervisor's driver loop
149/// against pool scaling and any other application wake sources.
150#[cfg(feature = "control")]
151pub async fn wait_control() -> ControlCommand {
152 CONTROL_REQ.receive().await
153}
154
155// ─── Declared dataflow coupling ───────────────────────────────────────────
156//
157// `deps:` says "spawn me after that". It is consumed once and says nothing
158// about the relationship that holds for the rest of the program's life: the
159// signals a task reads and writes. `reads:`/`writes:` declare *that* relation,
160// which outlives the spawn the `deps:` edge described.
161
162/// A signal a node declares it reads or writes.
163///
164/// Implemented for every `Sync` type by a blanket impl, so no consumer ever
165/// writes one: the graph's `reads:`/`writes:` clauses coerce the named statics
166/// to `&'static dyn CouplingPoint` for you. The trait carries **no methods** —
167/// the supervisor neither knows nor cares what a signal is. Its only purpose is
168/// to type-erase heterogeneous statics into one slice, so identity is the
169/// static's address and nothing else.
170#[cfg(feature = "coupling")]
171pub trait CouplingPoint: Sync {}
172
173#[cfg(feature = "coupling")]
174impl<T: Sync + ?Sized> CouplingPoint for T {}
175
176#[cfg(feature = "heap-state")]
177pub use bytemuck::Zeroable;
178
179#[cfg(feature = "coupling-observe")]
180pub use embassy_supervisor_observe::Observable;
181
182#[cfg(feature = "coupling-observe")]
183#[derive(Clone, Copy)]
184/// A callable that returns a signal's change count for observation.
185pub struct Observer {
186 count: fn() -> u32,
187}
188
189#[cfg(feature = "coupling-observe")]
190impl Observer {
191 /// Wrap a function that returns the signal's current change count.
192 pub const fn new(count: fn() -> u32) -> Self {
193 Self { count }
194 }
195
196 /// Return the current change count.
197 pub fn count(&self) -> u32 {
198 (self.count)()
199 }
200}
201
202#[cfg(feature = "coupling")]
203#[derive(Clone, Copy)]
204/// A declared read/write coupling between a node and a signal.
205pub struct Coupling {
206 name: &'static str,
207 point: &'static dyn CouplingPoint,
208 #[cfg(feature = "coupling-observe")]
209 observe: Option<Observer>,
210 #[cfg(feature = "coupling-observe")]
211 beat: bool,
212 #[cfg(feature = "veto")]
213 veto: Option<u8>,
214}
215
216#[cfg(feature = "coupling")]
217impl Coupling {
218 /// Create a coupling with the given path name and wiring point.
219 pub const fn new(name: &'static str, point: &'static dyn CouplingPoint) -> Self {
220 Self {
221 name,
222 point,
223 #[cfg(feature = "coupling-observe")]
224 observe: None,
225 #[cfg(feature = "coupling-observe")]
226 beat: false,
227 #[cfg(feature = "veto")]
228 veto: None,
229 }
230 }
231
232 #[cfg(feature = "veto")]
233 /// Mark this coupling as a `veto` write holding contributor `slot`.
234 pub const fn veto(mut self, slot: u8) -> Self {
235 self.veto = Some(slot);
236 self
237 }
238
239 /// This writer's contributor slot, if the entry carries the `veto` marker.
240 #[cfg(feature = "veto")]
241 pub const fn veto_slot(&self) -> Option<u8> {
242 self.veto
243 }
244
245 #[cfg(feature = "coupling-observe")]
246 /// Mark this coupling as observed with the given observer.
247 pub const fn observed(mut self, observer: Observer) -> Self {
248 self.observe = Some(observer);
249 self
250 }
251
252 #[cfg(feature = "coupling-observe")]
253 /// Mark this coupling as feeding a heartbeat.
254 pub const fn beat(mut self) -> Self {
255 self.beat = true;
256 self
257 }
258
259 /// Return the path name of this coupling.
260 pub const fn name(&self) -> &'static str {
261 self.name
262 }
263
264 /// This entry's accessor, if it carries the `observed` marker.
265 #[cfg(feature = "coupling-observe")]
266 pub const fn observer(&self) -> Option<Observer> {
267 self.observe
268 }
269
270 /// Return whether this coupling feeds a heartbeat.
271 #[cfg(feature = "coupling-observe")]
272 pub const fn beats(&self) -> bool {
273 self.beat
274 }
275}
276
277#[cfg(feature = "coupling")]
278impl core::fmt::Debug for Coupling {
279 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
280 f.write_str(self.name)
281 }
282}
283
284/// Do two declarations refer to the same static? Compares **data pointers
285/// only** (`ptr::addr_eq`): the same static reached through two different
286/// generic instantiations can carry different vtables, and a plain
287/// `ptr::eq` on trait objects would compare those too and miss the match.
288#[cfg(feature = "coupling")]
289fn same_signal(a: &Coupling, b: &Coupling) -> bool {
290 core::ptr::addr_eq(
291 a.point as *const dyn CouplingPoint,
292 b.point as *const dyn CouplingPoint,
293 )
294}
295
296/// Does `table` carry an entry whose declared path ends in the same segment as
297/// `name`? Emitted by `supervisor_graph!` as a const assertion behind a marked
298/// entry beside `discover`: the entry may only add a marker to a signal the
299/// task fn already accesses, and this is what checks it before anything runs.
300///
301/// **By name, and only the last segment**, because a const context cannot
302/// compare addresses: `ptr::addr_eq` is not `const`, and a pointer has no
303/// integer value at compile time. The declaration and the call site usually
304/// spell the same static differently (`crate::signals::EST` against an aliased
305/// `s::EST`), so the tail is the most that can be matched. Two consequences,
306/// both benign: a signal reached through a renaming re-export fails the check
307/// though it is legitimate, and two distinct signals sharing a final segment
308/// pass it. A marker that lands on the wrong static costs nothing silently —
309/// no verb write matches it, so the node simply never beats and the liveness
310/// monitor reports it stale.
311#[doc(hidden)]
312#[cfg(feature = "coupling")]
313pub const fn __sv_tail_declared(table: &[Coupling], name: &str) -> bool {
314 let mut i = 0;
315 while i < table.len() {
316 if tail_eq(table[i].name, name) {
317 return true;
318 }
319 i += 1;
320 }
321 false
322}
323
324/// Byte offset just past the last `::` in `s`, or 0.
325#[cfg(feature = "coupling")]
326const fn tail_start(s: &str) -> usize {
327 let b = s.as_bytes();
328 let mut i = b.len();
329 while i >= 2 {
330 if b[i - 1] == b':' && b[i - 2] == b':' {
331 return i;
332 }
333 i -= 1;
334 }
335 0
336}
337
338/// Do two paths end in the same segment, index and all (`ARR[1]`)?
339#[cfg(feature = "coupling")]
340const fn tail_eq(a: &str, b: &str) -> bool {
341 let (ab, bb) = (a.as_bytes(), b.as_bytes());
342 let (ai, bi) = (tail_start(a), tail_start(b));
343 if ab.len() - ai != bb.len() - bi {
344 return false;
345 }
346 let mut k = 0;
347 while ai + k < ab.len() {
348 if ab[ai + k] != bb[bi + k] {
349 return false;
350 }
351 k += 1;
352 }
353 true
354}
355
356// ─── Bound readiness edges (task → supervisor) ────────────────────────────
357//
358// A dedicated mailbox rather than a widening of CONTROL_REQ: a flapping link
359// can produce readiness transitions far faster than an operator produces
360// control commands, and the two must not be able to starve each other.
361
362/// "Some node's readiness changed." A coalescing `Signal`, not a queue, and
363/// deliberately so: the cascade handler re-reads live `is_ready` state rather
364/// than trusting a message, so several transitions collapsing into one wake is
365/// not a loss — it is the whole point. A flapping link cannot flood the
366/// supervisor, and no readiness change can ever be dropped for lack of queue
367/// space. Same single-consumer shape as `SCALE_REQ`: many tasks `signal()`,
368/// only the supervisor `wait()`s.
369#[cfg(feature = "bound-deps")]
370static BIND_REQ: Signal<CriticalSectionRawMutex, ()> = Signal::new();
371
372/// Post a readiness transition. Sync and non-blocking — called from task
373/// context inside `set_ready`/`clear_ready`, which must never park.
374#[cfg(feature = "bound-deps")]
375fn notify_bind() {
376 BIND_REQ.signal(());
377}
378
379/// Await the next readiness transition of any node. The supervisor's driver
380/// loop selects this alongside pool scaling and control.
381#[cfg(feature = "bound-deps")]
382pub async fn wait_bind() {
383 BIND_REQ.wait().await
384}
385
386// ─── Fault injection (`fault-inject`) ─────────────────────────────────────
387
388/// A fault injected into a node through [`TaskNode::inject`]. The verbs act on
389/// the task, not the worker.
390///
391/// - **Stall**: stop polling the worker.
392/// - **Wedge**: hide shutdown and swallow the ack.
393/// - **Crash**: drop the worker future.
394/// - **Hog**: busy-spin the executor for the given bound.
395///
396/// Off by default. For benches, tests, and dashboards.
397#[cfg(feature = "fault-inject")]
398#[non_exhaustive]
399#[derive(Clone, Copy, PartialEq, Eq, Debug)]
400pub enum Fault {
401 /// Healthy.
402 None,
403 /// Stop polling the worker.
404 Stall,
405 /// Hide the shutdown request and swallow the ack.
406 Wedge,
407 /// Drop the worker future; the node reads as exited.
408 Crash,
409 /// Busy-spin the executor for the given bound.
410 Hog(embassy_time::Duration),
411}
412
413#[cfg(feature = "fault-inject")]
414impl Fault {
415 /// The verb as a short lowercase name (`"stall"`, `"wedge"`, `"crash"`,
416 /// `"hog"`; `None` is `"none"`), for logs and JSON.
417 pub const fn as_str(self) -> &'static str {
418 match self {
419 Fault::None => "none",
420 Fault::Stall => "stall",
421 Fault::Wedge => "wedge",
422 Fault::Crash => "crash",
423 Fault::Hog(_) => "hog",
424 }
425 }
426
427 fn encode(self) -> (u8, u32) {
428 match self {
429 Fault::None => (0, 0),
430 Fault::Stall => (1, 0),
431 Fault::Wedge => (2, 0),
432 Fault::Crash => (3, 0),
433 Fault::Hog(d) => (4, d.as_millis().min(u32::MAX as u64) as u32),
434 }
435 }
436
437 fn decode(code: u8, ms: u32) -> Self {
438 match code {
439 1 => Fault::Stall,
440 2 => Fault::Wedge,
441 3 => Fault::Crash,
442 4 => Fault::Hog(embassy_time::Duration::from_millis(ms as u64)),
443 _ => Fault::None,
444 }
445 }
446}
447
448#[cfg(all(feature = "fault-inject", feature = "defmt"))]
449impl defmt::Format for Fault {
450 fn format(&self, fmt: defmt::Formatter) {
451 defmt::write!(fmt, "{}", self.as_str());
452 }
453}
454
455/// Why [`TaskNode::inject`] refused.
456#[cfg(feature = "fault-inject")]
457#[non_exhaustive]
458#[derive(Clone, Copy, PartialEq, Eq, Debug)]
459pub enum InjectError {
460 /// The node is a hand-written `spawn:` fn; only wedge applies.
461 NoShell,
462}
463
464#[cfg(all(feature = "fault-inject", feature = "defmt"))]
465impl defmt::Format for InjectError {
466 fn format(&self, fmt: defmt::Formatter) {
467 defmt::write!(
468 fmt,
469 "no shell: only a `task:` node can be stalled, crashed or hogged"
470 );
471 }
472}
473
474/// State for an in-progress `Hog` fault.
475#[cfg(feature = "fault-inject")]
476struct HogState {
477 grace: Timer,
478 bound: embassy_time::Duration,
479}
480
481/// Wrapper for `task:` worker futures. Returns `Some(output)` on completion,
482/// or `None` if [`Fault::Crash`] dropped the worker.
483#[cfg(feature = "fault-inject")]
484#[doc(hidden)]
485pub struct Injected<'a, F> {
486 node: &'a TaskNode,
487 inner: Option<F>,
488 hog: Option<HogState>,
489}
490
491#[cfg(feature = "fault-inject")]
492impl<'a, F: Future + Unpin> Injected<'a, F> {
493 /// Grace period before a `Hog` spin begins.
494 pub const HOG_GRACE: embassy_time::Duration = embassy_time::Duration::from_millis(250);
495
496 #[doc(hidden)]
497 pub fn new(node: &'a TaskNode, inner: F) -> Self {
498 Self {
499 node,
500 inner: Some(inner),
501 hog: None,
502 }
503 }
504}
505
506#[cfg(feature = "fault-inject")]
507impl<F: Future + Unpin> Future for Injected<'_, F> {
508 type Output = Option<F::Output>;
509
510 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
511 let this = self.get_mut();
512 let node = this.node;
513 node.handle.fault_waker.register(cx.waker());
514 loop {
515 match node.fault() {
516 Fault::Stall if !node.handle.flag(flag::SHUTDOWN) => {
517 if core::pin::pin!(node.handle.shutdown_wake.wait())
518 .poll(cx)
519 .is_ready()
520 {
521 continue;
522 }
523 return Poll::Pending;
524 }
525 Fault::Crash => {
526 this.inner = None;
527 let _ = node.handle.fault.compare_exchange(
528 3,
529 0,
530 Ordering::AcqRel,
531 Ordering::Relaxed,
532 );
533 warn!("supervisor: {} crashed (injected)", node.name());
534 return Poll::Ready(None);
535 }
536 Fault::Hog(bound) => {
537 if !matches!(&this.hog, Some(h) if h.bound == bound) {
538 this.hog = Some(HogState {
539 grace: Timer::after(Injected::<F>::HOG_GRACE),
540 bound,
541 });
542 }
543 let hog = this.hog.as_mut().unwrap();
544 if Pin::new(&mut hog.grace).poll(cx).is_pending() {
545 return Poll::Pending;
546 }
547 let bound = hog.bound;
548 this.hog = None;
549 warn!(
550 "supervisor: {} hogging its executor for {} ms (injected)",
551 node.name(),
552 bound.as_millis()
553 );
554 let deadline = embassy_time::Instant::now() + bound;
555 while embassy_time::Instant::now() < deadline {
556 core::hint::spin_loop();
557 }
558 let _ = node.handle.fault.compare_exchange(
559 4,
560 0,
561 Ordering::AcqRel,
562 Ordering::Relaxed,
563 );
564 }
565 _ => {}
566 }
567 break;
568 }
569 let Some(inner) = this.inner.as_mut() else {
570 return Poll::Pending;
571 };
572 match Pin::new(inner).poll(cx) {
573 Poll::Ready(out) => {
574 this.inner = None;
575 Poll::Ready(Some(out))
576 }
577 Poll::Pending => Poll::Pending,
578 }
579 }
580}
581
582// ─── Health events (supervisor → app) ─────────────────────────────────────
583//
584// The reporting half of `liveness-monitor`. Deliberately a mailbox rather than
585// a callback: the supervisor names what it saw, and the application decides —
586// on its own task, at its own priority — what that means. See the module docs
587// on why escalation is not built in.
588
589/// What the monitor observed about a node.
590///
591/// `#[non_exhaustive]`: further observations may be added without a breaking
592/// change, so match with a `_` arm.
593#[cfg(feature = "liveness-monitor")]
594#[non_exhaustive]
595#[derive(Clone, Copy, PartialEq, Eq, Debug)]
596pub enum HealthKind {
597 /// The node is still marked running but has not beaten within its
598 /// `beat_timeout:` for `beat_window:` consecutive sweeps — alive but
599 /// stalled, parked on an await that will never complete. Emitted **once**
600 /// per stall; the next event for this node is a [`Recovered`](Self::Recovered).
601 Stale {
602 /// Ticks since the node's last beat when the sweep tripped.
603 ticks: u32,
604 },
605 /// A node previously reported [`Stale`](Self::Stale) has beaten again.
606 Recovered,
607}
608
609/// One observation from [`Supervisor::monitor`], delivered through
610/// [`wait_health`].
611#[cfg(feature = "liveness-monitor")]
612#[derive(Clone, Copy, Debug)]
613pub struct HealthEvent {
614 /// The node the observation is about.
615 pub node: &'static TaskNode,
616 /// What was observed.
617 pub kind: HealthKind,
618}
619
620#[cfg(all(feature = "liveness-monitor", feature = "defmt"))]
621impl defmt::Format for HealthKind {
622 fn format(&self, fmt: defmt::Formatter) {
623 match self {
624 HealthKind::Stale { ticks } => defmt::write!(fmt, "stale for {} ticks", ticks),
625 HealthKind::Recovered => defmt::write!(fmt, "recovered"),
626 }
627 }
628}
629
630/// Supervisor → app health mailbox. Lossy on purpose (see
631/// [`Supervisor::monitor`]): the monitor must never block on a slow or absent
632/// consumer, and a still-stale node is re-reported by a later sweep anyway.
633#[cfg(feature = "liveness-monitor")]
634static HEALTH_EVT: Channel<CriticalSectionRawMutex, HealthEvent, 4> = Channel::new();
635
636/// Await the next health observation from [`Supervisor::monitor`].
637///
638/// The application's escalation point. What to do with a `Stale` report is
639/// domain-specific and therefore yours: log it, degrade to a safe mode, request
640/// a `Deactivate`, `clear_ready()` the node so future bring-up defers — the
641/// supervisor deliberately does none of these on its own.
642///
643/// ```ignore
644/// loop {
645/// let ev = embassy_supervisor::wait_health().await;
646/// match ev.kind {
647/// HealthKind::Stale { ticks } => warn!("{} stalled for {} ticks", ev.node.name, ticks),
648/// HealthKind::Recovered => info!("{} is beating", ev.node.name),
649/// _ => {}
650/// }
651/// }
652/// ```
653#[cfg(feature = "liveness-monitor")]
654pub async fn wait_health() -> HealthEvent {
655 HEALTH_EVT.receive().await
656}
657
658/// Non-blocking [`wait_health`], for a consumer that polls (a status endpoint
659/// draining pending events, an existing loop that must not park here).
660#[cfg(feature = "liveness-monitor")]
661pub fn try_wait_health() -> Option<HealthEvent> {
662 HEALTH_EVT.try_receive().ok()
663}
664
665/// Post one observation, dropping it if the app isn't keeping up. The monitor
666/// runs on the supervisor task, which must not park behind a health consumer —
667/// blocking here would stall pool scaling and control commands over a
668/// diagnostic.
669#[cfg(feature = "liveness-monitor")]
670fn emit_health(ev: HealthEvent) {
671 if HEALTH_EVT.try_send(ev).is_err() {
672 warn!(
673 "supervisor: health mailbox full, dropped an event for {}",
674 ev.node.name()
675 );
676 }
677}
678
679/// Default per-node timeout for `wait_dropped` (`ack_timeout:` in the graph
680/// overrides it per node). A task that doesn't ack within its window is a bug
681/// (a body that never notices the stop) or a wedge; the shutdown paths
682/// surface it as a [`NodeFault`] naming the node, and the application decides
683/// the escalation. 2 s comfortably exceeds a typical task's poll period and
684/// peripheral settle time.
685const SHUTDOWN_ACK_TIMEOUT_MS: u64 = 2_000;
686
687/// How much finer than its beat budget a `ready_on_write` node is probed while
688/// it has yet to assert. Eight keeps the added readiness latency well inside
689/// what a dependent's `slot_timeout` tolerates, without making bring-up busy.
690#[cfg(all(
691 feature = "liveness-monitor",
692 feature = "coupling-observe",
693 feature = "readiness"
694))]
695const READY_PROBE_DIVISOR: u64 = 8;
696
697/// A node-scoped lifecycle failure: which node, and what went wrong.
698///
699/// Every way bring-up or teardown can fail is about one node, so one type says
700/// so — the same `{ node, kind }` shape as [`HealthEvent`].
701/// It is what [`Supervisor::start`], [`Supervisor::teardown`],
702/// [`Supervisor::run`] and [`Supervisor::restart`] all return.
703///
704/// [`Display`](core::fmt::Display) is unconditional, so an application logging
705/// through anything other than `defmt` can render one without matching on
706/// [`FaultKind`].
707#[derive(Clone, Copy, Debug)]
708pub struct NodeFault {
709 /// The node the failure is about.
710 pub node: &'static TaskNode,
711 /// What went wrong.
712 pub kind: FaultKind,
713}
714
715/// What went wrong in a [`NodeFault`].
716///
717/// `#[non_exhaustive]`: new failure modes may be added without a breaking
718/// change.
719#[non_exhaustive]
720#[derive(Clone, Copy, Debug)]
721pub enum FaultKind {
722 /// A `ready`-marked dep did not assert readiness within the dependent's
723 /// `slot_timeout`. Either the dep is failing to reach its serving state, or
724 /// the budget is too tight for this build. Only plain `ready` edges fault
725 /// here: a `bound` edge parks the dependent instead.
726 ReadyDepTimeout {
727 /// The dep that never asserted.
728 dep: &'static TaskNode,
729 },
730 /// A `resources:` slot was still empty at the deadline — nothing called
731 /// `provide()` before the supervisor started, or a previous instance never
732 /// restored it.
733 ResourceMissing,
734 /// The node names an `executor:` slot that was never filled with a spawner.
735 ExecutorSlotEmpty,
736 /// The spawn itself was rejected: the task's pool is exhausted, or (with
737 /// `heap-state`) its `state:` allocation was refused. These are the only
738 /// cases embassy's own `SpawnError` describes.
739 Spawn(SpawnError),
740 /// The node did not acknowledge a requested shutdown within its ack window.
741 /// It is still marked running; the sane escalations are app-level. Its
742 /// `divisible` shares, if any, have been released on its behalf.
743 ShutdownTimeout,
744}
745
746impl core::fmt::Display for NodeFault {
747 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
748 match self.kind {
749 FaultKind::ReadyDepTimeout { dep } => write!(
750 f,
751 "{}: ready-dep {} did not assert within {}ms",
752 self.node.name(),
753 dep.name(),
754 self.node.slot_timeout().as_millis()
755 ),
756 FaultKind::ResourceMissing => {
757 write!(
758 f,
759 "{}: a resource slot was never provided",
760 self.node.name()
761 )
762 }
763 FaultKind::ExecutorSlotEmpty => {
764 write!(
765 f,
766 "{}: its executor slot was never filled",
767 self.node.name()
768 )
769 }
770 FaultKind::Spawn(_) => {
771 write!(
772 f,
773 "{}: spawn failed, its task pool is exhausted",
774 self.node.name()
775 )
776 }
777 FaultKind::ShutdownTimeout => {
778 write!(f, "{}: missed its shutdown ack", self.node.name())
779 }
780 }
781 }
782}
783
784#[cfg(feature = "defmt")]
785impl defmt::Format for NodeFault {
786 fn format(&self, fmt: defmt::Formatter) {
787 match self.kind {
788 FaultKind::ReadyDepTimeout { dep } => defmt::write!(
789 fmt,
790 "{}: ready-dep {} did not assert within {}ms",
791 self.node.name(),
792 dep.name(),
793 self.node.slot_timeout().as_millis()
794 ),
795 FaultKind::ResourceMissing => {
796 defmt::write!(
797 fmt,
798 "{}: a resource slot was never provided",
799 self.node.name()
800 )
801 }
802 FaultKind::ExecutorSlotEmpty => {
803 defmt::write!(
804 fmt,
805 "{}: its executor slot was never filled",
806 self.node.name()
807 )
808 }
809 FaultKind::Spawn(_) => {
810 defmt::write!(
811 fmt,
812 "{}: spawn failed, its task pool is exhausted",
813 self.node.name()
814 )
815 }
816 FaultKind::ShutdownTimeout => {
817 defmt::write!(fmt, "{}: missed its shutdown ack", self.node.name())
818 }
819 }
820 }
821}
822
823/// The shutdown side of [`TaskNode::run_cancellable`]'s result: the raced work
824/// future was cancelled at its await point because a stop/pause request won the
825/// select. Pairs naturally with the `exit:` slot — a worker returning
826/// `Result<R, Aborted>` records completed-vs-cancelled for whoever reads the
827/// exit value.
828#[derive(Clone, Copy, PartialEq, Eq, Debug)]
829pub struct Aborted;
830
831#[cfg(feature = "defmt")]
832impl defmt::Format for Aborted {
833 fn format(&self, fmt: defmt::Formatter) {
834 defmt::write!(fmt, "aborted by shutdown");
835 }
836}
837
838/// The pause side of [`TaskNode::run_pausable`]'s result: a stop/pause request
839/// won the race, the combinator acked and parked, and the supervisor has since
840/// resumed the node. By the time a body sees `Err(Resumed)` the park is already
841/// over — the next loop iteration is the fresh cycle.
842#[derive(Clone, Copy, PartialEq, Eq, Debug)]
843pub struct Resumed;
844
845#[cfg(feature = "defmt")]
846impl defmt::Format for Resumed {
847 fn format(&self, fmt: defmt::Formatter) {
848 defmt::write!(fmt, "resumed after pause");
849 }
850}
851
852pin_project_lite::pin_project! {
853 /// The future behind [`TaskNode::run_cancellable`] and
854 /// [`run_cancellable_acked`](TaskNode::run_cancellable_acked): races the
855 /// worker against the node's shutdown signal, holding the worker's state
856 /// machine **once**.
857 ///
858 /// Written by hand rather than as `select(fut, wait_shutdown()).await` inside
859 /// an `async fn`, because that shape stores the worker both as the function's
860 /// argument and inside the select (rust-lang/rust#62958) — a doubling paid in
861 /// every caller's static task storage, which for a graph of `cancel` nodes is
862 /// the sum of every worker future in the binary.
863 ///
864 /// `fut` is an `Option` so the abort path can drop the worker in place, via
865 /// the safe `Pin::set`, *before* acking: the ack is what releases the
866 /// supervisor's teardown wait, and a runner whose `Drop` frees hardware must
867 /// have run by then.
868 ///
869 /// Shutdown is polled inline off `node` rather than stored as a
870 /// `wait_shutdown()` future: the signal wait is stateless (register-on-poll),
871 /// so embedding its state machine here would spend ~8 bytes of every
872 /// caller's task storage carrying a second copy of the node reference.
873 struct RunCancellable<'a, F> {
874 #[pin]
875 fut: Option<F>,
876 node: &'a TaskNode,
877 // True only for the `_acked` variant.
878 ack: bool,
879 }
880}
881
882impl<F: Future> Future for RunCancellable<'_, F> {
883 type Output = Result<F::Output, Aborted>;
884
885 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
886 let mut this = self.project();
887 // Worker first, shutdown second — the polling order `select` gave this
888 // before, so a worker that completes in the same wake as a stop request
889 // still reports completion rather than abort.
890 if let Some(fut) = this.fut.as_mut().as_pin_mut()
891 && let Poll::Ready(out) = fut.poll(cx)
892 {
893 return Poll::Ready(Ok(out));
894 }
895 // The flag fast path covers a request that predates this future (the
896 // signal is edge-triggered); a fresh `wait()` per poll is sound because
897 // the `Signal` latches its value and re-registers the waker each poll.
898 if this.node.poll_shutdown(cx) {
899 this.fut.set(None);
900 if *this.ack {
901 this.node.ack_dropped();
902 }
903 return Poll::Ready(Err(Aborted));
904 }
905 Poll::Pending
906 }
907}
908
909pin_project_lite::pin_project! {
910 /// The future behind [`TaskNode::run_pausable`]: [`RunCancellable`] with the
911 /// `Pause` protocol's tail folded in. Racing, it behaves as the `_acked`
912 /// variant; on abort it drops the worker in place, acks, and becomes the
913 /// `wait_resume()` park — one state flag, and the worker's state machine is
914 /// still held exactly once (same layout rationale as [`RunCancellable`]).
915 struct RunPausable<'a, F> {
916 #[pin]
917 fut: Option<F>,
918 node: &'a TaskNode,
919 parked: bool,
920 }
921}
922
923impl<F: Future> Future for RunPausable<'_, F> {
924 type Output = Result<F::Output, Resumed>;
925
926 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
927 let mut this = self.project();
928 if !*this.parked {
929 // Same race, same ordering as `RunCancellable`: worker first, so a
930 // completion in the same wake as a stop request still reports
931 // completion; flag fast path, then the latched signal.
932 if let Some(fut) = this.fut.as_mut().as_pin_mut()
933 && let Poll::Ready(out) = fut.poll(cx)
934 {
935 return Poll::Ready(Ok(out));
936 }
937 if !this.node.poll_shutdown(cx) {
938 return Poll::Pending;
939 }
940 this.fut.set(None);
941 this.node.ack_dropped();
942 *this.parked = true;
943 // Fall through to the resume poll in this same call: `resume_node`
944 // is eligible the instant the ack lands, and a resume signaled
945 // before this poll returns would otherwise latch with no waker
946 // registered — a lost wakeup.
947 }
948 if core::pin::pin!(this.node.handle.resume_wake.wait())
949 .poll(cx)
950 .is_ready()
951 {
952 return Poll::Ready(Err(Resumed));
953 }
954 Poll::Pending
955 }
956}
957
958/// How long the supervisor's bring-up waits for a node's `executor:`
959/// [`SpawnerSlot`] to be filled before failing the spawn with
960/// [`FaultKind::ExecutorSlotEmpty`]. A genuine cross-core rendezvous resolves in microseconds;
961/// a slot empty this long is a misconfiguration (the app never registered that
962/// executor's spawner). Bounded, so a misconfigured graph fails loudly instead of
963/// hanging bring-up forever.
964const SLOT_READY_TIMEOUT: embassy_time::Duration = embassy_time::Duration::from_millis(100);
965
966// ─── Mode ────────────────────────────────────────────────────────────────
967
968/// Lifecycle policy for a managed task: what the task does on shutdown and what
969/// the supervisor does to bring it back.
970#[derive(Clone, Copy, PartialEq, Eq, Debug)]
971pub enum Mode {
972 /// Task exits its loop on shutdown. The supervisor respawns it via the
973 /// node's `spawn` fn from `respawn_terminate`.
974 Terminate,
975 /// Task acks shutdown and parks on `wait_resume()`. The supervisor resumes
976 /// it from `resume_pausable`; the task is never respawned, so it keeps any
977 /// resource it holds (a peripheral handle, a socket) across the pause.
978 Pause,
979 /// Like `Terminate` (exits on shutdown), but **not** started at boot and
980 /// **not** auto-respawned. The supervisor brings it up and down at runtime
981 /// via `start_node` / `stop_node` in response to load — see [`ElasticPool`].
982 /// `start()` skips it; `respawn_terminate()` leaves it down (it
983 /// re-grows under demand); `teardown()` only acts on it while it is running.
984 OnDemand,
985}
986
987impl Mode {
988 /// Stable lower-case wire name, used both for serialization (e.g. a JSON
989 /// task-state view) and for `defmt` logging — the single source of these
990 /// strings.
991 pub fn as_str(&self) -> &'static str {
992 match self {
993 Mode::Terminate => "terminate",
994 Mode::Pause => "pause",
995 Mode::OnDemand => "ondemand",
996 }
997 }
998}
999
1000#[cfg(feature = "defmt")]
1001impl defmt::Format for Mode {
1002 fn format(&self, f: defmt::Formatter) {
1003 defmt::write!(f, "{}", self.as_str());
1004 }
1005}
1006
1007/// Renders [`as_str`](Self::as_str), so a `{}` in this crate's log macros reads
1008/// the same whichever backend is compiled in.
1009impl core::fmt::Display for Mode {
1010 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1011 f.write_str(self.as_str())
1012 }
1013}
1014
1015// ─── TaskHandle ──────────────────────────────────────────────────────────
1016
1017/// Bit assignments of [`TaskHandle::flags`].
1018mod flag {
1019 /// Shutdown requested by the supervisor. Cleared by `reset()`.
1020 pub const SHUTDOWN: u16 = 1 << 0;
1021 /// The instance acked the shutdown (a flag, not a count, since every node
1022 /// is single-instance). Cleared by `reset()`.
1023 pub const DROPPED: u16 = 1 << 1;
1024 /// The supervisor has the node spawned and it hasn't exited. Always-on
1025 /// nodes are set by `start()`; `OnDemand` nodes by `start_node()` /
1026 /// `stop_node()`. `teardown()` only acts on running nodes, so a down
1027 /// `OnDemand` node doesn't stall it.
1028 pub const RUNNING: u16 = 1 << 2;
1029 /// Actively serving (`mark_busy()` / `mark_idle()`); read by the scaling
1030 /// policy.
1031 pub const BUSY: u16 = 1 << 3;
1032 /// The task body returned (`mark_exited()`). Cleared by `reset()`.
1033 /// Together with the lifecycle-spanning `SHUTDOWN` this distinguishes an
1034 /// autonomous completion (`COMPLETED && !SHUTDOWN`) from an acked stop.
1035 pub const COMPLETED: u16 = 1 << 4;
1036 /// Manually deactivated via the control interface, or declared `disabled`
1037 /// in the graph. **Lifecycle-spanning**: not cleared by `reset()`, so a
1038 /// manual stop sticks against the automatic bring-up paths until
1039 /// `Supervisor::activate`; living in a `static`, it also survives a
1040 /// RAM-retaining power-state transition.
1041 pub const DISABLED: u16 = 1 << 5;
1042 /// Self-managed: the supervisor never drives this node. Not cleared by
1043 /// `reset()`. Full rationale on `TaskNode::set_detached`.
1044 pub const DETACHED: u16 = 1 << 6;
1045 /// Task-asserted readiness ("initialized and serving") — distinct from
1046 /// `RUNNING` (spawned). Set by `set_ready()`, cleared by `clear_ready()`
1047 /// and by `reset()` so a respawned provider re-asserts.
1048 #[cfg(feature = "readiness")]
1049 pub const READY: u16 = 1 << 7;
1050 /// Stopped by a bound provider's `clear_ready` — or never started, when a
1051 /// bound dep was still un-ready at the end of the bring-up gate budget —
1052 /// and eligible to come back when readiness returns. Deliberately NOT
1053 /// `DISABLED`: a manual stop must survive a readiness flap, and a bound
1054 /// stop must not survive the provider's recovery. Not cleared by `reset()`
1055 /// — it spans the stopped instance's whole absence.
1056 #[cfg(feature = "bound-deps")]
1057 pub const BOUND_STOPPED: u16 = 1 << 8;
1058 /// Stopped as a dependent of a deactivated node. `deactivate` marks only
1059 /// its seed as `DISABLED`; dependents are marked `COLLATERAL` instead.
1060 /// Both block automatic bring-up, but `activate` clears `COLLATERAL` once
1061 /// no disabled node remains in the node's transitive dependencies. A manual
1062 /// `start_node` also clears it. Unlike `BOUND_STOPPED`, a readiness flap
1063 /// does not release a control stop. Lifecycle-spanning: not cleared by `reset()`.
1064 pub const COLLATERAL: u16 = 1 << 9;
1065 /// `fault-inject`: swallowed ack pending replay.
1066 #[cfg(feature = "fault-inject")]
1067 pub const PENDING_ACK: u16 = 1 << 10;
1068 /// `fault-inject`: swallowed exit pending replay.
1069 #[cfg(feature = "fault-inject")]
1070 pub const PENDING_EXIT: u16 = 1 << 11;
1071}
1072
1073/// Coordination state for one task. Embedded inside [`TaskNode`].
1074///
1075/// Every node is single-instance, so the state is one word of per-node flags
1076/// plus single-consumer signals — no counts, no fan-out. Written by one side
1077/// (task or supervisor) and read by the other: the supervisor requests exit
1078/// (`SHUTDOWN` + `shutdown_wake`), the task acks it (`DROPPED` +
1079/// `dropped_wake`), a parked Pause-mode task resumes on `resume_wake`, and
1080/// the scaling policy reads `RUNNING`/`BUSY`. See the private `flag` module
1081/// for each bit.
1082pub struct TaskHandle {
1083 /// The lifecycle flags, one bit each (see [`flag`]) — one atomic word
1084 /// where separate booleans would pad the handle. Writes pair `Release`
1085 /// with `Acquire` reads, per flag.
1086 flags: AtomicU16,
1087 /// Wake source for `wait_shutdown()`. Fired by `signal_shutdown()`.
1088 shutdown_wake: Signal<CriticalSectionRawMutex, ()>,
1089 /// Wake source for `wait_dropped()`. Fired by `ack_dropped()`.
1090 dropped_wake: Signal<CriticalSectionRawMutex, ()>,
1091 /// Wake source for `wait_resume()` on Pause-mode tasks. Fired by
1092 /// `signal_resume()`.
1093 resume_wake: Signal<CriticalSectionRawMutex, ()>,
1094 /// Wake source for `wait_ready()`. Latching; the supervisor's bring-up is
1095 /// the only pre-fill waiter (single-waiter Signal semantics).
1096 #[cfg(feature = "readiness")]
1097 ready_wake: Signal<CriticalSectionRawMutex, ()>,
1098 /// Instant ticks (truncated) of the last `beat()`; also stamped by
1099 /// `set_running(true)` so a freshly spawned node is never instantly stale.
1100 #[cfg(feature = "liveness")]
1101 last_beat: AtomicU32,
1102 /// Activation generation, bumped on every false→true `running` transition.
1103 /// Starts at 0 ("never activated"); the first spawn makes it 1. Wrapping —
1104 /// dependents compare for *inequality*, never ordering.
1105 #[cfg(feature = "epochs")]
1106 epoch: AtomicU32,
1107 /// Wake source for `wait_epoch_change()`. Single-waiter Signal, like
1108 /// `ready_wake`; the multi-consumer path is polling `epoch()`.
1109 #[cfg(feature = "epochs")]
1110 epoch_wake: Signal<CriticalSectionRawMutex, ()>,
1111 /// Consecutive monitor sweeps that found this node stale. Reset to 0 by any
1112 /// sweep that finds it beating. Saturates at `beat_window`: once the event
1113 /// has been emitted the count only needs to hold the threshold, so a node
1114 /// stale for hours cannot wrap back around and re-report.
1115 #[cfg(feature = "liveness-monitor")]
1116 stale_strikes: AtomicU8,
1117 /// Wrapping sum of this node's beat-feeding `observed` write counters as
1118 /// of the last sweep. One word per node rather than one per entry, which
1119 /// is why a `beat` entry's token must be a counter — a documented
1120 /// requirement on [`Observer`], since value-tokens could cancel in the sum.
1121 #[cfg(all(feature = "coupling-observe", feature = "liveness"))]
1122 write_mark: AtomicU32,
1123 /// The node beat since anyone last looked;
1124 /// [`TaskNode::ticks_since_beat`] converts it into a timestamp with the
1125 /// `now` it reads anyway. One relaxed store per beat instead of a timer
1126 /// read — the write-rate decimation, with the clock cost on the checker,
1127 /// and the reason [`TaskNode::beat`] is cheap enough to call per message.
1128 #[cfg(feature = "liveness")]
1129 pending_beat: AtomicBool,
1130 /// The node's self-description (`report_status`), shown when asked and
1131 /// never acted on. A mutexed cell because a `&'static str` is two words —
1132 /// too wide to swap atomically.
1133 #[cfg(feature = "node-status")]
1134 status: BlockingMutex<CriticalSectionRawMutex, Cell<Option<&'static str>>>,
1135 /// Encoded current fault.
1136 #[cfg(feature = "fault-inject")]
1137 fault: AtomicU8,
1138 /// Hog spin duration in milliseconds.
1139 #[cfg(feature = "fault-inject")]
1140 hog_ms: AtomicU32,
1141 /// Wakes the task on inject or clear.
1142 #[cfg(feature = "fault-inject")]
1143 fault_waker: embassy_sync::waitqueue::AtomicWaker,
1144 /// The executor task id currently running this node (`TaskRef::id()`, captured
1145 /// from the `SpawnToken` by the macro's spawn glue). `0` = unknown (not yet
1146 /// spawned, or a parked/closure-spawned node that never registered). Overwritten
1147 /// on every (re)spawn, so — unlike an external tracker — it stays correct across
1148 /// respawns without any unlinking.
1149 #[cfg(feature = "trace")]
1150 task_id: AtomicU32,
1151 /// Accumulated executor-poll time for this node, in embassy-time ticks,
1152 /// wrapping. Consumers sample twice and `wrapping_sub` to get a rate; the
1153 /// crate does no windowing.
1154 #[cfg(feature = "trace")]
1155 exec_ticks: AtomicU32,
1156 /// Number of executor polls of this node, wrapping.
1157 #[cfg(feature = "trace")]
1158 polls: AtomicU32,
1159 /// Longest single poll ever observed, in ticks — the "never yields" watermark.
1160 /// A large value names the node that hogged the executor even after the fact,
1161 /// which a live check cannot do from the blocked executor itself.
1162 #[cfg(feature = "trace")]
1163 max_poll_ticks: AtomicU32,
1164}
1165
1166impl TaskHandle {
1167 const fn new(disabled_at_boot: bool) -> Self {
1168 Self {
1169 flags: AtomicU16::new(if disabled_at_boot { flag::DISABLED } else { 0 }),
1170 shutdown_wake: Signal::new(),
1171 dropped_wake: Signal::new(),
1172 resume_wake: Signal::new(),
1173 #[cfg(feature = "readiness")]
1174 ready_wake: Signal::new(),
1175 #[cfg(feature = "liveness")]
1176 last_beat: AtomicU32::new(0),
1177 #[cfg(feature = "epochs")]
1178 epoch: AtomicU32::new(0),
1179 #[cfg(feature = "epochs")]
1180 epoch_wake: Signal::new(),
1181 #[cfg(feature = "liveness-monitor")]
1182 stale_strikes: AtomicU8::new(0),
1183 #[cfg(all(feature = "coupling-observe", feature = "liveness"))]
1184 write_mark: AtomicU32::new(0),
1185 #[cfg(feature = "liveness")]
1186 pending_beat: AtomicBool::new(false),
1187 #[cfg(feature = "node-status")]
1188 status: BlockingMutex::new(Cell::new(None)),
1189 #[cfg(feature = "fault-inject")]
1190 fault: AtomicU8::new(0),
1191 #[cfg(feature = "fault-inject")]
1192 hog_ms: AtomicU32::new(0),
1193 #[cfg(feature = "fault-inject")]
1194 fault_waker: embassy_sync::waitqueue::AtomicWaker::new(),
1195 #[cfg(feature = "trace")]
1196 task_id: AtomicU32::new(0),
1197 #[cfg(feature = "trace")]
1198 exec_ticks: AtomicU32::new(0),
1199 #[cfg(feature = "trace")]
1200 polls: AtomicU32::new(0),
1201 #[cfg(feature = "trace")]
1202 max_poll_ticks: AtomicU32::new(0),
1203 }
1204 }
1205
1206 fn flag(&self, bit: u16) -> bool {
1207 self.flags.load(Ordering::Acquire) & bit != 0
1208 }
1209
1210 fn flag_set(&self, bit: u16) {
1211 self.flags.fetch_or(bit, Ordering::Release);
1212 }
1213
1214 /// Clear `bits` — one bit or a whole mask, as `reset()` uses it.
1215 fn flag_clear(&self, bits: u16) {
1216 self.flags.fetch_and(!bits, Ordering::Release);
1217 }
1218
1219 fn flag_put(&self, bit: u16, on: bool) {
1220 if on {
1221 self.flag_set(bit);
1222 } else {
1223 self.flag_clear(bit);
1224 }
1225 }
1226
1227 /// Set or clear `bit` and report whether it was set before — the
1228 /// transition test `mark_busy` / `mark_idle` signal scaling on.
1229 fn flag_swap(&self, bit: u16, on: bool) -> bool {
1230 let prior = if on {
1231 self.flags.fetch_or(bit, Ordering::Release)
1232 } else {
1233 self.flags.fetch_and(!bit, Ordering::Release)
1234 };
1235 prior & bit != 0
1236 }
1237}
1238
1239// ─── Executor spawner slots ──────────────────────────────────────────────
1240
1241/// Runtime-filled slot holding a foreign executor's [`SendSpawner`].
1242///
1243/// Declared by `executor NAME;` and filled by the app before or during
1244/// [`Supervisor::start`]. The supervisor waits on [`ready`](Self::ready)
1245/// before spawning a node into it; if the slot is still empty after the
1246/// bounded wait, the spawn fails with [`FaultKind::ExecutorSlotEmpty`].
1247///
1248/// `default executor NAME;` makes `NAME` the default for nodes that do not
1249/// specify an executor. This lets a supervisor running on an interrupt tier
1250/// keep most of its graph in thread mode:
1251///
1252/// ```ignore
1253/// supervisor_graph! {
1254/// default executor THREAD;
1255/// executor HIGH;
1256/// node LOGGER = Terminate, task: logger_worker;
1257/// node SAMPLER = Terminate, executor: HIGH, task: sampler_worker;
1258/// }
1259/// THREAD.set(spawner.make_send());
1260/// ```
1261///
1262/// `Send` is required on the spawn arguments, not the future. A `task:` shell's
1263/// arguments are always `Send`, so any `task:` worker can be routed to any tier.
1264/// A `spawn:` fn's own arguments must be `Send`, so this does not compile:
1265///
1266/// ```compile_fail
1267/// use std::rc::Rc;
1268/// use embassy_supervisor::{TaskNode, supervisor_graph};
1269///
1270/// #[embassy_executor::task]
1271/// async fn worker(_node: &'static TaskNode, _handle: Rc<u32>) {}
1272///
1273/// supervisor_graph! {
1274/// executor HIGH;
1275/// node A = Terminate, deps: [], executor: HIGH, spawn: worker(Rc::new(1));
1276/// }
1277/// ```
1278///
1279/// What the worker then touches from that tier is the author's contract.
1280pub struct SpawnerSlot {
1281 slot: BlockingMutex<CriticalSectionRawMutex, Cell<Option<SendSpawner>>>,
1282 /// Wakes a `ready()` waiter when `set` fills the slot (cross-core safe:
1283 /// `Signal` is critical-section based and latches).
1284 filled: Signal<CriticalSectionRawMutex, ()>,
1285}
1286
1287impl SpawnerSlot {
1288 /// An empty slot (`const` — it lives in a `static` the macro emits).
1289 pub const fn new() -> Self {
1290 Self {
1291 slot: BlockingMutex::new(Cell::new(None)),
1292 filled: Signal::new(),
1293 }
1294 }
1295
1296 /// Fill the slot (last set wins) and wake a [`ready`](Self::ready) waiter.
1297 /// Call before [`Supervisor::start`] — or from the other core's bring-up,
1298 /// with the supervisor awaiting `ready()`.
1299 pub fn set(&self, spawner: SendSpawner) {
1300 self.slot.lock(|c| c.set(Some(spawner)));
1301 self.filled.signal(());
1302 __sv_gate_event();
1303 }
1304
1305 /// The registered spawner, or `None` while unfilled.
1306 pub fn get(&self) -> Option<SendSpawner> {
1307 self.slot.lock(Cell::get)
1308 }
1309
1310 /// Await the slot and return the spawner. The rendezvous primitive: the
1311 /// supervisor's bring-up awaits this for a node's `executor:` slot before
1312 /// spawning it (bounded, see [`Supervisor::start`]), so a tier filled late — or
1313 /// from another core — is handled without a race. Returns immediately once the
1314 /// slot is filled, so any number of *late* callers are fine (an application can
1315 /// gate work on the executor being up). While the slot is still empty, at most
1316 /// one task should be parked here: the underlying `Signal` holds a single waker,
1317 /// so a second pre-fill waiter would displace the first.
1318 pub async fn ready(&self) -> SendSpawner {
1319 loop {
1320 if let Some(sp) = self.get() {
1321 return sp;
1322 }
1323 // `Signal` latches: a `set()` racing between the check above and
1324 // this wait still wakes us.
1325 self.filled.wait().await;
1326 }
1327 }
1328}
1329
1330impl Default for SpawnerSlot {
1331 fn default() -> Self {
1332 Self::new()
1333 }
1334}
1335
1336// ─── ResourceSlot ────────────────────────────────────────────────────────
1337
1338/// Type-erased readiness view of a [`ResourceSlot`], for the supervisor's
1339/// bring-up wait.
1340///
1341/// A `TaskNode` can gate on any number of slots of *different* `T`s, so the node
1342/// stores `&'static [&'static dyn ResourceGate]` (object-safe: no `T` in the
1343/// signatures). Same shape as embassy's `dyn` driver registries — see
1344/// <https://doc.rust-lang.org/reference/items/traits.html#object-safety>.
1345/// The supervisor only needs "is it filled?" plus the signal to park on; taking
1346/// the value stays in the generated spawn glue, where the concrete `T` is known.
1347pub trait ResourceGate: Sync {
1348 /// Non-consuming "is the slot currently filled" check.
1349 fn is_filled(&self) -> bool;
1350 /// The latching [`Signal`] fired by `provide`/`restore`, for the supervisor's
1351 /// bounded pre-spawn wait (see [`Supervisor::start`]).
1352 fn filled_signal(&self) -> &Signal<CriticalSectionRawMutex, ()>;
1353 /// Empty the slot, dropping any held value — how a stopping provider's
1354 /// `provides:` list is cleared, so a value that dies with its producer
1355 /// reads empty until the next activation re-provides. Default no-op, for a
1356 /// gate view with nothing to clear.
1357 fn clear(&self) {}
1358}
1359
1360/// A one-value handoff cell threading an owned resource from `main` into a
1361/// supervised task — the safe replacement for `Peripherals::steal()` inside
1362/// the task body.
1363///
1364/// Declared (as a `pub static`) by [`supervisor_graph!`] for each entry in a
1365/// node's `resources:` clause. The protocol:
1366///
1367/// 1. `main` splits `Peripherals` and **moves** the resource in with
1368/// [`provide`](Self::provide). This is where the compile-time guarantee
1369/// lives: the singleton field is *consumed*, so no second owner — and no
1370/// `unsafe` steal — can exist.
1371/// 2. The bring-up awaits the slot being filled, then the generated task
1372/// shell [`take`](Self::take)s it — inside the spawned task, so a value
1373/// never crosses the spawn call. A slot still empty at the gate deadline
1374/// fails the spawn with [`FaultKind::ResourceMissing`] — a fail-closed
1375/// error out of [`Supervisor::start`], not a panic inside the task
1376/// (compare `static_cell::StaticCell`, which panics on misuse).
1377/// 3. The generated task shell hands the worker `&mut T` and
1378/// [`restore`](Self::restore)s the value after the worker returns, so a
1379/// `Terminate` respawn re-takes the *same instance* instead of stealing a
1380/// fresh one. (A `Pause` worker never returns — it parks — so it simply
1381/// retains the resource, exactly like a hand-written parked task.)
1382///
1383/// Same primitives as [`SpawnerSlot`]: a critical-section
1384/// [`BlockingMutex`]`<`[`Cell`]`<Option<T>>>` for the value (`Sync` for
1385/// `T: Send`, provided by embassy-sync — no `unsafe` here) plus a latching
1386/// [`Signal`] so the supervisor can await late provisioning (bounded; see
1387/// [`Supervisor::start`]).
1388pub struct ResourceSlot<T> {
1389 slot: BlockingMutex<CriticalSectionRawMutex, Cell<Option<T>>>,
1390 /// Wakes the supervisor's pre-spawn wait when `provide`/`restore` fills the
1391 /// slot (latching, so a fill racing the check-then-wait still wakes it).
1392 filled: Signal<CriticalSectionRawMutex, ()>,
1393}
1394
1395impl<T> ResourceSlot<T> {
1396 /// An empty slot (`const` — it lives in a `static` the macro emits).
1397 pub const fn new() -> Self {
1398 Self {
1399 slot: BlockingMutex::new(Cell::new(None)),
1400 filled: Signal::new(),
1401 }
1402 }
1403
1404 /// Move the resource in (from `main`'s `Peripherals` split) and wake the
1405 /// supervisor's pre-spawn wait. Call before [`Supervisor::start`]; a slot
1406 /// still empty after the supervisor's bounded wait fails that node's spawn
1407 /// with `FaultKind::ExecutorSlotEmpty`. Filling an occupied slot replaces (drops) the
1408 /// old value — don't: one resource, one slot, moved exactly once.
1409 pub fn provide(&self, value: T) {
1410 self.slot.lock(|c| c.set(Some(value)));
1411 self.filled.signal(());
1412 __sv_gate_event();
1413 }
1414
1415 /// Take the resource out, leaving the slot empty. Called by the generated
1416 /// task shell at its first poll; `None` means "not provided yet" or
1417 /// "currently held by a live task instance".
1418 pub fn take(&self) -> Option<T> {
1419 self.slot.lock(Cell::take)
1420 }
1421
1422 /// Copy the resource out **without emptying the slot** — the `shared`
1423 /// resource kind's read: any number of consumers (several nodes, a whole
1424 /// pool) get the same `Copy` handle, and the slot stays filled for the
1425 /// next one. Only for `T: Copy` (a `Stack`-like handle, a `&'static`
1426 /// registry ref); an owned singleton uses [`take`](Self::take).
1427 pub fn get(&self) -> Option<T>
1428 where
1429 T: Copy,
1430 {
1431 // Same peek shape as `is_filled`: `Cell` has no `&T` access, so
1432 // take-copy-put-back under one critical section.
1433 self.slot.lock(|c| {
1434 let v = c.take();
1435 c.set(v);
1436 v
1437 })
1438 }
1439
1440 /// Put the resource back for the next spawn. Called by the generated task
1441 /// shell after the worker returns (i.e. after its clean shutdown ack), so a
1442 /// respawn re-takes the same instance.
1443 pub fn restore(&self, value: T) {
1444 self.provide(value);
1445 }
1446
1447 /// Empty the slot, dropping any held value, and reset the latched filled
1448 /// signal. The provider side of the freshness convention: a task that
1449 /// rebuilds this slot's value each activation clears it on the way down —
1450 /// the `provides:` clause does it from the shutdown ack — so a consumer's
1451 /// gate wait holds for the next activation's value instead of taking this
1452 /// one's leftover.
1453 pub fn clear(&self) {
1454 let stale = self.slot.lock(Cell::take);
1455 drop(stale);
1456 self.filled.reset();
1457 }
1458
1459 /// Await the slot being filled, then take the value — how an application
1460 /// reads a node's `exit:` slot (the shell `provide()`s the worker's return
1461 /// value there just before recording the exit). Check-then-park, so a value
1462 /// provided earlier is returned immediately; the latching signal carries
1463 /// the same single-pre-fill-waiter caveat as [`SpawnerSlot::ready`] — for
1464 /// N concurrent readers fan out through an app-owned `Watch` instead.
1465 pub async fn wait_take(&self) -> T {
1466 loop {
1467 if let Some(v) = self.take() {
1468 return v;
1469 }
1470 self.filled.wait().await;
1471 }
1472 }
1473}
1474
1475// `T: Send` (not just any `T`): the gate is reachable from the supervisor task,
1476// which may run on a different core than the provider — the same bound the
1477// inner `BlockingMutex` requires for `Sync`, restated here so the `dyn` upcast
1478// can't outrun it.
1479impl<T: Send> ResourceGate for ResourceSlot<T> {
1480 fn is_filled(&self) -> bool {
1481 // Peek without consuming: `Cell` has no `&T` access (no `T: Copy`
1482 // here), so take-and-put-back under the same critical section.
1483 self.slot.lock(|c| {
1484 let v = c.take();
1485 let filled = v.is_some();
1486 c.set(v);
1487 filled
1488 })
1489 }
1490
1491 fn filled_signal(&self) -> &Signal<CriticalSectionRawMutex, ()> {
1492 &self.filled
1493 }
1494
1495 fn clear(&self) {
1496 ResourceSlot::clear(self);
1497 }
1498}
1499
1500impl<T> Default for ResourceSlot<T> {
1501 fn default() -> Self {
1502 Self::new()
1503 }
1504}
1505
1506// ─── TaskNode ────────────────────────────────────────────────────────────
1507
1508/// A node in the supervisor's task graph.
1509///
1510/// Designed to live in `static` memory: every field is `Sync`, all constructors
1511/// are `const`. Declared by [`supervisor_graph!`], which emits one per managed
1512/// task along with the [`Graph`] (`GRAPH`) that [`Supervisor::new`] consumes.
1513///
1514/// Split in two on purpose. The handle's atomics force this static into RAM —
1515/// and would force everything beside them into RAM too — so the node holds
1516/// only its live state plus one reference to its [`NodeCfg`], the immutable
1517/// half (name, mode, spawn fn, gates, budgets, coupling tables), which has no
1518/// interior mutability and therefore stays in flash. Same lesson as the
1519/// graph's [`Topology`], one level down: keep the constant data out of reach
1520/// of the atomics.
1521pub struct TaskNode {
1522 /// The immutable half — flash-resident, emitted as its own `static` by
1523 /// [`supervisor_graph!`] beside the node.
1524 cfg: &'static NodeCfg,
1525 handle: TaskHandle,
1526}
1527
1528/// The immutable half of a [`TaskNode`]: everything the graph *declared* about
1529/// the node, none of what happens to it at runtime. No interior mutability, so
1530/// the `static` carrying it lives in flash (`.rodata`); the RAM-resident node
1531/// points at it. Built `const` with [`new`](Self::new) plus the chainable
1532/// `with_*` methods, exactly as [`supervisor_graph!`] emits it.
1533pub struct NodeCfg {
1534 /// Human-readable name. Used in defmt logs and panic messages.
1535 pub name: &'static str,
1536 /// Lifecycle policy. See [`Mode`].
1537 pub mode: Mode,
1538 /// App-provided spawn function (typically an inline closure at the node's
1539 /// declaration). Called once at boot from `Supervisor::start`, again from
1540 /// `respawn_terminate` for Terminate nodes, and at runtime from `start_node`
1541 /// for `OnDemand` nodes. `None` for a **parked** node the application spawns
1542 /// itself (e.g. a `Pause` sensor holding a peripheral handle): the supervisor
1543 /// tracks its lifecycle but never spawns it.
1544 pub spawn: Option<fn(Spawner) -> Result<(), SpawnError>>,
1545 /// The executor [`SpawnerSlot`] this node spawns through (`executor: NAME` in
1546 /// the graph), or `None` to spawn on the supervisor's own `Spawner`. When
1547 /// `Some`, the supervisor holds the spawn until the slot is filled
1548 /// (bounded by [`slot_timeout`](TaskNode::slot_timeout)), so the
1549 /// generated glue's own non-blocking `SpawnerSlot::get` is already filled. Set
1550 /// by the macro via [`with_executor`](Self::with_executor); `const`, zero-cost.
1551 spawn_slot: Option<&'static SpawnerSlot>,
1552 /// The [`ResourceSlot`]s this node's spawn takes from (`resources:` in the
1553 /// graph), type-erased to their [`ResourceGate`] readiness view. The
1554 /// supervisor holds the spawn until every gate is filled (bounded by
1555 /// [`slot_timeout`](TaskNode::slot_timeout)), so (a) a `main` that
1556 /// provides late is tolerated and (b) a respawn cannot race the previous
1557 /// instance's shell restoring the value (the restore happens after the
1558 /// worker's shutdown ack). Empty for nodes without `resources:`. Set by the
1559 /// macro via [`with_resources`](Self::with_resources); `const`, zero-cost.
1560 resource_gates: &'static [&'static dyn ResourceGate],
1561 /// The slots this node's task fills at runtime (`provides:` in the graph),
1562 /// cleared when the node acknowledges a stop so a consumer's gate wait
1563 /// sees the value's absence rather than a previous activation's leftover.
1564 /// A `Pause` ack is exempt: the parked task still backs what it published.
1565 provides: &'static [&'static dyn ResourceGate],
1566 /// `divisible` budget slots this node claims (`resources:`). Released on
1567 /// shutdown, or by the supervisor if the ack times out, so a dead holder
1568 /// does not strand its share. A `Pause` ack keeps the claim.
1569 #[cfg(feature = "budget")]
1570 claims: &'static [(&'static dyn Divisible, u8)],
1571 /// Deps whose task-asserted readiness (`set_ready`) bring-up awaits before
1572 /// spawning this node — the `ready`-marked subset of `deps:`. Spawn-order
1573 /// deps stay in the graph's dep table; this is the readiness overlay.
1574 #[cfg(feature = "readiness")]
1575 ready_deps: &'static [&'static TaskNode],
1576 /// Bound on the pre-spawn waits for this node's `executor:` slot and
1577 /// `resources:` gates. Defaults to [`SLOT_READY_TIMEOUT`] (100 ms — sized
1578 /// for "main provided before start"); raise it (`slot_timeout:` in the
1579 /// graph) for a node whose slots are filled by a **provider node** at
1580 /// runtime — e.g. an async radio bring-up worth hundreds of milliseconds.
1581 /// Set by the macro via [`with_slot_timeout`](Self::with_slot_timeout).
1582 slot_timeout: embassy_time::Duration,
1583 /// How long a stop waits for this node's shutdown ack before faulting it
1584 /// (`ack_timeout:` in the graph). Defaults to [`SHUTDOWN_ACK_TIMEOUT_MS`]
1585 /// (2 s); raise it for a node whose cleanup legitimately takes longer —
1586 /// a flash sync, a peripheral settle. Set by the macro via
1587 /// [`with_ack_timeout`](Self::with_ack_timeout).
1588 ack_timeout: embassy_time::Duration,
1589 /// How long this node may go without a `beat()` before the monitor calls it
1590 /// stale (`beat_timeout:` in the graph). `None` — the default — opts the
1591 /// node OUT of policing entirely, which is right for every node whose body
1592 /// does not beat: it would otherwise read permanently stale.
1593 #[cfg(feature = "liveness-monitor")]
1594 beat_timeout: Option<embassy_time::Duration>,
1595 /// How many *consecutive* stale sweeps are needed before the monitor emits
1596 /// (`beat_window:` in the graph). 1 (the default) reports the first miss;
1597 /// raise it to tolerate a node whose beat interval is legitimately jittery.
1598 #[cfg(feature = "liveness-monitor")]
1599 beat_window: u8,
1600 /// `ready_on_write` in the graph: the sweep calls [`TaskNode::set_ready`]
1601 /// the first time an `observed` write advances, instead of the task
1602 /// asserting readiness itself.
1603 #[cfg(all(feature = "coupling-observe", feature = "readiness"))]
1604 ready_on_write: bool,
1605 /// The signals this node consumes, as one table per source — the
1606 /// `reads:` list, a `discover` node's derived table, each adopted
1607 /// `dataflow:` fn's table. Read by the signal-indexed queries and the
1608 /// diagram tool; never by the spawn machinery.
1609 #[cfg(feature = "coupling")]
1610 reads: &'static [&'static [Coupling]],
1611 /// The write-side tables. An `observed` entry here is additionally polled
1612 /// by [`Supervisor::monitor`], which turns an advance into a beat.
1613 #[cfg(feature = "coupling")]
1614 writes: &'static [&'static [Coupling]],
1615 /// The `bound`-marked subset of `deps:` — providers whose readiness
1616 /// *controls* this node rather than merely gating its first spawn.
1617 #[cfg(feature = "bound-deps")]
1618 bound_deps: &'static [&'static TaskNode],
1619 /// The graph this node belongs to — its view of its own peers, and what a
1620 /// data-driven dependency resolves a producer through. Set by
1621 /// [`supervisor_graph!`]; the graph names the nodes and each node names the
1622 /// graph, which is a cycle only in the address sense and so is a perfectly
1623 /// ordinary pair of statics.
1624 ///
1625 /// [`NO_GRAPH`](graph_ref::NO_GRAPH) for a hand-built node that belongs to
1626 /// no graph, which simply has no peers to answer about.
1627 #[cfg(feature = "data-deps")]
1628 graph: &'static GraphRef,
1629 /// `fault-inject`: true if the node uses a `task:` shell wrapped by [`Injected`].
1630 #[cfg(feature = "fault-inject")]
1631 shelled: bool,
1632}
1633
1634impl NodeCfg {
1635 /// The declared side of a single-instance node started at boot
1636 /// (`Terminate`/`Pause`) or on demand (`Mode::OnDemand`). Every node is
1637 /// single-instance; an elastic service is modelled as several `OnDemand`
1638 /// nodes of the same pooled task fn.
1639 ///
1640 /// A node carries only its own identity and behaviour; the graph's
1641 /// dependency edges live in the compile-time index table that
1642 /// [`supervisor_graph!`] emits and [`Supervisor::new`] consumes.
1643 /// `spawn` is `None` for a parked node the application spawns itself.
1644 pub const fn new(
1645 name: &'static str,
1646 mode: Mode,
1647 spawn: Option<fn(Spawner) -> Result<(), SpawnError>>,
1648 ) -> Self {
1649 Self {
1650 name,
1651 mode,
1652 spawn,
1653 spawn_slot: None,
1654 resource_gates: &[],
1655 provides: &[],
1656 #[cfg(feature = "budget")]
1657 claims: &[],
1658 #[cfg(feature = "readiness")]
1659 ready_deps: &[],
1660 slot_timeout: SLOT_READY_TIMEOUT,
1661 ack_timeout: embassy_time::Duration::from_millis(SHUTDOWN_ACK_TIMEOUT_MS),
1662 #[cfg(feature = "liveness-monitor")]
1663 beat_timeout: None,
1664 #[cfg(feature = "liveness-monitor")]
1665 beat_window: 1,
1666 #[cfg(all(feature = "coupling-observe", feature = "readiness"))]
1667 ready_on_write: false,
1668 #[cfg(feature = "coupling")]
1669 reads: &[],
1670 #[cfg(feature = "coupling")]
1671 writes: &[],
1672 #[cfg(feature = "bound-deps")]
1673 bound_deps: &[],
1674 #[cfg(feature = "data-deps")]
1675 graph: &graph_ref::NO_GRAPH,
1676 #[cfg(feature = "fault-inject")]
1677 shelled: false,
1678 }
1679 }
1680
1681 /// Mark the node as a `task:` shell. Only shells can be stalled, crashed or hogged.
1682 #[cfg(feature = "fault-inject")]
1683 pub const fn with_shell(mut self) -> Self {
1684 self.shelled = true;
1685 self
1686 }
1687
1688 /// Route this node's spawn through the given executor [`SpawnerSlot`] (the
1689 /// `executor: NAME` graph annotation). The supervisor awaits the slot before
1690 /// spawning the node, so a tier filled late — or from another core — is handled
1691 /// without a race, and the generated glue's non-blocking `get` is already filled.
1692 /// `const` and chainable in a `static` initializer; emitted by [`supervisor_graph!`].
1693 pub const fn with_executor(mut self, slot: &'static SpawnerSlot) -> Self {
1694 self.spawn_slot = Some(slot);
1695 self
1696 }
1697
1698 /// Declare the [`ResourceSlot`]s this node's spawn takes from (the
1699 /// `resources:` graph clause). The supervisor awaits every gate being
1700 /// filled before spawning the node, so the generated glue's non-blocking
1701 /// `take()` finds the value. `const` and chainable in a `static`
1702 /// initializer; emitted by [`supervisor_graph!`].
1703 pub const fn with_resources(mut self, gates: &'static [&'static dyn ResourceGate]) -> Self {
1704 self.resource_gates = gates;
1705 self
1706 }
1707
1708 /// Declare the slots this node's task fills at runtime (the `provides:`
1709 /// graph clause); a stop ack clears them. `const` and chainable in a
1710 /// `static` initializer; emitted by [`supervisor_graph!`].
1711 pub const fn with_provides(mut self, slots: &'static [&'static dyn ResourceGate]) -> Self {
1712 self.provides = slots;
1713 self
1714 }
1715
1716 /// Declare the `divisible` budget slots this node claims (`resources:`).
1717 /// Released on stop. `const` and chainable; emitted by [`supervisor_graph!`].
1718 #[cfg(feature = "budget")]
1719 pub const fn with_claims(mut self, claims: &'static [(&'static dyn Divisible, u8)]) -> Self {
1720 self.claims = claims;
1721 self
1722 }
1723
1724 /// Declare the deps whose task-asserted readiness bring-up awaits before
1725 /// spawning this node (the `ready`-marked subset of `deps:`). `const` and
1726 /// chainable in a `static` initializer; emitted by [`supervisor_graph!`].
1727 #[cfg(feature = "readiness")]
1728 pub const fn with_ready_deps(mut self, deps: &'static [&'static TaskNode]) -> Self {
1729 self.ready_deps = deps;
1730 self
1731 }
1732
1733 /// Override the pre-spawn slot/gate wait bound for this node (the
1734 /// `slot_timeout: <millis>` graph clause). The default
1735 /// (`SLOT_READY_TIMEOUT`, 100 ms) assumes slots are provided *before*
1736 /// `start()`; a node consuming a **provider node's** outputs must cover the
1737 /// provider's async build time (the failure mode stays a loud
1738 /// `NodeFault`, just later). `const` and chainable in a `static`
1739 /// initializer; emitted by [`supervisor_graph!`].
1740 pub const fn with_slot_timeout(mut self, timeout: embassy_time::Duration) -> Self {
1741 self.slot_timeout = timeout;
1742 self
1743 }
1744
1745 /// Override how long a stop waits for this node's shutdown ack before
1746 /// faulting it with [`FaultKind::ShutdownTimeout`] (the
1747 /// `ack_timeout: <millis>` graph clause, default 2 s). Raise it for a node
1748 /// whose cleanup legitimately outlasts the default — a flash sync, a
1749 /// peripheral settle; the missed-ack failure mode stays a loud
1750 /// [`NodeFault`], just later. `const` and chainable in a `static`
1751 /// initializer; emitted by [`supervisor_graph!`].
1752 pub const fn with_ack_timeout(mut self, timeout: embassy_time::Duration) -> Self {
1753 self.ack_timeout = timeout;
1754 self
1755 }
1756
1757 /// Opt this node into liveness policing (the `beat_timeout: <millis>` graph
1758 /// clause): [`Supervisor::monitor`] reports it once it has been running
1759 /// without a [`beat`](TaskNode::beat) for longer than `timeout`.
1760 ///
1761 /// Only declare this on a node whose body actually beats — an un-beating
1762 /// node reads permanently stale. `const` and chainable in a `static`
1763 /// initializer; emitted by [`supervisor_graph!`].
1764 #[cfg(feature = "liveness-monitor")]
1765 pub const fn with_beat_timeout(mut self, timeout: embassy_time::Duration) -> Self {
1766 self.beat_timeout = Some(timeout);
1767 self
1768 }
1769
1770 /// How many consecutive stale sweeps the monitor requires before it reports
1771 /// this node (the `beat_window: <n>` graph clause, default 1). Raise it for
1772 /// a node whose beat interval is legitimately jittery — the effective
1773 /// grace period becomes roughly `beat_timeout` + `n` sweep periods.
1774 ///
1775 /// `0` is treated as `1`. `const` and chainable in a `static` initializer;
1776 /// emitted by [`supervisor_graph!`].
1777 #[cfg(feature = "liveness-monitor")]
1778 pub const fn with_beat_window(mut self, sweeps: u8) -> Self {
1779 self.beat_window = if sweeps == 0 { 1 } else { sweeps };
1780 self
1781 }
1782
1783 /// Let an observed write assert readiness (the `ready_on_write` graph
1784 /// clause).
1785 ///
1786 /// The sweep calls [`set_ready`](TaskNode::set_ready) the first time one of this
1787 /// node's `observed` writes advances, so "ready" means "actually producing"
1788 /// rather than "reached the line where it says so". Requires
1789 /// `beat_timeout:`, which is what puts the node in the sweep at all.
1790 ///
1791 /// Monotone by design: it never withdraws readiness. A node that goes quiet
1792 /// is reported through [`wait_health`], and what to do about that stays the
1793 /// application's decision.
1794 #[cfg(all(feature = "coupling-observe", feature = "readiness"))]
1795 pub const fn with_ready_on_write(mut self) -> Self {
1796 self.ready_on_write = true;
1797 self
1798 }
1799
1800 /// Declare the signals this node consumes (the `reads:` graph clause).
1801 /// Purely descriptive: the supervisor never gates on it, and reads carry
1802 /// neither heartbeat nor readiness. What it buys is a graph that says what
1803 /// the node consumes — to [`Graph::readers_of`], to the diagram tool.
1804 /// `const` and chainable
1805 /// in a `static` initializer; emitted by [`supervisor_graph!`].
1806 #[cfg(feature = "coupling")]
1807 pub const fn with_reads(mut self, reads: &'static [&'static [Coupling]]) -> Self {
1808 self.reads = reads;
1809 self
1810 }
1811
1812 /// Declare the signals this node produces (the `writes:` graph clause).
1813 /// See [`with_reads`](Self::with_reads).
1814 #[cfg(feature = "coupling")]
1815 pub const fn with_writes(mut self, writes: &'static [&'static [Coupling]]) -> Self {
1816 self.writes = writes;
1817 self
1818 }
1819
1820 /// Declare the `bound`-marked subset of `deps:` — providers whose
1821 /// readiness controls this node. `const` and chainable in a `static`
1822 /// initializer; emitted by [`supervisor_graph!`].
1823 #[cfg(feature = "bound-deps")]
1824 pub const fn with_bound_deps(mut self, deps: &'static [&'static TaskNode]) -> Self {
1825 self.bound_deps = deps;
1826 self
1827 }
1828
1829 /// Point the node at its own graph. `const` and chainable in a `static`
1830 /// initializer; emitted by [`supervisor_graph!`].
1831 #[cfg(feature = "data-deps")]
1832 pub const fn with_graph(mut self, graph: &'static GraphRef) -> Self {
1833 self.graph = graph;
1834 self
1835 }
1836}
1837
1838impl TaskNode {
1839 /// A node over its flash-resident [`NodeCfg`]. `disabled_at_boot` seeds
1840 /// the node's disabled flag so a control-started node (e.g. an OTA task)
1841 /// can be declared down and started later via a control op. `const`;
1842 /// [`supervisor_graph!`] emits the config `static` and this call together.
1843 pub const fn new(cfg: &'static NodeCfg, disabled_at_boot: bool) -> Self {
1844 Self {
1845 cfg,
1846 handle: TaskHandle::new(disabled_at_boot),
1847 }
1848 }
1849
1850 /// Human-readable name. Used in defmt logs and panic messages.
1851 pub const fn name(&self) -> &'static str {
1852 self.cfg.name
1853 }
1854
1855 /// Lifecycle policy. See [`Mode`].
1856 pub const fn mode(&self) -> Mode {
1857 self.cfg.mode
1858 }
1859
1860 /// Does this node let an observed write assert its readiness?
1861 #[cfg(all(feature = "coupling-observe", feature = "readiness"))]
1862 pub const fn ready_on_write(&self) -> bool {
1863 self.cfg.ready_on_write
1864 }
1865
1866 // ── Declaration getters ──────────────────────────────────────────────
1867 //
1868 // The `with_*` builders are write-only from the application's side; these
1869 // read back what the graph declared, so a status endpoint or a diagnostic
1870 // can report the configuration alongside the live state.
1871
1872 /// This node's pre-spawn gate-wait bound (see
1873 /// [`with_slot_timeout`](NodeCfg::with_slot_timeout)). The whole-graph
1874 /// waves budget all of a node's gates together, from when its in-pass deps
1875 /// resolve; the single-node path ([`start_node`](Supervisor::start_node))
1876 /// gives each gate — executor slot, each `resources:` slot, each `ready`
1877 /// dep — the full budget.
1878 pub const fn slot_timeout(&self) -> embassy_time::Duration {
1879 self.cfg.slot_timeout
1880 }
1881
1882 /// How long a stop waits for this node's shutdown ack before faulting it
1883 /// (see [`with_ack_timeout`](NodeCfg::with_ack_timeout)). Both stop paths
1884 /// honor it: the single-node wait, and the whole-graph wave, where each
1885 /// node's window runs from the moment *it* is signalled.
1886 pub const fn ack_timeout(&self) -> embassy_time::Duration {
1887 self.cfg.ack_timeout
1888 }
1889
1890 /// The deps whose readiness bring-up awaits before spawning this node (the
1891 /// `ready`-marked subset of `deps:`). Empty when none are marked.
1892 #[cfg(feature = "readiness")]
1893 pub const fn ready_deps(&self) -> &'static [&'static TaskNode] {
1894 self.cfg.ready_deps
1895 }
1896
1897 /// This node's liveness budget, or `None` when it is not policed (see
1898 /// [`with_beat_timeout`](NodeCfg::with_beat_timeout)).
1899 #[cfg(feature = "liveness-monitor")]
1900 pub const fn beat_timeout(&self) -> Option<embassy_time::Duration> {
1901 self.cfg.beat_timeout
1902 }
1903
1904 /// Consecutive stale sweeps required before the monitor reports this node
1905 /// (see [`with_beat_window`](NodeCfg::with_beat_window)).
1906 #[cfg(feature = "liveness-monitor")]
1907 pub const fn beat_window(&self) -> u8 {
1908 self.cfg.beat_window
1909 }
1910
1911 /// The signals this node declares it consumes (`reads:`).
1912 #[cfg(feature = "coupling")]
1913 pub const fn reads(&self) -> &'static [&'static [Coupling]] {
1914 self.cfg.reads
1915 }
1916
1917 /// The signals this node declares it produces (`writes:`).
1918 #[cfg(feature = "coupling")]
1919 pub const fn writes(&self) -> &'static [&'static [Coupling]] {
1920 self.cfg.writes
1921 }
1922
1923 /// Every coupling entry in one direction: tables in bound order, entries
1924 /// in table order.
1925 #[cfg(feature = "coupling")]
1926 fn entries(&self, is_write: bool) -> impl Iterator<Item = &'static Coupling> {
1927 let tables = if is_write {
1928 self.cfg.writes
1929 } else {
1930 self.cfg.reads
1931 };
1932 tables.iter().flat_map(|t| t.iter())
1933 }
1934
1935 /// Is `signal` among this node's entries in the given direction?
1936 #[cfg(feature = "coupling")]
1937 fn has_entry(&self, signal: &Coupling, is_write: bool) -> bool {
1938 self.entries(is_write).any(|e| same_signal(e, signal))
1939 }
1940
1941 /// The `bound`-marked subset of `deps:` (see
1942 /// [`with_bound_deps`](NodeCfg::with_bound_deps)).
1943 #[cfg(feature = "bound-deps")]
1944 pub const fn bound_deps(&self) -> &'static [&'static TaskNode] {
1945 self.cfg.bound_deps
1946 }
1947
1948 /// The node slots of the graph this node belongs to, `#[cfg]`-ed-out slots
1949 /// included as `None` — the same table [`Graph::nodes`] exposes, reached
1950 /// from a node rather than from the graph static. Empty for a node no
1951 /// graph declared.
1952 #[cfg(feature = "data-deps")]
1953 pub const fn graph(&self) -> &'static [Option<&'static TaskNode>] {
1954 self.cfg.graph.nodes()
1955 }
1956
1957 /// True while this node is down because a bound provider withdrew
1958 /// readiness — as opposed to `is_disabled`, which means somebody stopped it
1959 /// on purpose. The distinction matters: a bound stop must lift by itself
1960 /// when the provider recovers, and a manual stop must not.
1961 #[cfg(feature = "bound-deps")]
1962 pub fn is_bound_stopped(&self) -> bool {
1963 self.handle.flag(flag::BOUND_STOPPED)
1964 }
1965
1966 // ── Task-side API ────────────────────────────────────────────────────
1967 //
1968 // Called from inside the `#[embassy_executor::task] async fn` body. The
1969 // whole task-side protocol is four rules (the README's "Writing supervised
1970
1971 /// True if the supervisor has asked this node to shut down.
1972 /// A [`Fault::Wedge`] hides the request until cleared.
1973 pub fn shutdown_requested(&self) -> bool {
1974 #[cfg(feature = "fault-inject")]
1975 if self.fault() == Fault::Wedge {
1976 return false;
1977 }
1978 self.handle.flag(flag::SHUTDOWN)
1979 }
1980
1981 /// Wait until the supervisor asks this node to shut down.
1982 /// A [`Fault::Wedge`] keeps this pending until cleared.
1983 pub async fn wait_shutdown(&self) {
1984 #[cfg(feature = "fault-inject")]
1985 while !self.shutdown_requested() {
1986 core::future::poll_fn(|cx| {
1987 self.handle.fault_waker.register(cx.waker());
1988 if self.shutdown_requested() {
1989 return Poll::Ready(());
1990 }
1991 core::pin::pin!(self.handle.shutdown_wake.wait()).poll(cx)
1992 })
1993 .await;
1994 }
1995 #[cfg(not(feature = "fault-inject"))]
1996 if !self.handle.flag(flag::SHUTDOWN) {
1997 self.handle.shutdown_wake.wait().await;
1998 }
1999 }
2000
2001 /// Poll the shutdown request, used by the cancel/pause drivers.
2002 /// A [`Fault::Wedge`] hides the request but leaves the waker registered.
2003 fn poll_shutdown(&self, cx: &mut Context<'_>) -> bool {
2004 #[cfg(feature = "fault-inject")]
2005 self.handle.fault_waker.register(cx.waker());
2006 #[cfg(feature = "fault-inject")]
2007 if self.fault() == Fault::Wedge {
2008 return false;
2009 }
2010 self.handle.flag(flag::SHUTDOWN)
2011 || core::pin::pin!(self.handle.shutdown_wake.wait())
2012 .poll(cx)
2013 .is_ready()
2014 }
2015
2016 /// Acknowledge that this node's instance has dropped and notify waiters.
2017 /// A [`Fault::Wedge`] swallows the ack until cleared.
2018 pub fn ack_dropped(&self) {
2019 self.release_provided();
2020 #[cfg(feature = "fault-inject")]
2021 if self.wedge_swallows(flag::PENDING_ACK) {
2022 return;
2023 }
2024 self.settle_dropped();
2025 }
2026
2027 /// Swallow an ack while wedged. Returns true if the clear must deliver it.
2028 #[cfg(feature = "fault-inject")]
2029 fn wedge_swallows(&self, bits: u16) -> bool {
2030 if self.fault() == Fault::Wedge {
2031 self.handle.flag_set(bits);
2032 if self.fault() == Fault::Wedge {
2033 return true;
2034 }
2035 let prev = self.handle.flags.fetch_and(!bits, Ordering::AcqRel);
2036 if prev & flag::PENDING_ACK == 0 {
2037 return true;
2038 }
2039 }
2040 false
2041 }
2042
2043 /// Drop what this instance provided and release its claims.
2044 fn release_provided(&self) {
2045 if !matches!(self.cfg.mode, Mode::Pause) {
2046 for gate in self.cfg.provides {
2047 gate.clear();
2048 }
2049 #[cfg(feature = "budget")]
2050 self.release_claims();
2051 }
2052 }
2053
2054 /// Update flags and wakes to record the dropped instance.
2055 fn settle_dropped(&self) {
2056 self.handle.flag_clear(flag::RUNNING);
2057 self.handle.flag_set(flag::DROPPED);
2058 self.handle.dropped_wake.signal(());
2059 STOP_EVT.signal(());
2060 #[cfg(feature = "bound-deps")]
2061 notify_bind();
2062 #[cfg(all(feature = "data-deps", feature = "readiness"))]
2063 crate::data_deps::notify_serving();
2064 }
2065
2066 /// Deliver an ack or exit that a wedge swallowed.
2067 #[cfg(feature = "fault-inject")]
2068 fn replay_swallowed(&self) {
2069 let pending = self
2070 .handle
2071 .flags
2072 .fetch_and(!(flag::PENDING_ACK | flag::PENDING_EXIT), Ordering::AcqRel);
2073 if pending & flag::PENDING_ACK != 0 {
2074 if pending & flag::PENDING_EXIT != 0 {
2075 self.handle.flag_set(flag::COMPLETED);
2076 }
2077 self.settle_dropped();
2078 }
2079 }
2080
2081 /// Give back every `divisible` share this node holds (see
2082 /// [`NodeCfg::with_claims`]). Idempotent: releasing an empty slot stores zero.
2083 #[cfg(feature = "budget")]
2084 pub fn release_claims(&self) {
2085 for (budget, slot) in self.cfg.claims {
2086 budget.release(*slot);
2087 }
2088 }
2089
2090 /// Mark the task as completed and acknowledge its drop.
2091 /// A wedge hides this until cleared.
2092 pub fn mark_exited(&self) {
2093 self.release_provided();
2094 #[cfg(feature = "fault-inject")]
2095 if self.wedge_swallows(flag::PENDING_EXIT | flag::PENDING_ACK) {
2096 return;
2097 }
2098 self.handle.flag_set(flag::COMPLETED);
2099 self.settle_dropped();
2100 }
2101
2102 /// Current injected fault, or `None` if healthy.
2103 #[cfg(feature = "fault-inject")]
2104 pub fn fault(&self) -> Fault {
2105 Fault::decode(
2106 self.handle.fault.load(Ordering::Acquire),
2107 self.handle.hog_ms.load(Ordering::Relaxed),
2108 )
2109 }
2110
2111 /// Inject a fault, or clear it with `Fault::None`.
2112 /// Stall, crash and hog need a `task:` shell; wedge works on any node.
2113 #[cfg(feature = "fault-inject")]
2114 pub fn inject(&self, fault: Fault) -> Result<(), InjectError> {
2115 if fault == Fault::None {
2116 self.clear_fault();
2117 return Ok(());
2118 }
2119 if fault != Fault::Wedge && !self.cfg.shelled {
2120 return Err(InjectError::NoShell);
2121 }
2122 let (code, ms) = fault.encode();
2123 self.handle.hog_ms.store(ms, Ordering::Relaxed);
2124 self.handle.fault.store(code, Ordering::Release);
2125 // Replacing a wedge releases anything it was holding back.
2126 if fault != Fault::Wedge {
2127 self.replay_swallowed();
2128 }
2129 // Wake the shell so stall or hog take effect immediately.
2130 self.handle.fault_waker.wake();
2131 Ok(())
2132 }
2133
2134 /// Clear the injected fault and replay anything it withheld.
2135 #[cfg(feature = "fault-inject")]
2136 pub fn clear_fault(&self) {
2137 self.handle.fault.swap(0, Ordering::AcqRel);
2138 self.replay_swallowed();
2139 // Wake any worker parked on the now-visible shutdown request.
2140 // Do not re-fire `shutdown_wake`; its latch would outlive this fault.
2141 self.handle.fault_waker.wake();
2142 }
2143
2144 #[doc(hidden)]
2145 pub fn mark_lost_resource(&self) {
2146 warn!(
2147 "supervisor: {} lost a resource between spawn and first poll",
2148 self.name()
2149 );
2150 self.mark_exited();
2151 }
2152
2153 /// Return `true` if the node has been marked as exited.
2154 pub fn has_exited(&self) -> bool {
2155 self.handle.flag(flag::COMPLETED)
2156 }
2157
2158 #[cfg(feature = "readiness")]
2159 /// Assert this node's readiness and wake dependents waiting on it.
2160 pub fn set_ready(&self) {
2161 self.handle.flag_set(flag::READY);
2162 self.handle.ready_wake.signal(());
2163 __sv_gate_event();
2164 #[cfg(feature = "data-deps")]
2165 crate::data_deps::notify_serving();
2166 #[cfg(feature = "bound-deps")]
2167 notify_bind();
2168 #[cfg(feature = "pool")]
2169 request_scale();
2170 }
2171
2172 #[cfg(feature = "readiness")]
2173 /// Clear this node's readiness.
2174 pub fn clear_ready(&self) {
2175 self.handle.flag_clear(flag::READY);
2176 #[cfg(feature = "bound-deps")]
2177 notify_bind();
2178 }
2179
2180 #[cfg(feature = "readiness")]
2181 /// Return whether this node is currently ready.
2182 pub fn is_ready(&self) -> bool {
2183 self.handle.flag(flag::READY)
2184 }
2185
2186 #[cfg(feature = "readiness")]
2187 /// Wait until this node becomes ready.
2188 pub async fn wait_ready(&self) {
2189 loop {
2190 if self.is_ready() {
2191 return;
2192 }
2193 self.handle.ready_wake.wait().await;
2194 }
2195 }
2196
2197 #[cfg(all(feature = "pool", feature = "readiness"))]
2198 pub(crate) fn ready_deps_ok(&self) -> bool {
2199 self.cfg.ready_deps.iter().all(|d| d.is_ready())
2200 }
2201 #[cfg(all(feature = "pool", not(feature = "readiness")))]
2202 pub(crate) fn ready_deps_ok(&self) -> bool {
2203 true
2204 }
2205
2206 #[cfg(feature = "liveness")]
2207 #[inline]
2208 /// Record a liveness beat for this node.
2209 pub fn beat(&self) {
2210 self.handle.pending_beat.store(true, Ordering::Relaxed);
2211 }
2212
2213 #[cfg(all(feature = "coupling-observe", feature = "liveness"))]
2214 /// Return whether any observed beat coupling has changed since last call.
2215 pub fn poll_observed_writes(&self) -> bool {
2216 let mut mark = 0u32;
2217 let mut any = false;
2218 for w in self.entries(true) {
2219 if !w.beats() {
2220 continue;
2221 }
2222 if let Some(o) = w.observer() {
2223 mark = mark.wrapping_add(o.count());
2224 any = true;
2225 }
2226 }
2227 any && self.handle.write_mark.swap(mark, Ordering::AcqRel) != mark
2228 }
2229
2230 #[cfg(all(feature = "coupling-observe", feature = "liveness"))]
2231 fn seed_write_mark(&self) {
2232 let mut mark = 0u32;
2233 for w in self.entries(true) {
2234 if w.beats()
2235 && let Some(o) = w.observer()
2236 {
2237 mark = mark.wrapping_add(o.count());
2238 }
2239 }
2240 self.handle.write_mark.store(mark, Ordering::Release);
2241 }
2242
2243 #[cfg(feature = "node-status")]
2244 /// Report a status string for this node.
2245 pub fn report_status(&self, status: &'static str) {
2246 let prev = self.handle.status.lock(|s| s.replace(Some(status)));
2247 // `&'static str`s for a status are typically literals, so pointer
2248 // inequality is "changed" for logging purposes; a same-text status
2249 // reached through two literals logs once more, harmlessly.
2250 if prev.is_none_or(|p| !core::ptr::eq(p.as_ptr(), status.as_ptr())) {
2251 info!("supervisor: {}: {}", self.cfg.name, status);
2252 }
2253 }
2254
2255 /// The node's current self-description, if it reported one this activation.
2256 #[cfg(feature = "node-status")]
2257 pub fn status(&self) -> Option<&'static str> {
2258 self.handle.status.lock(|s| s.get())
2259 }
2260
2261 /// Ticks since the last [`beat`](Self::beat) — where, with
2262 /// `dataflow`, a write through the node's verbs since the previous
2263 /// call counts as one, granted here (wrapping arithmetic; correct
2264 /// for gaps under the u32 tick wrap, ~71 min at 1 MHz — far above any sane
2265 /// `max_age`).
2266 #[cfg(feature = "liveness")]
2267 pub fn ticks_since_beat(&self) -> u32 {
2268 let now = embassy_time::Instant::now().as_ticks() as u32;
2269 // A beat since the last look is granted here: the checker pays the
2270 // (already-read) clock, the beating task never does. Load-then-swap so
2271 // a plain check stays one load; racing checkers are benign — one
2272 // stamps, the rest see it stamped.
2273 if self.handle.pending_beat.load(Ordering::Relaxed)
2274 && self.handle.pending_beat.swap(false, Ordering::AcqRel)
2275 {
2276 self.handle.last_beat.store(now, Ordering::Release);
2277 }
2278 now.wrapping_sub(self.handle.last_beat.load(Ordering::Acquire))
2279 }
2280
2281 /// Ticks until this node next needs looking at, for the monitor's sleep.
2282 ///
2283 /// `None` when the node is unpoliced. Three cases, in the order they matter:
2284 ///
2285 /// * **Waiting to assert readiness** (`ready_on_write`, not yet ready) — a
2286 /// short probe. Readiness gates dependents' spawns against their
2287 /// `slot_timeout`, so noticing the first write late spends someone else's
2288 /// budget. Bring-up is brief and latency-sensitive; a fraction of the beat
2289 /// budget buys that back and costs nothing once the node is ready.
2290 /// * **Overdue** — half a budget, so a stalled node's `beat_window` strikes
2291 /// accumulate at a bounded rate instead of spinning on a zero delay.
2292 /// * **Running normally** — exactly when it would go stale.
2293 ///
2294 /// A node that is down or detached is re-examined a budget later: there is
2295 /// nothing to report about it now, but it may be running by then.
2296 #[cfg(feature = "liveness-monitor")]
2297 fn ticks_until_check(&self) -> Option<u64> {
2298 let budget = self.cfg.beat_timeout?.as_ticks();
2299 if self.is_detached() || !self.is_running() {
2300 return Some(budget);
2301 }
2302 // The probe serves the sweep-driven (`observed`) form only; a verb
2303 // write asserts readiness inline, with nothing to poll for.
2304 #[cfg(all(feature = "coupling-observe", feature = "readiness"))]
2305 if self.cfg.ready_on_write && !self.is_ready() {
2306 return Some((budget / READY_PROBE_DIVISOR).max(1));
2307 }
2308 Some(
2309 match budget.saturating_sub(self.ticks_since_beat() as u64) {
2310 0 => (budget / 2).max(1),
2311 remaining => remaining,
2312 },
2313 )
2314 }
2315
2316 /// True when the node is running but hasn't beaten within `max_age` — the
2317 /// alive-but-stalled detector (a task hogging nothing, parked on an await
2318 /// that will never complete). Not-running nodes are never stale: a stopped
2319 /// or completed node is *down*, which `is_running`/`has_exited` already
2320 /// report. Complements the `trace` stall watermark, which catches the
2321 /// opposite failure (a poll that never yields).
2322 #[cfg(feature = "liveness")]
2323 pub fn is_stale(&self, max_age: embassy_time::Duration) -> bool {
2324 self.is_running() && u64::from(self.ticks_since_beat()) > max_age.as_ticks()
2325 }
2326
2327 /// This node's activation generation: `0` before the first spawn, then
2328 /// incremented on every transition into `running` — a fresh spawn, a pool
2329 /// grow, a `respawn_terminate`, or a `Pause` node's resume.
2330 ///
2331 /// **The dependent-side answer to "my provider was restarted underneath
2332 /// me".** `deps:` gates a spawn once; nothing re-gates a node that is
2333 /// *already running* when one of its providers cycles. A consumer holding
2334 /// derived state (a filter, a session, a cached handle) samples this once
2335 /// and compares it each iteration — one relaxed load, cheap enough for a
2336 /// 1 kHz loop:
2337 ///
2338 /// ```ignore
2339 /// let mut seen = PROVIDER.epoch();
2340 /// loop {
2341 /// let sample = INPUT.wait().await;
2342 /// let now = PROVIDER.epoch();
2343 /// if now != seen {
2344 /// seen = now;
2345 /// filter.reset(); // the provider is a new instance; derived state is stale
2346 /// }
2347 /// // ...
2348 /// }
2349 /// ```
2350 #[cfg(feature = "epochs")]
2351 pub fn epoch(&self) -> u32 {
2352 self.handle.epoch.load(Ordering::Acquire)
2353 }
2354
2355 #[cfg(feature = "epochs")]
2356 /// Wait until the node's epoch counter differs from `seen`.
2357 pub async fn wait_epoch_change(&self, seen: u32) -> u32 {
2358 loop {
2359 let now = self.epoch();
2360 if now != seen {
2361 return now;
2362 }
2363 self.handle.epoch_wake.wait().await;
2364 }
2365 }
2366
2367 /// Wait until the supervisor signals this Pause-mode node to resume.
2368 pub async fn wait_resume(&self) {
2369 self.handle.resume_wake.wait().await;
2370 }
2371
2372 /// Run `fut` until it completes or the node is stopped, returning [`Aborted`] on stop.
2373 pub fn run_cancellable<F: Future>(
2374 &self,
2375 fut: F,
2376 ) -> impl Future<Output = Result<F::Output, Aborted>> {
2377 RunCancellable {
2378 fut: Some(fut),
2379 node: self,
2380 ack: false,
2381 }
2382 }
2383
2384 /// Like [`run_cancellable`](Self::run_cancellable), but also acks the stop handshake.
2385 pub fn run_cancellable_acked<F: Future>(
2386 &self,
2387 fut: F,
2388 ) -> impl Future<Output = Result<F::Output, Aborted>> {
2389 RunCancellable {
2390 fut: Some(fut),
2391 node: self,
2392 ack: true,
2393 }
2394 }
2395
2396 /// Run `fut` until it completes or the node is paused, returning [`Resumed`] on pause.
2397 pub fn run_pausable<F: Future>(
2398 &self,
2399 fut: F,
2400 ) -> impl Future<Output = Result<F::Output, Resumed>> {
2401 RunPausable {
2402 fut: Some(fut),
2403 node: self,
2404 parked: false,
2405 }
2406 }
2407
2408 /// Run `body` in a `run_pausable` loop forever, surviving pause/resume cycles.
2409 pub async fn run_pausable_loop(&self, mut body: impl AsyncFnMut()) -> ! {
2410 loop {
2411 let _ = self.run_pausable(body()).await;
2412 }
2413 }
2414
2415 /// Mark this node as busy, requesting pool scale-out if one is configured.
2416 pub fn mark_busy(&self) {
2417 if !self.handle.flag_swap(flag::BUSY, true) {
2418 request_scale();
2419 }
2420 }
2421
2422 /// Mark this node as idle, requesting pool scale-in if one is configured.
2423 pub fn mark_idle(&self) {
2424 if self.handle.flag_swap(flag::BUSY, false) {
2425 request_scale();
2426 }
2427 }
2428
2429 /// Return `true` if the node is currently marked busy.
2430 pub fn is_busy(&self) -> bool {
2431 self.handle.flag(flag::BUSY)
2432 }
2433
2434 /// Return `true` if the node has a running instance.
2435 pub fn is_running(&self) -> bool {
2436 self.handle.flag(flag::RUNNING)
2437 }
2438
2439 /// Return `true` if the node has been manually disabled.
2440 pub fn is_disabled(&self) -> bool {
2441 self.handle.flag(flag::DISABLED)
2442 }
2443
2444 /// Return `true` if the node is held stopped as a dependent of a
2445 /// deactivated node.
2446 pub fn is_collateral(&self) -> bool {
2447 self.handle.flag(flag::COLLATERAL)
2448 }
2449
2450 /// Set whether this node is detached from automatic lifecycle management.
2451 pub fn set_detached(&self, detached: bool) {
2452 self.handle.flag_put(flag::DETACHED, detached);
2453 }
2454
2455 /// Return `true` if the node is detached from automatic lifecycle management.
2456 pub fn is_detached(&self) -> bool {
2457 self.handle.flag(flag::DETACHED)
2458 }
2459
2460 #[cfg(feature = "trace")]
2461 /// Record the executor task id for this node instance.
2462 pub fn set_task_id(&self, id: u32) {
2463 self.handle.task_id.store(id, Ordering::Release);
2464 }
2465
2466 #[cfg(feature = "trace")]
2467 /// Adopt a spawn token's task id (and name, if enabled) for tracing.
2468 pub fn adopt<S>(&self, token: &embassy_executor::SpawnToken<S>) {
2469 let id = token.id();
2470 #[cfg(embassy_supervisor_trace_v2)]
2471 let id = trace::task_key(id);
2472 self.set_task_id(id);
2473 #[cfg(feature = "metadata-names")]
2474 self.stamp_name(token);
2475 }
2476
2477 #[cfg(feature = "trace")]
2478 /// Adopt the current task's id for tracing.
2479 pub async fn adopt_current(&self) {
2480 self.set_task_id(trace::current_task_id().await);
2481 }
2482
2483 #[cfg(feature = "metadata-names")]
2484 /// Set the spawn token's task name to this node's configured name.
2485 pub fn stamp_name<S>(&self, token: &embassy_executor::SpawnToken<S>) {
2486 token.metadata().set_name(self.cfg.name);
2487 }
2488
2489 #[cfg(feature = "trace")]
2490 /// Return the id of the task currently adopted by this node.
2491 pub fn task_id(&self) -> u32 {
2492 self.handle.task_id.load(Ordering::Acquire)
2493 }
2494
2495 #[cfg(feature = "trace")]
2496 /// Return the accumulated execution tick count for this node.
2497 pub fn exec_ticks(&self) -> u32 {
2498 self.handle.exec_ticks.load(Ordering::Relaxed)
2499 }
2500
2501 #[cfg(feature = "trace")]
2502 /// Return the number of poll cycles recorded for this node.
2503 pub fn poll_count(&self) -> u32 {
2504 self.handle.polls.load(Ordering::Relaxed)
2505 }
2506
2507 #[cfg(feature = "trace")]
2508 /// Return the longest single-poll tick count recorded for this node.
2509 pub fn max_poll_ticks(&self) -> u32 {
2510 self.handle.max_poll_ticks.load(Ordering::Relaxed)
2511 }
2512
2513 pub(crate) fn signal_shutdown(&self) {
2514 self.handle.flag_set(flag::SHUTDOWN);
2515 self.handle.shutdown_wake.signal(());
2516 }
2517
2518 pub(crate) fn signal_resume(&self) {
2519 self.handle.resume_wake.signal(());
2520 }
2521
2522 pub(crate) fn set_running(&self, running: bool) {
2523 self.handle.flag_put(flag::RUNNING, running);
2524 #[cfg(feature = "liveness")]
2525 if running {
2526 self.handle.last_beat.store(
2527 embassy_time::Instant::now().as_ticks() as u32,
2528 Ordering::Release,
2529 );
2530 #[cfg(feature = "coupling-observe")]
2531 self.seed_write_mark();
2532 }
2533 #[cfg(feature = "liveness")]
2534 if running {
2535 self.handle.pending_beat.store(false, Ordering::Release);
2536 }
2537 #[cfg(feature = "node-status")]
2538 if running {
2539 self.handle.status.lock(|s| s.set(None));
2540 }
2541 #[cfg(feature = "epochs")]
2542 if running {
2543 self.handle.epoch.fetch_add(1, Ordering::AcqRel);
2544 self.handle.epoch_wake.signal(());
2545 }
2546 }
2547
2548 /// Manually disable or re-enable this node.
2549 pub fn set_disabled(&self, disabled: bool) {
2550 self.handle.flag_put(flag::DISABLED, disabled);
2551 }
2552
2553 pub(crate) fn has_acked_stop(&self) -> bool {
2554 self.handle.flag(flag::DROPPED) && !self.handle.flag(flag::COMPLETED)
2555 }
2556
2557 pub(crate) async fn wait_dropped(&self) {
2558 if self.handle.flag(flag::DROPPED) {
2559 return;
2560 }
2561 self.handle.dropped_wake.wait().await;
2562 }
2563
2564 pub(crate) fn has_dropped(&self) -> bool {
2565 self.handle.flag(flag::DROPPED)
2566 }
2567
2568 pub(crate) fn reset(&self) {
2569 let stale = flag::SHUTDOWN | flag::DROPPED | flag::BUSY | flag::COMPLETED;
2570 #[cfg(feature = "readiness")]
2571 let stale = stale | flag::READY;
2572 // Pending bits belong to the old instance; do not carry them forward.
2573 #[cfg(feature = "fault-inject")]
2574 let stale = stale | flag::PENDING_ACK | flag::PENDING_EXIT;
2575 self.handle.flag_clear(stale);
2576 #[cfg(feature = "readiness")]
2577 self.handle.ready_wake.reset();
2578 self.handle.shutdown_wake.reset();
2579 self.handle.dropped_wake.reset();
2580 }
2581}
2582
2583impl core::fmt::Debug for TaskNode {
2584 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2585 let mut d = f.debug_struct("TaskNode");
2586 d.field("name", &self.cfg.name)
2587 .field("mode", &self.cfg.mode)
2588 .field("running", &self.is_running())
2589 .field("busy", &self.is_busy())
2590 .field("disabled", &self.is_disabled())
2591 .field("collateral", &self.is_collateral())
2592 .field("detached", &self.is_detached());
2593 #[cfg(feature = "epochs")]
2594 d.field("epoch", &self.epoch());
2595 d.finish_non_exhaustive()
2596 }
2597}
2598
2599// ─── Topology ────────────────────────────────────────────────────────────
2600
2601/// Structural facts about a graph, as bits in [`Topology::SHAPE`] — what the
2602/// graph *contains*, decided at `supervisor_graph!` expansion and carried in
2603/// the topology's **type**, so lifecycle code serving an absent structure is
2604pub mod shape {
2605 /// The graph contains at least one `ready` dependency.
2606 pub const READY_DEPS: u32 = 1 << 0;
2607 /// The graph declares at least one named executor slot.
2608 pub const EXEC_SLOTS: u32 = 1 << 1;
2609 /// The graph declares at least one resource slot.
2610 pub const RESOURCES: u32 = 1 << 2;
2611 /// The graph contains at least one `Pause` node or pool member.
2612 pub const PAUSE: u32 = 1 << 3;
2613 /// The graph contains at least one `OnDemand` node or pool member.
2614 pub const ON_DEMAND: u32 = 1 << 4;
2615 /// The graph declares at least one heartbeat (`beat_timeout:` or `beat`).
2616 pub const BEATS: u32 = 1 << 5;
2617 /// The graph declares at least one `observed` signal entry.
2618 pub const OBSERVED: u32 = 1 << 6;
2619 /// The graph contains at least one `bound` dependency.
2620 pub const BOUND_DEPS: u32 = 1 << 7;
2621 /// The graph declares at least one elastic pool.
2622 pub const POOLS: u32 = 1 << 8;
2623 /// The graph declares at least one `divisible` resource.
2624 pub const CLAIMS: u32 = 1 << 9;
2625 /// All shape bits set.
2626 pub const ALL: u32 = u32::MAX;
2627}
2628
2629/// Structural information about a graph, used by [`Supervisor`] to decide
2630/// which lifecycle code paths can be compiled out.
2631pub trait Topology<const N: usize>: 'static {
2632 /// Structural-fact bits (see [`shape`]). An unset bit promises the
2633 /// structure is absent from the whole graph.
2634 const SHAPE: u32;
2635
2636 /// The dependency indices of slot `i` — what node `i` declared it needs
2637 /// spawned first. **Spawn ordering, not runtime coupling**: see the crate
2638 /// docs on what a `deps:` edge does and does not assert.
2639 fn deps_of(&self, i: u8) -> &'static [u8];
2640
2641 /// Return the slot index at topological position `k` (0..N).
2642 fn order_at(&self, k: usize) -> u8;
2643}
2644
2645/// A [`Topology`] whose nodes are topologically sorted by their `deps:` edges.
2646pub struct Ordered<const N: usize, const SHAPE: u32> {
2647 deps: &'static [&'static [u8]; N],
2648 order: [u8; N],
2649}
2650
2651impl<const N: usize, const SHAPE: u32> Ordered<N, SHAPE> {
2652 /// Build a topology from a static array of dependency lists.
2653 pub const fn new(deps: &'static [&'static [u8]; N]) -> Self {
2654 Self {
2655 deps,
2656 order: topo_sort_const(deps),
2657 }
2658 }
2659}
2660
2661impl<const N: usize, const SHAPE: u32> Topology<N> for Ordered<N, SHAPE> {
2662 const SHAPE: u32 = SHAPE;
2663
2664 fn deps_of(&self, i: u8) -> &'static [u8] {
2665 self.deps[i as usize]
2666 }
2667
2668 fn order_at(&self, k: usize) -> u8 {
2669 self.order[k]
2670 }
2671}
2672
2673/// The [`Topology`] of a graph with **no** `deps:` edges anywhere: zero-sized,
2674/// every dep list is empty by type, and the topological order is declaration
2675/// order. The walks a [`Supervisor`] runs over it fold to plain index loops,
2676/// and the dependency cascades (`activate`, `deactivate`, `restart`) collapse
2677/// to their seed sets.
2678pub struct Flat<const SHAPE: u32>;
2679
2680impl<const SHAPE: u32> Flat<SHAPE> {
2681 /// The (zero-sized) flat topology.
2682 pub const fn new() -> Self {
2683 Self
2684 }
2685}
2686
2687impl<const SHAPE: u32> Default for Flat<SHAPE> {
2688 fn default() -> Self {
2689 Self
2690 }
2691}
2692
2693impl<const N: usize, const SHAPE: u32> Topology<N> for Flat<SHAPE> {
2694 const SHAPE: u32 = SHAPE;
2695
2696 fn deps_of(&self, _i: u8) -> &'static [u8] {
2697 &[]
2698 }
2699
2700 fn order_at(&self, k: usize) -> u8 {
2701 k as u8
2702 }
2703}
2704
2705const fn has(shape_bits: u32, bit: u32) -> bool {
2706 shape_bits & bit != 0
2707}
2708
2709/// A static task graph: the nodes, the topology over them, and optional pools.
2710pub struct Graph<const N: usize, T: Topology<N> = Ordered<N, { shape::ALL }>> {
2711 /// The fixed array of node slots; `None` marks a disabled or cfg-gapped slot.
2712 pub nodes: &'static [Option<&'static TaskNode>; N],
2713 /// The topology that defines spawn order and dependency rows.
2714 pub topo: T,
2715 #[cfg(feature = "pool")]
2716 /// The elastic pools declared in the graph.
2717 pub pools: &'static [&'static dyn Pool],
2718 #[cfg(feature = "graph-ref")]
2719 /// A reference used to enumerate the graph at runtime.
2720 pub graph_ref: &'static GraphRef,
2721}
2722
2723impl<const N: usize, T: Topology<N>> Graph<N, T> {
2724 /// Slot index of `node` in this graph (pointer identity — every node is a
2725 /// `&'static`), or `None` if it belongs to another graph. The inverse of
2726 /// indexing [`nodes`](Self::nodes), and the bridge an app-side health view
2727 /// needs to get from a node back to its [`deps_of`](Self::deps_of) row.
2728 pub fn index_of(&self, node: &'static TaskNode) -> Option<u8> {
2729 self.nodes
2730 .iter()
2731 .position(|s| s.is_some_and(|n| core::ptr::eq(n, node)))
2732 .map(|i| i as u8)
2733 }
2734
2735 /// The dependency indices of slot `i` — what node `i` declared it needs
2736 /// spawned first. **Spawn ordering, not runtime coupling**: see the crate
2737 /// docs on what a `deps:` edge does and does not assert.
2738 ///
2739 /// # Panics
2740 /// If `i >= N` (on an [`Ordered`] topology; [`Flat`] has no rows to index).
2741 pub fn deps_of(&self, i: u8) -> &'static [u8] {
2742 self.topo.deps_of(i)
2743 }
2744
2745 /// The slot indices in topological order (dependencies before their
2746 /// dependents; `.rev()` is the teardown order). Declaration order on a
2747 /// [`Flat`] topology.
2748 pub fn order(&self) -> impl DoubleEndedIterator<Item = u8> + ExactSizeIterator + '_ {
2749 (0..N).map(|k| self.topo.order_at(k))
2750 }
2751
2752 /// Call `visit` with the slot index of every node that declares slot `i` as
2753 /// a dependency (direct dependents only). Computed by a forward scan of
2754 /// the dep rows — no reverse-edge table is stored, so this is
2755 /// O(N·E) and meant for control paths and status endpoints, not hot loops.
2756 ///
2757 /// `&mut dyn FnMut` rather than a generic: one instantiation, no
2758 /// monomorphization per call site.
2759 pub fn dependents_of(&self, i: u8, visit: &mut dyn FnMut(u8)) {
2760 for j in 0..N {
2761 if self.topo.deps_of(j as u8).contains(&i) {
2762 visit(j as u8);
2763 }
2764 }
2765 }
2766
2767 /// Iterate the live nodes with their slot indices, skipping `#[cfg]`-ed-out
2768 /// slots — the ergonomic form of `GRAPH.nodes.iter().enumerate()` for a
2769 /// status endpoint that needs the index (to reach `deps`) alongside the node.
2770 pub fn iter_nodes(&self) -> impl Iterator<Item = (u8, &'static TaskNode)> + '_ {
2771 self.nodes
2772 .iter()
2773 .enumerate()
2774 .filter_map(|(i, s)| s.map(|n| (i as u8, n)))
2775 }
2776
2777 /// Every node declaring `signal` in its `writes:`, by slot and node.
2778 /// Matched by address, so it is the *static* that is compared, not the
2779 /// path text.
2780 #[cfg(feature = "coupling")]
2781 pub fn writers_of(&self, signal: &Coupling, visit: &mut dyn FnMut(u8, &'static TaskNode)) {
2782 for (i, node) in self.iter_nodes() {
2783 if node.has_entry(signal, true) {
2784 visit(i, node);
2785 }
2786 }
2787 }
2788
2789 /// Every node declaring `signal` in its `reads:`. The counterpart to
2790 /// [`writers_of`](Self::writers_of); together they answer the structural
2791 /// questions about one signal — who produces it, who consumes it, which
2792 /// pairs are coupled in a loop.
2793 #[cfg(feature = "coupling")]
2794 pub fn readers_of(&self, signal: &Coupling, visit: &mut dyn FnMut(u8, &'static TaskNode)) {
2795 for (i, node) in self.iter_nodes() {
2796 if node.has_entry(signal, false) {
2797 visit(i, node);
2798 }
2799 }
2800 }
2801}
2802
2803// ─── Supervisor ──────────────────────────────────────────────────────────
2804
2805/// Orchestrates a set of managed tasks across spawn / teardown / bring-up.
2806///
2807/// Owned by a single supervisor task. Concurrent access from other tasks goes
2808/// through each [`TaskNode`]'s own atomic state, not the `Supervisor` struct.
2809pub struct Supervisor<const N: usize, T: Topology<N> = Ordered<N, { shape::ALL }>> {
2810 /// Node slots, one per declared node. `None` marks a slot whose node was
2811 /// `#[cfg]`-ed out of the build (feature-gated); every method skips those.
2812 nodes: &'static [Option<&'static TaskNode>; N],
2813 /// The graph's [`Topology`]: the dep rows and topological order every walk
2814 /// iterates (reverse iteration is the teardown order), and the structural
2815 /// [`shape`] bits the gates fold on. Borrowed from the `static` [`Graph`]
2816 /// rather than copied: a `Supervisor` usually lives inside a task future
2817 /// (i.e. in that task's `static` storage), so an inline order array would
2818 /// cost N bytes of RAM per supervisor plus the copy code for no benefit —
2819 /// and for [`Flat`] this reference is the field's entire cost.
2820 topo: &'static T,
2821 /// Elastic pools, so the control interface can co-control a whole pool from
2822 /// any one member (`apply_control` expands the target through
2823 /// [`Pool::members`]) — the same registry `run_pools` drives. Taken from
2824 /// `GRAPH.pools` at construction (empty when no pool is declared).
2825 #[cfg(feature = "pool")]
2826 pools: &'static [&'static dyn Pool],
2827 /// This graph as one `'static`, linked into the binary-wide chain by
2828 /// [`start`](Supervisor::start) so the trace hooks can resolve a task id to
2829 /// one of its nodes.
2830 #[cfg(feature = "trace")]
2831 graph_ref: &'static GraphRef,
2832}
2833
2834async fn await_spawn_slot(node: &'static TaskNode) -> Result<(), NodeFault> {
2835 if let Some(slot) = node.cfg.spawn_slot {
2836 with_timeout(node.slot_timeout(), slot.ready())
2837 .await
2838 .map_err(|_| NodeFault {
2839 node,
2840 kind: FaultKind::ExecutorSlotEmpty,
2841 })?;
2842 }
2843 Ok(())
2844}
2845
2846/// Await every [`ResourceSlot`] a node's `resources:` clause takes from being
2847async fn await_resources(node: &'static TaskNode) -> Result<(), NodeFault> {
2848 for gate in node.cfg.resource_gates {
2849 let wait = async {
2850 loop {
2851 if gate.is_filled() {
2852 break;
2853 }
2854 gate.filled_signal().wait().await;
2855 }
2856 };
2857 with_timeout(node.slot_timeout(), wait)
2858 .await
2859 .map_err(|_| NodeFault {
2860 node,
2861 kind: FaultKind::ResourceMissing,
2862 })?;
2863 }
2864 Ok(())
2865}
2866
2867/// Await every `ready`-marked dep's task-asserted readiness before spawning
2868#[cfg(feature = "readiness")]
2869async fn await_ready_deps(node: &'static TaskNode) -> Result<(), NodeFault> {
2870 for dep in node.ready_deps() {
2871 if with_timeout(node.slot_timeout(), dep.wait_ready())
2872 .await
2873 .is_err()
2874 {
2875 return Err(NodeFault {
2876 node,
2877 kind: FaultKind::ReadyDepTimeout { dep },
2878 });
2879 }
2880 }
2881 Ok(())
2882}
2883#[cfg(not(feature = "readiness"))]
2884async fn await_ready_deps(_node: &'static TaskNode) -> Result<(), NodeFault> {
2885 Ok(())
2886}
2887
2888impl<const N: usize, T: Topology<N>> Supervisor<N, T> {
2889 const NODE_CAP: () = assert!(N <= 256, "supervisor: a graph holds at most 256 nodes");
2890
2891 /// Create a supervisor from a statically-built graph.
2892 pub const fn new(graph: &'static Graph<N, T>) -> Self {
2893 let () = Self::NODE_CAP;
2894 Self {
2895 nodes: graph.nodes,
2896 topo: &graph.topo,
2897 #[cfg(feature = "pool")]
2898 pools: graph.pools,
2899 #[cfg(feature = "trace")]
2900 graph_ref: graph.graph_ref,
2901 }
2902 }
2903
2904 /// Does this graph's shape carry `bit` (see [`shape`])? `T::SHAPE` is a
2905 #[inline(always)]
2906 fn has(bit: u32) -> bool {
2907 has(T::SHAPE, bit)
2908 }
2909
2910 fn order_iter(&self) -> impl DoubleEndedIterator<Item = usize> + '_ {
2911 (0..N).map(|k| self.topo.order_at(k) as usize)
2912 }
2913
2914 /// The pre-spawn gate sequence — executor slot, resource gates, `ready`
2915 /// deps — with each wait compiled out when the graph's shape lacks the
2916 async fn await_gates(node: &'static TaskNode) -> Result<(), NodeFault> {
2917 if Self::has(shape::EXEC_SLOTS) {
2918 await_spawn_slot(node).await?;
2919 }
2920 if Self::has(shape::RESOURCES) {
2921 await_resources(node).await?;
2922 }
2923 if Self::has(shape::READY_DEPS) {
2924 await_ready_deps(node).await?;
2925 }
2926 Ok(())
2927 }
2928
2929 /// Bring the graph from any quiescent state to running, in dependency
2930 /// order — cold boot AND re-entry (a sub-graph supervisor is legitimately
2931 /// `start()`/`teardown()`-cycled per app phase). Idempotent: running nodes
2932 /// are skipped; detached nodes are skipped on re-entry (their instance
2933 /// survived the teardown — the first start still spawns them, the flag is
2934 /// app-set afterwards); a `Pause` instance parked by an earlier teardown is
2935 /// **resumed in place** (never double-spawned; like
2936 /// [`resume_pausable`](Self::resume_pausable) this bypasses the gate waits,
2937 /// since the parked instance retains its resources and its slots are empty
2938 /// by design). `Mode::OnDemand` nodes are skipped — they're brought up at
2939 pub async fn start(&self, spawner: &Spawner) -> Result<(), NodeFault> {
2940 #[cfg(feature = "trace")]
2941 self.graph_ref.register();
2942
2943 #[cfg(feature = "trace-self")]
2944 if let Some(node) = self.graph_ref.self_node() {
2945 node.set_task_id(trace::current_task_id().await);
2946 node.handle.flag_set(flag::RUNNING | flag::DETACHED);
2947 }
2948
2949 self.start_nodes(
2950 spawner,
2951 &mut |_, node| {
2952 !(Self::has(shape::ON_DEMAND) && matches!(node.mode(), Mode::OnDemand))
2953 && !node.is_disabled()
2954 && !node.is_collateral()
2955 && !node.is_running()
2956 && !node.is_detached()
2957 },
2958 false,
2959 )
2960 .await
2961 }
2962
2963 #[cfg(any(feature = "pool", feature = "control"))]
2964 /// Run this node to completion, handling start, driver, and monitoring.
2965 pub async fn run(&self, spawner: &Spawner) -> NodeFault {
2966 if let Err(e) = self.start(spawner).await {
2967 return e;
2968 }
2969 #[cfg(feature = "liveness-monitor")]
2970 match select(self.run_driver(spawner), self.monitor()).await {
2971 Either::First(e) => e,
2972 Either::Second(never) => match never {},
2973 }
2974 #[cfg(not(feature = "liveness-monitor"))]
2975 self.run_driver(spawner).await
2976 }
2977
2978 #[cfg(any(feature = "pool", feature = "control"))]
2979 async fn run_driver(&self, spawner: &Spawner) -> NodeFault {
2980 #[cfg(feature = "bound-deps")]
2981 return match select(self.run_driver_inner(spawner), self.run_binds(spawner)).await {
2982 Either::First(e) | Either::Second(e) => e,
2983 };
2984 #[cfg(not(feature = "bound-deps"))]
2985 self.run_driver_inner(spawner).await
2986 }
2987
2988 #[cfg(all(feature = "bound-deps", any(feature = "pool", feature = "control")))]
2989 async fn run_binds(&self, spawner: &Spawner) -> NodeFault {
2990 loop {
2991 wait_bind().await;
2992 if let Err(e) = self.apply_bind(spawner).await {
2993 return e;
2994 }
2995 }
2996 }
2997
2998 #[cfg(any(feature = "pool", feature = "control"))]
2999 async fn run_driver_inner(&self, spawner: &Spawner) -> NodeFault {
3000 #[cfg(all(feature = "pool", feature = "control"))]
3001 loop {
3002 match select(self.run_pools(spawner), wait_control()).await {
3003 Either::First(e) => return e,
3004 Either::Second(cmd) => {
3005 if let Err(e) = self.apply_control(cmd, spawner).await {
3006 return e;
3007 }
3008 }
3009 }
3010 }
3011 #[cfg(all(feature = "pool", not(feature = "control")))]
3012 return self.run_pools(spawner).await;
3013 #[cfg(all(feature = "control", not(feature = "pool")))]
3014 loop {
3015 let cmd = wait_control().await;
3016 if let Err(e) = self.apply_control(cmd, spawner).await {
3017 return e;
3018 }
3019 }
3020 }
3021
3022 #[cfg(feature = "liveness-monitor")]
3023 /// Monitor node liveness beats and restart nodes that miss deadlines.
3024 pub async fn monitor(&self) -> core::convert::Infallible {
3025 if !Self::has(shape::BEATS)
3026 || !self
3027 .nodes
3028 .iter()
3029 .flatten()
3030 .any(|n| n.beat_timeout().is_some())
3031 {
3032 info!("supervisor: liveness monitor idle (no node declares beat_timeout)");
3033 let never: core::convert::Infallible = core::future::pending().await;
3034 match never {}
3035 }
3036
3037 loop {
3038 let sleep = self
3039 .nodes
3040 .iter()
3041 .flatten()
3042 .filter_map(|n| n.ticks_until_check())
3043 .min()
3044 .unwrap_or(1);
3045 Timer::after(embassy_time::Duration::from_ticks(sleep)).await;
3046
3047 for node in self.nodes.iter().flatten() {
3048 let Some(budget) = node.beat_timeout() else {
3049 continue;
3050 };
3051 if node.is_detached() || !node.is_running() {
3052 node.handle.stale_strikes.store(0, Ordering::Release);
3053 continue;
3054 }
3055
3056 #[cfg(feature = "coupling-observe")]
3057 if Self::has(shape::OBSERVED) && node.poll_observed_writes() {
3058 node.beat();
3059 #[cfg(feature = "readiness")]
3060 if node.ready_on_write() && !node.is_ready() {
3061 node.set_ready();
3062 }
3063 }
3064
3065 if node.is_stale(budget) {
3066 let window = node.beat_window().max(1);
3067 let strikes = node.handle.stale_strikes.load(Ordering::Acquire);
3068 if strikes < window {
3069 let strikes = strikes + 1;
3070 node.handle.stale_strikes.store(strikes, Ordering::Release);
3071 if strikes == window {
3072 warn!(
3073 "supervisor: {} has not beaten in {} ticks",
3074 node.name(),
3075 node.ticks_since_beat()
3076 );
3077 emit_health(HealthEvent {
3078 node,
3079 kind: HealthKind::Stale {
3080 ticks: node.ticks_since_beat(),
3081 },
3082 });
3083 }
3084 }
3085 } else {
3086 let had = node.handle.stale_strikes.swap(0, Ordering::AcqRel);
3087 if had >= node.beat_window().max(1) {
3088 info!("supervisor: {} is beating", node.name());
3089 emit_health(HealthEvent {
3090 node,
3091 kind: HealthKind::Recovered,
3092 });
3093 }
3094 }
3095 }
3096 }
3097 }
3098
3099 /// Start a single node if it is not already running and not detached.
3100 pub async fn start_node(
3101 &self,
3102 node: &'static TaskNode,
3103 spawner: &Spawner,
3104 ) -> Result<(), NodeFault> {
3105 node.reset();
3106 node.handle.flag_clear(flag::COLLATERAL);
3107 if let Some(spawn) = node.cfg.spawn {
3108 match Self::await_gates(node).await {
3109 Ok(()) => {}
3110 Err(_fault) => {
3111 #[cfg(feature = "bound-deps")]
3112 if let FaultKind::ReadyDepTimeout { dep } = _fault.kind
3113 && node.bound_deps().iter().any(|b| core::ptr::eq(*b, dep))
3114 {
3115 info!(
3116 "supervisor: {} parked, bound dep {} not ready",
3117 node.name(),
3118 dep.name()
3119 );
3120 node.handle.flag_set(flag::BOUND_STOPPED);
3121 return Ok(());
3122 }
3123 return Err(_fault);
3124 }
3125 }
3126 let mut result = spawn(*spawner);
3127 if result.is_err() {
3128 // A just-stopped instance's storage frees one executor pass
3129 embassy_futures::yield_now().await;
3130 result = spawn(*spawner);
3131 }
3132 result.map_err(|err| NodeFault {
3133 node,
3134 kind: FaultKind::Spawn(err),
3135 })?;
3136 }
3137 node.set_running(true);
3138 #[cfg(feature = "bound-deps")]
3139 node.handle.flag_clear(flag::BOUND_STOPPED);
3140 info!("supervisor: started {}", node.name());
3141 Ok(())
3142 }
3143
3144 async fn shutdown_and_wait(&self, node: &'static TaskNode) -> Result<(), NodeFault> {
3145 node.signal_shutdown();
3146 self.await_ack(node).await
3147 }
3148
3149 /// The waiting half of [`shutdown_and_wait`](Self::shutdown_and_wait), for
3150 /// a caller that signalled the node earlier. Returns immediately for a node
3151 /// that already acked.
3152 async fn await_ack(&self, node: &'static TaskNode) -> Result<(), NodeFault> {
3153 if let Either::Second(()) =
3154 select(node.wait_dropped(), Timer::after(node.ack_timeout())).await
3155 {
3156 warn!(
3157 "supervisor: task {} did not ack shutdown within {}ms",
3158 node.name(),
3159 node.ack_timeout().as_millis(),
3160 );
3161 #[cfg(feature = "budget")]
3162 if Self::has(shape::CLAIMS) {
3163 node.release_claims();
3164 }
3165 return Err(NodeFault {
3166 node,
3167 kind: FaultKind::ShutdownTimeout,
3168 });
3169 }
3170 node.set_running(false);
3171 Ok(())
3172 }
3173
3174 async fn stop_nodes(
3175 &self,
3176 select: &mut dyn FnMut(usize, &'static TaskNode) -> bool,
3177 keep_going: bool,
3178 ) -> Result<(), NodeFault> {
3179 let mut chosen = [false; N];
3180 for j in self.order_iter().rev() {
3181 let Some(node) = self.nodes[j] else {
3182 continue;
3183 };
3184 if select(j, node) {
3185 chosen[j] = true;
3186 }
3187 }
3188
3189 self.stop_wave(&chosen, keep_going).await
3190 }
3191
3192 /// The down direction: signal each chosen node the moment every *chosen
3193 /// dependent* of it has resolved, parking on the
3194 /// ack event ([`STOP_EVT`]) between rounds. Two guarantees at once, which
3195 /// signalling everything up front could not give together:
3196 ///
3197 /// * a `deps:` dependency keeps serving until its stopping dependents
3198 /// have acked, so a dependent may flush over a link — or drive one
3199 /// last ioctl through a runner it depends on — during its own
3200 /// shutdown;
3201 /// * a node whose shutdown waits on a node it has NO edge to (a
3202 /// producer draining a [`Leased`] signal) is never kept waiting on
3203 /// the wave's own progress: every node without an unresolved chosen
3204 async fn stop_wave(&self, chosen: &[bool; N], keep_going: bool) -> Result<(), NodeFault> {
3205 const UNSIGNALED: u32 = u32::MAX;
3206 let epoch = embassy_time::Instant::now();
3207 let mut resolved = [false; N];
3208 let mut signaled: [u32; N] = [UNSIGNALED; N];
3209 let mut first_err = Ok(());
3210 loop {
3211 for j in self.order_iter().rev() {
3212 if !chosen[j] || resolved[j] || signaled[j] != UNSIGNALED {
3213 continue;
3214 }
3215 let held = (0..N).any(|k| {
3216 chosen[k] && !resolved[k] && self.topo.deps_of(k as u8).contains(&(j as u8))
3217 });
3218 if held {
3219 continue;
3220 }
3221 self.nodes[j]
3222 .expect("a chosen slot is occupied")
3223 .signal_shutdown();
3224 signaled[j] = ((embassy_time::Instant::now() - epoch).as_millis())
3225 .min(UNSIGNALED as u64 - 1) as u32;
3226 }
3227
3228 let mut progress = false;
3229 for j in self.order_iter().rev() {
3230 if !chosen[j] || resolved[j] {
3231 continue;
3232 }
3233 let node = self.nodes[j].expect("a chosen slot is occupied");
3234 if node.has_dropped() {
3235 node.set_running(false);
3236 resolved[j] = true;
3237 progress = true;
3238 }
3239 }
3240 if (0..N).all(|j| !chosen[j] || resolved[j]) {
3241 return first_err;
3242 }
3243 if progress {
3244 continue;
3245 }
3246
3247 let now_ms = (embassy_time::Instant::now() - epoch).as_millis();
3248 let mut deadline: Option<embassy_time::Instant> = None;
3249 for j in self.order_iter().rev() {
3250 if !chosen[j] || resolved[j] || signaled[j] == UNSIGNALED {
3251 continue;
3252 }
3253 let node = self.nodes[j].expect("a chosen slot is occupied");
3254 let due_ms = signaled[j] as u64 + node.ack_timeout().as_millis();
3255 if now_ms < due_ms {
3256 let due = epoch + embassy_time::Duration::from_millis(due_ms);
3257 deadline = Some(deadline.map_or(due, |d| d.min(due)));
3258 continue;
3259 }
3260 warn!(
3261 "supervisor: task {} did not ack shutdown within {}ms",
3262 node.name(),
3263 node.ack_timeout().as_millis(),
3264 );
3265 #[cfg(feature = "budget")]
3266 if Self::has(shape::CLAIMS) {
3267 node.release_claims();
3268 }
3269 let fault = NodeFault {
3270 node,
3271 kind: FaultKind::ShutdownTimeout,
3272 };
3273 if !keep_going {
3274 return Err(fault);
3275 }
3276 if first_err.is_ok() {
3277 first_err = Err(fault);
3278 }
3279 resolved[j] = true;
3280 progress = true;
3281 }
3282 if progress {
3283 continue;
3284 }
3285 let deadline = deadline.expect("an unresolved wave has a signalled node");
3286 let _ = embassy_futures::select::select(STOP_EVT.wait(), Timer::at(deadline)).await;
3287 }
3288 }
3289
3290 async fn start_nodes(
3291 &self,
3292 spawner: &Spawner,
3293 select: &mut dyn FnMut(usize, &'static TaskNode) -> bool,
3294 keep_going: bool,
3295 ) -> Result<(), NodeFault> {
3296 let mut pending = [false; N];
3297 for j in self.order_iter() {
3298 let Some(node) = self.nodes[j] else {
3299 continue;
3300 };
3301 if select(j, node) {
3302 pending[j] = true;
3303 }
3304 }
3305
3306 // When each node's deps resolved — the start of its gate budget, and
3307 const UNARMED: u32 = u32::MAX;
3308 let epoch = embassy_time::Instant::now();
3309 let mut armed: [u32; N] = [UNARMED; N];
3310 let mut first_err = Ok(());
3311 loop {
3312 let mut progress = false;
3313 let mut waiting = false;
3314 let mut respawn_wait = false;
3315 'nodes: for j in self.order_iter() {
3316 if !pending[j] {
3317 continue;
3318 }
3319 let node = self.nodes[j].expect("a pending slot is occupied");
3320 if self
3321 .topo
3322 .deps_of(j as u8)
3323 .iter()
3324 .any(|&d| pending[d as usize])
3325 {
3326 waiting = true;
3327 continue;
3328 }
3329 let budget_start = if armed[j] != UNARMED {
3330 armed[j]
3331 } else {
3332 {
3333 let now = ((embassy_time::Instant::now() - epoch).as_millis())
3334 .min(UNARMED as u64 - 1) as u32;
3335 armed[j] = now;
3336 // Parked `Pause` instance: resume in place, before the
3337 // reset that clears the ack flags this reads — and
3338 // deliberately without the gate waits, since the
3339 // parked instance retains its resources.
3340 if Self::has(shape::PAUSE)
3341 && matches!(node.mode(), Mode::Pause)
3342 && node.has_acked_stop()
3343 {
3344 node.reset();
3345 info!("supervisor: resuming {} in place", node.name());
3346 node.signal_resume();
3347 node.set_running(true);
3348 #[cfg(feature = "bound-deps")]
3349 node.handle.flag_clear(flag::BOUND_STOPPED);
3350 pending[j] = false;
3351 progress = true;
3352 continue;
3353 }
3354 node.reset();
3355 info!("supervisor: spawning {} ({})", node.name(), node.mode());
3356 now
3357 }
3358 };
3359 let Some(spawn) = node.cfg.spawn else {
3360 // A parked node the app spawns itself; only marked.
3361 node.set_running(true);
3362 pending[j] = false;
3363 progress = true;
3364 continue;
3365 };
3366 // The gate sequence — executor slot, resources, ready deps —
3367 // tested without blocking; the first unsatisfied gate defers
3368 // the node to the next round, or faults it once its budget is
3369 // spent.
3370 let overdue = (embassy_time::Instant::now() - epoch).as_millis()
3371 >= budget_start as u64 + node.slot_timeout().as_millis();
3372 let mut unsatisfied = |kind: FaultKind| -> Result<bool, NodeFault> {
3373 if !overdue {
3374 return Ok(true);
3375 }
3376 let fault = NodeFault { node, kind };
3377 if !keep_going {
3378 return Err(fault);
3379 }
3380 warn!("supervisor: {}", fault);
3381 if first_err.is_ok() {
3382 first_err = Err(fault);
3383 }
3384 Ok(false)
3385 };
3386 let blocked = if Self::has(shape::EXEC_SLOTS)
3387 && node.cfg.spawn_slot.is_some_and(|s| s.get().is_none())
3388 {
3389 Some(unsatisfied(FaultKind::ExecutorSlotEmpty)?)
3390 } else if Self::has(shape::RESOURCES)
3391 && node.cfg.resource_gates.iter().any(|g| !g.is_filled())
3392 {
3393 Some(unsatisfied(FaultKind::ResourceMissing)?)
3394 } else {
3395 None
3396 };
3397 #[cfg(feature = "readiness")]
3398 let blocked = match blocked {
3399 Some(b) => Some(b),
3400 None if Self::has(shape::READY_DEPS) => {
3401 match node.ready_deps().iter().find(|d| !d.is_ready()) {
3402 Some(dep) => {
3403 #[cfg(feature = "bound-deps")]
3404 if overdue
3405 && node.bound_deps().iter().any(|b| core::ptr::eq(*b, *dep))
3406 {
3407 info!(
3408 "supervisor: {} parked, bound dep {} not ready",
3409 node.name(),
3410 dep.name()
3411 );
3412 node.handle.flag_set(flag::BOUND_STOPPED);
3413 pending[j] = false;
3414 progress = true;
3415 continue 'nodes;
3416 }
3417 Some(unsatisfied(FaultKind::ReadyDepTimeout { dep })?)
3418 }
3419 None => None,
3420 }
3421 }
3422 None => None,
3423 };
3424 match blocked {
3425 Some(true) => {
3426 waiting = true;
3427 continue 'nodes;
3428 }
3429 Some(false) => {
3430 pending[j] = false;
3431 progress = true;
3432 continue 'nodes;
3433 }
3434 None => {}
3435 }
3436 if let Err(err) = spawn(*spawner) {
3437 // A respawn can catch the previous instance's storage
3438 if unsatisfied(FaultKind::Spawn(err))? {
3439 respawn_wait = true;
3440 waiting = true;
3441 } else {
3442 pending[j] = false;
3443 progress = true;
3444 }
3445 continue;
3446 }
3447 node.set_running(true);
3448 #[cfg(feature = "bound-deps")]
3449 node.handle.flag_clear(flag::BOUND_STOPPED);
3450 pending[j] = false;
3451 progress = true;
3452 }
3453 if !waiting {
3454 return first_err;
3455 }
3456 if progress {
3457 continue;
3458 }
3459 if respawn_wait {
3460 embassy_futures::yield_now().await;
3461 continue;
3462 }
3463 let deadline_ms = self
3464 .order_iter()
3465 .filter(|&j| pending[j] && armed[j] != UNARMED)
3466 .map(|j| {
3467 armed[j] as u64
3468 + self.nodes[j]
3469 .expect("a pending slot is occupied")
3470 .slot_timeout()
3471 .as_millis()
3472 })
3473 .min()
3474 .expect("a waiting wave has an armed node");
3475 let deadline = epoch + embassy_time::Duration::from_millis(deadline_ms);
3476 let _ = embassy_futures::select::select(GATE_EVT.wait(), Timer::at(deadline)).await;
3477 }
3478 }
3479
3480 /// Stop a single node, waiting for its shutdown ack.
3481 pub async fn stop_node(&self, node: &'static TaskNode) -> Result<(), NodeFault> {
3482 if !node.is_running() || node.is_detached() {
3483 return Ok(());
3484 }
3485 self.shutdown_and_wait(node).await?;
3486 info!("supervisor: stopped {}", node.name());
3487 Ok(())
3488 }
3489
3490 /// Stop every **running** node, dependents before their dependencies.
3491 /// Down `OnDemand` nodes are skipped (no instance to ack). Pause-mode nodes
3492 /// ack and park on `wait_resume()`; Terminate/OnDemand nodes exit.
3493 ///
3494 /// The stop runs as a wave: a node is signalled once every dependent
3495 /// stopping with it has acked — a `deps:` dependency keeps serving
3496 /// through its dependents' cleanup — and a node with no such dependents
3497 pub async fn teardown(&self) -> Result<(), NodeFault> {
3498 self.stop_nodes(
3499 &mut |_, node| {
3500 if !node.is_running() || node.is_detached() {
3501 return false;
3502 }
3503 info!("supervisor: tearing down {}", node.name());
3504 true
3505 },
3506 false,
3507 )
3508 .await
3509 }
3510
3511 /// Like [`teardown`](Self::teardown), but do not clear the shutdown flags
3512 /// so a later [`respawn_terminate`](Self::respawn_terminate) can restart.
3513 pub async fn teardown_continue(&self) -> Result<(), NodeFault> {
3514 self.stop_nodes(
3515 &mut |_, node| {
3516 if !node.is_running() || node.is_detached() {
3517 return false;
3518 }
3519 info!("supervisor: tearing down {}", node.name());
3520 true
3521 },
3522 true,
3523 )
3524 .await
3525 }
3526
3527 /// Resume a single parked Pause-mode node.
3528 pub fn resume_node(&self, node: &'static TaskNode) {
3529 if !Self::has(shape::PAUSE)
3530 || !matches!(node.mode(), Mode::Pause)
3531 || node.is_disabled()
3532 || node.is_collateral()
3533 || node.is_detached()
3534 || !node.has_acked_stop()
3535 {
3536 return;
3537 }
3538 node.reset();
3539 info!("supervisor: resuming {}", node.name());
3540 node.signal_resume();
3541 node.set_running(true);
3542 }
3543
3544 /// Signal every **parked** Pause-mode node to resume. Cheap and synchronous —
3545 /// the tasks were parked on `wait_resume()` and pick up immediately. Called
3546 /// separately from `respawn_terminate` so the application can fire resume
3547 /// independently of the respawn step. Disabled (manually-paused) nodes are
3548 /// skipped so a manual pause sticks, detached (self-managed) Pause nodes are
3549 /// left parked, and — as in [`resume_node`](Self::resume_node) — a node
3550 /// without a parked instance (`has_acked_stop`) is skipped: signaling it
3551 /// would latch `resume_wake` with no waiter, and the node's *next* park
3552 pub fn resume_pausable(&self) {
3553 if !Self::has(shape::PAUSE) {
3554 return;
3555 }
3556 for j in self.order_iter() {
3557 let Some(node) = self.nodes[j] else {
3558 continue;
3559 };
3560 if matches!(node.mode(), Mode::Pause)
3561 && !node.is_disabled()
3562 && !node.is_collateral()
3563 && !node.is_detached()
3564 && node.has_acked_stop()
3565 {
3566 node.reset();
3567 info!("supervisor: resuming {}", node.name());
3568 node.signal_resume();
3569 node.set_running(true);
3570 }
3571 }
3572 }
3573
3574 /// Restart every terminated Terminate-mode node that is not running,
3575 /// disabled, or detached.
3576 pub async fn respawn_terminate(&self, spawner: &Spawner) -> Result<(), NodeFault> {
3577 self.start_nodes(
3578 spawner,
3579 &mut |_, node| {
3580 matches!(node.mode(), Mode::Terminate)
3581 && !node.is_disabled()
3582 && !node.is_collateral()
3583 && !node.is_detached()
3584 && !node.is_running()
3585 },
3586 false,
3587 )
3588 .await
3589 }
3590}
3591
3592#[cfg(any(feature = "control", feature = "pool"))]
3593impl<const N: usize, T: Topology<N>> Supervisor<N, T> {
3594 fn index_of(&self, node: &'static TaskNode) -> Option<usize> {
3595 self.nodes
3596 .iter()
3597 .position(|n| n.is_some_and(|x| core::ptr::eq(x, node)))
3598 }
3599
3600 /// Whether every dependency of `node` is currently running, resolved through
3601 /// the graph's index table. The pool driver checks this before growing a
3602 #[cfg(feature = "pool")]
3603 pub(crate) fn deps_running(&self, node: &'static TaskNode) -> bool {
3604 match self.index_of(node) {
3605 Some(i) => self
3606 .topo
3607 .deps_of(i as u8)
3608 .iter()
3609 .all(|&di| self.nodes[di as usize].is_some_and(|n| n.is_running())),
3610 None => false,
3611 }
3612 }
3613}
3614
3615#[cfg(feature = "control")]
3616impl<const N: usize, T: Topology<N>> Supervisor<N, T> {
3617 /// Seed a membership set with `target` plus — if `target` belongs to an
3618 /// elastic pool — every member of that pool, so control is applied to the
3619 /// whole pool atomically. Pool membership is read from `GRAPH.pools`; with no
3620 /// pools (the `pool` feature off, or none declared) this is just `{target}`.
3621 fn seed(&self, target: &'static TaskNode, set: &mut [bool; N]) {
3622 if let Some(i) = self.index_of(target) {
3623 set[i] = true;
3624 }
3625 #[cfg(feature = "pool")]
3626 if Self::has(shape::POOLS) {
3627 for pool in self.pools {
3628 let members = pool.members();
3629 if members.iter().any(|m| core::ptr::eq(*m, target)) {
3630 for m in members {
3631 if let Some(i) = self.index_of(m) {
3632 set[i] = true;
3633 }
3634 }
3635 }
3636 }
3637 }
3638 }
3639
3640 fn collect_dependents(&self, set: &mut [bool; N]) {
3641 for j in self.order_iter() {
3642 if set[j] {
3643 continue;
3644 }
3645 let Some(node) = self.nodes[j] else {
3646 continue;
3647 };
3648 if node.is_detached() {
3649 continue;
3650 }
3651 if self
3652 .topo
3653 .deps_of(j as u8)
3654 .iter()
3655 .any(|&di| set[di as usize])
3656 {
3657 set[j] = true;
3658 }
3659 }
3660 }
3661
3662 /// Apply a control command to the requested node.
3663 pub async fn apply_control(
3664 &self,
3665 cmd: ControlCommand,
3666 spawner: &Spawner,
3667 ) -> Result<(), NodeFault> {
3668 match cmd.op {
3669 ControlOp::Deactivate => self.deactivate(cmd.node).await,
3670 ControlOp::Activate => {
3671 self.activate(cmd.node, spawner).await;
3672 Ok(())
3673 }
3674 #[cfg(feature = "restart")]
3675 ControlOp::Restart => match self.restart(cmd.node, spawner).await {
3676 Ok(()) => Ok(()),
3677 Err(e) if matches!(e.kind, FaultKind::ShutdownTimeout) => Err(e),
3678 Err(e) => {
3679 let node = e.node;
3680 warn!(
3681 "supervisor: {} did not come back after restart",
3682 node.name()
3683 );
3684 Ok(())
3685 }
3686 },
3687 #[allow(unreachable_patterns)]
3688 _ => Ok(()),
3689 }
3690 }
3691
3692 /// Deactivate the target node and all of its dependents.
3693 pub async fn deactivate(&self, target: &'static TaskNode) -> Result<(), NodeFault> {
3694 let mut seed = [false; N];
3695 self.seed(target, &mut seed);
3696 let mut set = seed;
3697 self.collect_dependents(&mut set);
3698
3699 // Down in reverse topo order (dependents before their deps).
3700 self.stop_nodes(
3701 &mut |j, node| {
3702 if !set[j] {
3703 return false;
3704 }
3705 // A detached node is self-managed — never control-stop it. The growth
3706 // loop keeps detached *dependents* out of the set; this also covers a
3707 // detached node that was seeded directly (or a detached pool member).
3708 // Without it a detached one-shot that already exited (stale
3709 // `is_running`, no ack path) would be signalled a shutdown it can never
3710 // acknowledge, failing here with a spurious missed-ack fault.
3711 if node.is_detached() {
3712 return false;
3713 }
3714 if seed[j] {
3715 node.set_disabled(true);
3716 } else {
3717 node.handle.flag_set(flag::COLLATERAL);
3718 }
3719 if !node.is_running() {
3720 return false;
3721 }
3722 info!("supervisor: control-stop {}", node.name());
3723 true
3724 },
3725 false,
3726 )
3727 .await
3728 }
3729
3730 /// Bring `target` (and its pool, and every transitive dependency) up, in
3731 /// topological order so each dependency starts before its dependent — the
3732 /// cascading "turn this subsystem on" verb, and the entry half of the
3733 /// subordinate sub-graph pattern's one-graph variant: `activate` on a
3734 pub async fn activate(&self, target: &'static TaskNode, spawner: &Spawner) {
3735 let mut set = [false; N];
3736 self.seed(target, &mut set);
3737
3738 // Grow the set to include transitive deps. Walk dependents-first
3739 // (reverse topo); when a set member is seen, pull in its direct deps.
3740 // A detached member's `deps:` are start-ordering only (the node is
3741 for j in self.order_iter().rev() {
3742 if set[j] && !self.nodes[j].is_some_and(|n| n.is_detached()) {
3743 for &di in self.topo.deps_of(j as u8) {
3744 set[di as usize] = true;
3745 }
3746 }
3747 }
3748 for j in self.order_iter() {
3749 if set[j]
3750 && let Some(node) = self.nodes[j]
3751 && !node.is_detached()
3752 {
3753 node.set_disabled(false);
3754 }
3755 }
3756 let mut held = [false; N]; // disabled, or depends on a disabled node
3757 let mut revive = [false; N];
3758 for j in self.order_iter() {
3759 let Some(node) = self.nodes[j] else {
3760 continue;
3761 };
3762 if node.is_detached() {
3763 continue;
3764 }
3765 let blocked = self
3766 .topo
3767 .deps_of(j as u8)
3768 .iter()
3769 .any(|&di| held[di as usize]);
3770 held[j] = blocked || node.is_disabled();
3771 if !blocked && node.is_collateral() {
3772 node.handle.flag_clear(flag::COLLATERAL);
3773 revive[j] = !node.is_disabled();
3774 }
3775 }
3776
3777 let _ = self
3778 .start_nodes(
3779 spawner,
3780 &mut |j, node| {
3781 if !(set[j] || revive[j]) || node.is_detached() {
3782 return false;
3783 }
3784 !node.is_running()
3785 && !node.is_collateral()
3786 && !(Self::has(shape::ON_DEMAND) && matches!(node.mode(), Mode::OnDemand))
3787 },
3788 true,
3789 )
3790 .await;
3791 #[cfg(feature = "pool")]
3792 if Self::has(shape::POOLS) {
3793 request_scale();
3794 }
3795 }
3796}
3797
3798#[cfg(feature = "bound-deps")]
3799fn is_serving(node: &TaskNode) -> bool {
3800 node.is_running() && node.is_ready()
3801}
3802
3803#[cfg(feature = "bound-deps")]
3804impl<const N: usize, T: Topology<N>> Supervisor<N, T> {
3805 /// React to bound-dependency readiness changes by stopping or resuming nodes.
3806 pub async fn apply_bind(&self, spawner: &Spawner) -> Result<(), NodeFault> {
3807 if !Self::has(shape::BOUND_DEPS) {
3808 return Ok(());
3809 }
3810 let mut stopping = [false; N];
3811 for j in self.order_iter() {
3812 let Some(node) = self.nodes[j] else {
3813 continue;
3814 };
3815 if node.is_detached() || !node.is_running() {
3816 continue;
3817 }
3818 let mut down = false;
3819 for d in node.bound_deps() {
3820 if !is_serving(d) {
3821 down = true;
3822 break;
3823 }
3824 for (k, slot) in self.nodes.iter().enumerate() {
3825 if stopping[k] && slot.is_some_and(|x| core::ptr::eq(x, *d)) {
3826 down = true;
3827 break;
3828 }
3829 }
3830 if down {
3831 break;
3832 }
3833 }
3834 stopping[j] = down;
3835 }
3836
3837 self.stop_nodes(
3838 &mut |j, node| {
3839 if !stopping[j] {
3840 return false;
3841 }
3842 info!(
3843 "supervisor: bound-stop {} (a bound provider withdrew readiness)",
3844 node.name()
3845 );
3846 node.handle.flag_set(flag::BOUND_STOPPED);
3847 true
3848 },
3849 false,
3850 )
3851 .await?;
3852
3853 for j in self.order_iter() {
3854 let Some(node) = self.nodes[j] else {
3855 continue;
3856 };
3857 if !node.handle.flag(flag::BOUND_STOPPED) {
3858 continue;
3859 }
3860 if node.is_disabled() || node.is_collateral() || node.is_detached() || node.is_running()
3861 {
3862 continue;
3863 }
3864 if !node.bound_deps().iter().all(|d| is_serving(d)) {
3865 continue;
3866 }
3867 match node.mode() {
3868 Mode::Terminate => {
3869 info!("supervisor: bound-restart {}", node.name());
3870 if self.start_node(node, spawner).await.is_err() {
3871 warn!("supervisor: {} could not be bound-restarted", node.name());
3872 }
3873 }
3874 Mode::Pause if Self::has(shape::PAUSE) => {
3875 info!("supervisor: bound-resume {}", node.name());
3876 node.reset();
3877 node.signal_resume();
3878 node.set_running(true);
3879 node.handle.flag_clear(flag::BOUND_STOPPED);
3880 }
3881 Mode::Pause => {}
3882 Mode::OnDemand => {
3883 node.handle.flag_clear(flag::BOUND_STOPPED);
3884 #[cfg(feature = "pool")]
3885 if Self::has(shape::POOLS) {
3886 request_scale();
3887 }
3888 }
3889 }
3890 }
3891 Ok(())
3892 }
3893}
3894
3895#[cfg(feature = "restart")]
3896impl<const N: usize, T: Topology<N>> Supervisor<N, T> {
3897 /// Restart the target node and all of its dependents.
3898 pub async fn restart(
3899 &self,
3900 target: &'static TaskNode,
3901 spawner: &Spawner,
3902 ) -> Result<(), NodeFault> {
3903 let mut set = [false; N];
3904 self.seed(target, &mut set);
3905 self.collect_dependents(&mut set);
3906
3907 info!(
3908 "supervisor: restarting {} and its dependents",
3909 target.name()
3910 );
3911
3912 // Down, dependents first. No `disabled` latch: this is a cycle, not a
3913 // stop, and a concurrent observer must never see the subtree as
3914 // deliberately disabled.
3915 self.stop_nodes(
3916 &mut |j, node| {
3917 if !set[j] || node.is_detached() || !node.is_running() {
3918 return false;
3919 }
3920 info!("supervisor: stopping {}", node.name());
3921 true
3922 },
3923 false,
3924 )
3925 .await?;
3926
3927 // Up, dependencies first, each through the full gate sequence — as a
3928 // wave, aborting on the first fault.
3929 let r = self
3930 .start_nodes(
3931 spawner,
3932 &mut |j, node| {
3933 set[j]
3934 && !node.is_detached()
3935 && !node.is_disabled()
3936 && !node.is_collateral()
3937 && !node.is_running()
3938 && !(Self::has(shape::ON_DEMAND) && matches!(node.mode(), Mode::OnDemand))
3939 },
3940 false,
3941 )
3942 .await;
3943 #[cfg(feature = "pool")]
3944 if Self::has(shape::POOLS) {
3945 request_scale();
3946 }
3947
3948 r
3949 }
3950}
3951
3952// ─── Topological sort (Kahn's algorithm, const) ───────────────────────────
3953
3954#[doc(hidden)]
3955#[must_use]
3956pub const fn topo_sort_const<const N: usize>(deps: &[&'static [u8]; N]) -> [u8; N] {
3957 assert!(
3958 N <= 256,
3959 "supervisor graph exceeds 256 node slots (indices are u8)"
3960 );
3961 // in_degree[i] = number of deps of node i not yet resolved.
3962 let mut in_degree = [0u8; N];
3963 let mut i = 0;
3964 while i < N {
3965 in_degree[i] = deps[i].len() as u8;
3966 i += 1;
3967 }
3968
3969 // Queue (fixed array, head/tail indices) seeded with the dependency-free nodes.
3970 let mut queue = [0u8; N];
3971 let mut tail = 0;
3972 i = 0;
3973 while i < N {
3974 if in_degree[i] == 0 {
3975 queue[tail] = i as u8;
3976 tail += 1;
3977 }
3978 i += 1;
3979 }
3980
3981 let mut order = [0u8; N];
3982 let mut produced = 0;
3983 let mut head = 0;
3984 while head < tail {
3985 let node = queue[head] as usize;
3986 head += 1;
3987 order[produced] = node as u8;
3988 produced += 1;
3989
3990 // Decrement the in-degree of every node that depends on `node`.
3991 let mut j = 0;
3992 while j < N {
3993 if in_degree[j] != 0 {
3994 let mut depends = false;
3995 let mut k = 0;
3996 while k < deps[j].len() {
3997 if deps[j][k] as usize == node {
3998 depends = true;
3999 }
4000 k += 1;
4001 }
4002 if depends {
4003 in_degree[j] -= 1;
4004 if in_degree[j] == 0 {
4005 queue[tail] = j as u8;
4006 tail += 1;
4007 }
4008 }
4009 }
4010 j += 1;
4011 }
4012 }
4013
4014 // A cycle leaves some nodes unproduced. During const eval this panic is a
4015 // compile error, so cyclic graphs are rejected at build time. `core::panic!`
4016 // (not the crate's defmt-shimmed `panic!`) keeps this const-evaluable.
4017 if produced != N {
4018 core::panic!("supervisor_graph!: dependency cycle");
4019 }
4020 order
4021}
4022
4023#[cfg(feature = "pool")]
4024mod pool;
4025#[cfg(feature = "pool")]
4026pub use pool::*;
4027
4028#[cfg(feature = "budget")]
4029mod budget;
4030#[cfg(feature = "budget")]
4031pub use budget::*;
4032
4033#[cfg(feature = "veto")]
4034mod veto;
4035#[cfg(feature = "veto")]
4036pub use veto::*;
4037
4038#[cfg(feature = "coupling")]
4039mod stamped;
4040#[cfg(feature = "coupling")]
4041pub use stamped::Stamped;
4042
4043#[cfg(feature = "dataflow")]
4044mod dataflow;
4045#[cfg(feature = "dataflow")]
4046pub use dataflow::*;
4047
4048#[cfg(feature = "data-deps")]
4049mod data_deps;
4050#[cfg(feature = "data-deps")]
4051pub use data_deps::*;
4052
4053#[cfg(feature = "graph-ref")]
4054mod graph_ref;
4055#[cfg(feature = "graph-ref")]
4056pub use graph_ref::*;
4057
4058#[cfg(feature = "trace")]
4059/// Runtime tracing hooks and task introspection helpers.
4060pub mod trace;
4061
4062#[cfg(feature = "trace")]
4063#[doc(hidden)]
4064pub use embassy_executor as __executor;
4065
4066/// The executor's trace hooks, forwarding to the [`trace`] recorders.
4067///
4068/// `supervisor_graph!` expands this once, at the unnamed graph's declaration
4069/// site, under `trace-hooks`; it lives here rather than in the proc-macro so
4070/// the hook *bodies* follow this crate's build, not the macro crate's. The
4071/// executor's hook API is an implementation detail that has already changed
4072/// shape once: embassy-executor 0.10 links seven `_embassy_trace_*` symbols
4073/// taking `u32` ids, while its git main (the next release) takes a
4074/// `raw::trace::Trace` impl registered with `trace_impl!` and passes
4075/// `ExecutorId`/`TaskRef`. `--cfg embassy_supervisor_trace_v2` (RUSTFLAGS)
4076/// selects the latter; the recorders keep their `u32` keys either way, ids
4077/// narrowing through [`trace::task_key`] / [`trace::executor_key`] at the hook.
4078#[cfg(all(feature = "trace", not(embassy_supervisor_trace_v2)))]
4079#[doc(hidden)]
4080#[macro_export]
4081macro_rules! __sv_trace_hooks {
4082 () => {
4083 #[unsafe(no_mangle)]
4084 fn _embassy_trace_poll_start(executor_id: u32) {
4085 $crate::trace::on_poll_start(executor_id);
4086 }
4087 #[unsafe(no_mangle)]
4088 fn _embassy_trace_task_new(_executor_id: u32, _task_id: u32) {}
4089 #[unsafe(no_mangle)]
4090 fn _embassy_trace_task_end(executor_id: u32, task_id: u32) {
4091 $crate::trace::on_task_end(executor_id, task_id);
4092 }
4093 #[unsafe(no_mangle)]
4094 fn _embassy_trace_task_exec_begin(executor_id: u32, task_id: u32) {
4095 $crate::trace::on_task_exec_begin(executor_id, task_id);
4096 }
4097 #[unsafe(no_mangle)]
4098 fn _embassy_trace_task_exec_end(executor_id: u32, task_id: u32) {
4099 $crate::trace::on_task_exec_end(executor_id, task_id);
4100 }
4101 #[unsafe(no_mangle)]
4102 fn _embassy_trace_task_ready_begin(_executor_id: u32, _task_id: u32) {}
4103 #[unsafe(no_mangle)]
4104 fn _embassy_trace_executor_idle(executor_id: u32) {
4105 $crate::trace::on_executor_idle(executor_id);
4106 }
4107 };
4108}
4109
4110#[cfg(all(feature = "trace", embassy_supervisor_trace_v2))]
4111#[doc(hidden)]
4112#[macro_export]
4113macro_rules! __sv_trace_hooks {
4114 () => {
4115 const _: () = {
4116 // `trace_impl!` spells its shims' parameter types unqualified, so
4117 // both must be in scope where it expands.
4118 use $crate::__executor::ExecutorId;
4119 use $crate::__executor::raw::TaskRef;
4120
4121 struct __SvTraceHooks;
4122 impl $crate::__executor::raw::trace::Trace for __SvTraceHooks {
4123 fn poll_start(executor: ExecutorId) {
4124 $crate::trace::on_poll_start($crate::trace::executor_key(executor));
4125 }
4126 fn task_new(_executor: ExecutorId, _task: TaskRef) {}
4127 fn task_end(executor: ExecutorId, task: TaskRef) {
4128 $crate::trace::on_task_end(
4129 $crate::trace::executor_key(executor),
4130 $crate::trace::task_key(task.id()),
4131 );
4132 }
4133 fn task_exec_begin(executor: ExecutorId, task: TaskRef) {
4134 $crate::trace::on_task_exec_begin(
4135 $crate::trace::executor_key(executor),
4136 $crate::trace::task_key(task.id()),
4137 );
4138 }
4139 fn task_exec_end(executor: ExecutorId, task: TaskRef) {
4140 $crate::trace::on_task_exec_end(
4141 $crate::trace::executor_key(executor),
4142 $crate::trace::task_key(task.id()),
4143 );
4144 }
4145 fn task_ready_begin(_executor: ExecutorId, _task: TaskRef) {}
4146 fn executor_idle(executor: ExecutorId) {
4147 $crate::trace::on_executor_idle($crate::trace::executor_key(executor));
4148 }
4149 fn idle() {}
4150 fn task_name_set(_task: TaskRef, _name: &'static str) {}
4151 fn task_priority_set(_task: TaskRef, _priority: u8) {}
4152 fn task_deadline_set(_task: TaskRef, _deadline: u64) {}
4153 }
4154 $crate::__executor::trace_impl!(__SvTraceHooks);
4155 };
4156 };
4157}
4158
4159#[cfg(feature = "macros")]
4160pub use embassy_supervisor_macros::supervisor_fragment;
4161#[cfg(feature = "macros")]
4162pub use embassy_supervisor_macros::supervisor_graph;
4163
4164#[cfg(feature = "macros")]
4165#[macro_export]
4166/// Compose a graph out of one or more `supervisor_fragment!` declarations.
4167///
4168/// The fragments are spliced into the final graph at the compose site. This
4169/// macro is re-exported by `embassy-supervisor` when the `macros` feature is
4170/// enabled.
4171macro_rules! compose_graph {
4172 (name: $n:ident, fragments: [$f:path $(, $r:path)* $(,)?], graph: {$($g:tt)*}) => {
4173 $f! { @emit $crate::compose_graph, [$($r),*], {name: $n;}, {$($g)*} }
4174 };
4175 (fragments: [$f:path $(, $r:path)* $(,)?], graph: {$($g:tt)*}) => {
4176 $f! { @emit $crate::compose_graph, [$($r),*], {}, {$($g)*} }
4177 };
4178 (@next [], {$($acc:tt)*}, {$($g:tt)*}) => {
4179 $crate::supervisor_graph! { $($acc)* $($g)* }
4180 };
4181 (@next [$f:path $(, $r:path)*], {$($acc:tt)*}, $g:tt) => {
4182 $f! { @emit $crate::compose_graph, [$($r),*], {$($acc)*}, $g }
4183 };
4184}
4185
4186#[doc(hidden)]
4187pub mod _export {
4188 pub use embassy_sync::blocking_mutex::Mutex as BlockingMutex;
4189 pub use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
4190 pub use embassy_sync::signal::Signal;
4191 pub use embassy_time::Duration;
4192}