Skip to main content

embassy_supervisor/
budget.rs

1//! A divisible resource: one budget of units, split among the nodes that
2//! declare `resources: [NAME: divisible]`, with every holder's share released
3//! by the supervisor when that holder stops.
4
5use core::cell::Cell;
6use core::task::{Poll, Waker};
7
8use embassy_sync::blocking_mutex::Mutex;
9use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
10use embassy_sync::signal::Signal;
11use embassy_sync::waitqueue::AtomicWaker;
12use embassy_time::{Duration, Instant};
13use portable_atomic::{AtomicU32, Ordering};
14
15use crate::ResourceGate;
16
17/// A budget of `u32` units divided among `N` claimant slots.
18///
19/// Declared (as a `pub static`) by [`supervisor_graph!`](crate::supervisor_graph)
20/// for each `divisible` resource name, sized to the nodes and pool members that
21/// declare it: one slot each, numbered in declaration order. The protocol:
22///
23/// 1. Something [`provide`](Self::provide)s the capacity — `main` for a fixed
24///    budget, or an allocator node that also names the slot in `provides:` so
25///    the budget empties when it stops. A holder whose budget is still
26///    unprovided at its gate deadline faults with
27///    [`FaultKind::ResourceMissing`](crate::FaultKind::ResourceMissing), like
28///    any other slot.
29/// 2. Each holder's shell receives a [`Claimant`] bound to its slot and states
30///    what it [`want`](Claimant::want)s. The allocator divides the capacity
31///    over the wants with a [`BudgetPolicy`] ([`rebalance`](Self::rebalance)),
32///    and each holder reads its [`grant`](Claimant::grant), or parks on
33///    [`wait_grant_change`](Claimant::wait_grant_change) until it moves.
34/// 3. When a holder stops — cleanly, or by missing its shutdown ack — the
35///    supervisor [`release`](Self::release)s its slot, so a dead session never
36///    strands its share. A parked (`Pause`) holder keeps its claim.
37///
38/// The budget never chooses a division itself: what "fair" means (equal,
39/// proportional, ramped) is the policy's, and *when* to re-divide is the
40/// allocator's — usually on [`wait_change`](Self::wait_change), which fires on
41/// every want, release and capacity change. Costs `4 + 8N` bytes of atomics,
42/// two `Signal`s and `N` single-waker slots per budget: `28 + 16N` bytes on a
43/// 32-bit target.
44pub struct Budget<const N: usize> {
45    /// `0` is "not provided": the gate reads empty.
46    capacity: AtomicU32,
47    wants: [AtomicU32; N],
48    grants: [AtomicU32; N],
49    /// The [`ResourceGate`] wake for the supervisor's pre-spawn wait.
50    filled: Signal<CriticalSectionRawMutex, ()>,
51    /// Holders parked in `wait_grant_change`, at most one per slot.
52    claimants: [AtomicWaker; N],
53    /// The allocator's wake, single waiter: anything that should trigger a
54    /// re-division.
55    watch: Signal<CriticalSectionRawMutex, ()>,
56}
57
58impl<const N: usize> Default for Budget<N> {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64impl<const N: usize> Budget<N> {
65    /// An unprovided budget (`const` — it lives in a `static` the macro emits).
66    pub const fn new() -> Self {
67        const { assert!(N > 0, "a Budget needs at least one claimant slot") };
68        Self {
69            capacity: AtomicU32::new(0),
70            wants: [const { AtomicU32::new(0) }; N],
71            grants: [const { AtomicU32::new(0) }; N],
72            filled: Signal::new(),
73            claimants: [const { AtomicWaker::new() }; N],
74            watch: Signal::new(),
75        }
76    }
77
78    /// Set the capacity and wake both the supervisor's gate wait and the
79    /// allocator.
80    pub fn provide(&self, capacity: u32) {
81        self.capacity.store(capacity, Ordering::Release);
82        if capacity > 0 {
83            self.filled.signal(());
84            crate::__sv_gate_event();
85        }
86        self.watch.signal(());
87    }
88
89    /// The number of claimant slots: what the graph sized the budget to.
90    pub const fn slots(&self) -> usize {
91        N
92    }
93
94    /// The provided capacity, `0` when unprovided.
95    pub fn capacity(&self) -> u32 {
96        self.capacity.load(Ordering::Acquire)
97    }
98
99    /// State slot `slot`'s demand and wake the allocator.
100    pub fn want(&self, slot: u8, units: u32) {
101        self.wants[usize::from(slot)].store(units, Ordering::Release);
102        self.watch.signal(());
103    }
104
105    /// Drop slot `slot`'s demand and grant, and wake the allocator: what the
106    /// supervisor does when the slot's holder stops.
107    pub fn release(&self, slot: u8) {
108        let slot = usize::from(slot);
109        self.wants[slot].store(0, Ordering::Release);
110        self.grants[slot].store(0, Ordering::Release);
111        self.watch.signal(());
112    }
113
114    /// Slot `slot`'s current grant.
115    pub fn grant(&self, slot: u8) -> u32 {
116        self.grants[usize::from(slot)].load(Ordering::Acquire)
117    }
118
119    /// Slot `slot`'s stated demand.
120    pub fn want_of(&self, slot: u8) -> u32 {
121        self.wants[usize::from(slot)].load(Ordering::Acquire)
122    }
123
124    /// The sum of every slot's grant.
125    pub fn total_granted(&self) -> u32 {
126        self.grants
127            .iter()
128            .fold(0u32, |acc, g| acc.saturating_add(g.load(Ordering::Acquire)))
129    }
130
131    /// Re-divide the capacity over the current wants with `policy`, publish
132    /// the new grants, and wake every holder whose grant moved.
133    pub fn rebalance(&self, policy: &impl BudgetPolicy, now: Instant) -> Option<Instant> {
134        let capacity = self.capacity();
135        let wants: [u32; N] = core::array::from_fn(|i| self.wants[i].load(Ordering::Acquire));
136        let mut grants: [u32; N] = core::array::from_fn(|i| self.grants[i].load(Ordering::Acquire));
137        let next = policy.divide(capacity, &wants, &mut grants, now);
138        for (i, g) in grants.iter().enumerate() {
139            if self.grants[i].swap(*g, Ordering::AcqRel) != *g {
140                self.claimants[i].wake();
141            }
142        }
143        let stale = self.capacity() != capacity
144            || wants
145                .iter()
146                .zip(&self.wants)
147                .any(|(seen, cur)| cur.load(Ordering::Acquire) != *seen);
148        if stale {
149            self.watch.signal(());
150        }
151        next
152    }
153
154    /// The allocator's wait: resolves after any want, release or capacity
155    /// change since the last wait (latching, single waiter).
156    pub async fn wait_change(&self) {
157        self.watch.wait().await;
158    }
159
160    /// The handle a holder of `slot` claims through. Emitted by the macro
161    /// into the holder's task shell; hand-built nodes may call it directly.
162    pub fn claimant(&'static self, slot: u8) -> Claimant {
163        Claimant { budget: self, slot }
164    }
165
166    fn wake_claimants(&self) {
167        for w in &self.claimants {
168            w.wake();
169        }
170    }
171}
172
173impl<const N: usize> ResourceGate for Budget<N> {
174    fn is_filled(&self) -> bool {
175        self.capacity() > 0
176    }
177
178    fn filled_signal(&self) -> &Signal<CriticalSectionRawMutex, ()> {
179        &self.filled
180    }
181
182    /// Empty the budget: no capacity, no grants. Holders are woken so a loop
183    /// parked on its grant sees the zero.
184    fn clear(&self) {
185        self.capacity.store(0, Ordering::Release);
186        for g in &self.grants {
187            g.store(0, Ordering::Release);
188        }
189        self.filled.reset();
190        self.wake_claimants();
191        self.watch.signal(());
192    }
193}
194
195/// The object-safe view of a [`Budget`] a [`Claimant`] and a node's claims
196/// table go through, so neither names `N`.
197pub trait Divisible: Sync {
198    /// State `slot`'s demand.
199    fn want(&self, slot: u8, units: u32);
200    /// Drop `slot`'s demand and grant.
201    fn release(&self, slot: u8);
202    /// `slot`'s current grant.
203    fn grant(&self, slot: u8) -> u32;
204    /// Park `waker` until the next rebalance that moves `slot`'s grant.
205    fn register(&self, slot: u8, waker: &Waker);
206}
207
208impl<const N: usize> Divisible for Budget<N> {
209    fn want(&self, slot: u8, units: u32) {
210        Budget::want(self, slot, units);
211    }
212
213    fn release(&self, slot: u8) {
214        Budget::release(self, slot);
215    }
216
217    fn grant(&self, slot: u8) -> u32 {
218        Budget::grant(self, slot)
219    }
220
221    fn register(&self, slot: u8, waker: &Waker) {
222        self.claimants[usize::from(slot)].register(waker);
223    }
224}
225
226/// A holder's handle on one slot of a [`Budget`]: what a `divisible` entry
227/// hands the worker. `Copy`, so a worker may keep one per loop it runs.
228#[derive(Clone, Copy)]
229pub struct Claimant {
230    budget: &'static dyn Divisible,
231    slot: u8,
232}
233
234impl Claimant {
235    /// State this slot's demand.
236    pub fn want(&self, units: u32) {
237        self.budget.want(self.slot, units);
238    }
239
240    /// Give the share back early. The supervisor does this when the holder
241    /// stops; a holder that is done with the budget mid-run does it itself.
242    pub fn release(&self) {
243        self.budget.release(self.slot);
244    }
245
246    /// This slot's current grant.
247    pub fn grant(&self) -> u32 {
248        self.budget.grant(self.slot)
249    }
250
251    /// Wait until the grant differs from `seen`, then return it.
252    /// Uses a check/register/recheck to avoid missing a rebalance between the
253    /// load and the park. Only one waiter may park per slot; use your own
254    /// `Claimant`.
255    pub async fn wait_grant_change(&self, seen: u32) -> u32 {
256        core::future::poll_fn(|cx| {
257            let g = self.grant();
258            if g != seen {
259                return Poll::Ready(g);
260            }
261            self.budget.register(self.slot, cx.waker());
262            let g = self.grant();
263            if g != seen {
264                Poll::Ready(g)
265            } else {
266                Poll::Pending
267            }
268        })
269        .await
270    }
271
272    /// The slot this handle is bound to.
273    pub const fn slot(&self) -> u8 {
274        self.slot
275    }
276}
277
278/// How a [`Budget`]'s capacity is divided over its holders' wants.
279///
280/// `&self`, like [`ScalingPolicy`](crate::ScalingPolicy): a policy that keeps
281/// state (a ramp deadline) holds it in interior mutability so the impl can
282/// live in a `static`.
283pub trait BudgetPolicy {
284    /// Write the new grants into `grants`, which arrives holding the previous
285    /// division. Return when to run again while the division is converging,
286    /// `None` once it is settled.
287    fn divide(
288        &self,
289        capacity: u32,
290        wants: &[u32],
291        grants: &mut [u32],
292        now: Instant,
293    ) -> Option<Instant>;
294}
295
296/// Every holder gets its want when the capacity covers the sum; otherwise
297/// the capacity is split in proportion to the wants, with the integer
298/// remainder handed one unit at a time to the lowest slots that can use it.
299/// Changes apply immediately in both directions.
300pub struct FairShare;
301
302impl FairShare {
303    fn targets(capacity: u32, wants: &[u32], mut visit: impl FnMut(usize, u32)) {
304        let total: u64 = wants.iter().map(|w| u64::from(*w)).sum();
305        if total <= u64::from(capacity) {
306            for (i, w) in wants.iter().enumerate() {
307                visit(i, *w);
308            }
309            return;
310        }
311        let capacity = u64::from(capacity);
312        let floor = |w: &u32| (capacity * u64::from(*w) / total) as u32;
313        let used: u64 = wants.iter().map(|w| u64::from(floor(w))).sum();
314        let mut left = capacity - used;
315        for (i, w) in wants.iter().enumerate() {
316            let mut t = floor(w);
317            if left > 0 && t < *w {
318                t += 1;
319                left -= 1;
320            }
321            visit(i, t);
322        }
323    }
324}
325
326impl BudgetPolicy for FairShare {
327    fn divide(
328        &self,
329        capacity: u32,
330        wants: &[u32],
331        grants: &mut [u32],
332        _now: Instant,
333    ) -> Option<Instant> {
334        Self::targets(capacity, wants, |i, t| grants[i] = t);
335        None
336    }
337}
338
339/// [`FairShare`]'s division, applied asymmetrically: a cut lands at once, an
340/// increase is ramped at most `step` units per `interval`. The safety shape
341/// of a shared power or bandwidth budget — a holder must never be granted
342/// more than the budget can carry, so reductions cannot wait, while a holder
343/// drawing more can wait for the others to have backed off.
344pub struct ShrinkFastGrowSlow {
345    step: u32,
346    interval: Duration,
347    /// The earliest instant the next increase may land.
348    next: Mutex<CriticalSectionRawMutex, Cell<Option<Instant>>>,
349}
350
351impl ShrinkFastGrowSlow {
352    /// A policy raising any grant by at most `step` units per `interval`.
353    pub const fn new(step: u32, interval: Duration) -> Self {
354        Self {
355            step,
356            interval,
357            next: Mutex::new(Cell::new(None)),
358        }
359    }
360}
361
362impl BudgetPolicy for ShrinkFastGrowSlow {
363    fn divide(
364        &self,
365        capacity: u32,
366        wants: &[u32],
367        grants: &mut [u32],
368        now: Instant,
369    ) -> Option<Instant> {
370        let due = self.next.lock(|c| c.get()).is_none_or(|t| now >= t);
371        let mut converging = false;
372        FairShare::targets(capacity, wants, |i, t| {
373            let g = &mut grants[i];
374            if t < *g {
375                *g = t;
376            } else if t > *g {
377                if due {
378                    *g = (*g).saturating_add(self.step).min(t);
379                }
380                converging |= *g < t;
381            }
382        });
383        let next = if converging {
384            let at = if due {
385                now + self.interval
386            } else {
387                self.next.lock(|c| c.get()).unwrap_or(now)
388            };
389            Some(at)
390        } else {
391            None
392        };
393        self.next.lock(|c| c.set(next));
394        next
395    }
396}