Skip to main content

dynamic_config_embedded/
asynchronous.rs

1//! Awaiting the next configuration, with no allocator and no runtime.
2//!
3//! The same design as the `std` crate's `changes()`: a generation counter and
4//! the tasks waiting on it. What differs is the storage — a `Vec<Waker>` needs
5//! an allocator, so this keeps a fixed number of slots in a critical section.
6//!
7//! [`DEFAULT_WAITERS`] is the number, and it is a type parameter because the
8//! right number is a property of the firmware, not of this crate: a device's
9//! tasks are known at compile time, so the count of them that can await a
10//! configuration is known at compile time too. That is why there is no queue
11//! here and no plan for one — see the module's `Beyond the budget` note below.
12//!
13//! Four is the default for the shape of the problem rather than as a guess: a
14//! device has a handful of tasks that care about configuration, not a handful
15//! of thousands. A fifth waiter replaces the occupant of a slot rather than
16//! being dropped — a task that is never woken is a bug that shows up as a hang,
17//! and one that is woken early merely polls again.
18//!
19//! # Beyond the budget
20//!
21//! With more waiting tasks than slots there is no honest outcome, only a choice
22//! of bad ones: a fixed array cannot park what does not fit. Dropping a waker
23//! hangs a task with no diagnostic; refusing the registration hangs it too,
24//! because a `Future` that returns `Pending` without a registered waker is one
25//! nobody will poll again. So this evicts and wakes, which loses no wake-up —
26//! and, measured, costs a device its idle loop: five tasks on a four-slot cell
27//! wake each other without end, with no configuration change to show for it.
28//!
29//! That is a livelock, not mere churn, and the only fix is to not be over
30//! budget. [`ConfigCell::waiter_evictions`](crate::ConfigCell::waiter_evictions)
31//! is how a firmware finds out that it is — non-zero means `WAITERS` is too
32//! small, and it is a number a lab bench can read long before a battery does.
33//!
34//! # Interrupts
35//!
36//! Every read and write of the state below happens inside
37//! `critical_section::with`, so registering a waker, storing a configuration
38//! and reading one are all sound from an interrupt handler. Two rules make
39//! that true, and both are load-bearing:
40//!
41//! - no borrow of the `RefCell` outlives its critical section, so an interrupt
42//!   that lands between two of them finds nothing borrowed;
43//! - no `Waker` is woken while the section is held. A `wake` implementation
44//!   belongs to the executor and may do anything — including storing a
45//!   configuration or polling the task it just woke, on this core, before it
46//!   returns. Waking inside the section would re-enter the `RefCell` and panic.
47//!
48//! The cost is the length of the section: a scan of `WAITERS` slots and at most
49//! one `Waker` clone, which is two word stores. Nothing here parses, and
50//! nothing here waits.
51
52use core::cell::RefCell;
53use core::future::Future;
54use core::pin::Pin;
55use core::task::{Context, Poll, Waker};
56
57use critical_section::Mutex;
58
59use crate::DEFAULT_WAITERS;
60
61/// A generation counter and the tasks waiting on it.
62#[derive(Debug)]
63pub(crate) struct Notify<const WAITERS: usize> {
64    inner: Mutex<RefCell<State<WAITERS>>>,
65}
66
67#[derive(Debug)]
68struct State<const WAITERS: usize> {
69    generation: u32,
70    /// Registrations that had to displace another waiter, saturating. Four
71    /// bytes per cell, and the only way a firmware learns that `WAITERS` is
72    /// too small before the symptom — a device that never idles — reaches a
73    /// battery.
74    evictions: u32,
75    waiting: [Option<Waker>; WAITERS],
76}
77
78impl<const WAITERS: usize> Notify<WAITERS> {
79    pub(crate) const fn new() -> Self {
80        Self {
81            inner: Mutex::new(RefCell::new(State {
82                generation: 0,
83                evictions: 0,
84                waiting: [const { None }; WAITERS],
85            })),
86        }
87    }
88
89    pub(crate) fn generation(&self) -> u32 {
90        critical_section::with(|token| self.inner.borrow(token).borrow().generation)
91    }
92
93    pub(crate) fn evictions(&self) -> u32 {
94        critical_section::with(|token| self.inner.borrow(token).borrow().evictions)
95    }
96
97    /// Records a new configuration and wakes everything waiting.
98    pub(crate) fn bump(&self) {
99        let woken = critical_section::with(|token| {
100            let mut state = self.inner.borrow(token).borrow_mut();
101
102            // Wrapping rather than saturating: a saturated counter stops
103            // moving, and a counter that stops moving means every `changed()`
104            // after reload 4294967295 hangs forever. A wrap can at worst
105            // confuse a handle that slept through exactly 2^32 reloads —
106            // one missed wake-up on a device that has reloaded four billion
107            // times, against a permanent hang for everyone. Not close.
108            state.generation = state.generation.wrapping_add(1);
109
110            // `mem::replace`, not `mem::take`: `Default` for arrays is only
111            // implemented up to fixed lengths, and a const-generic length is
112            // not among them. The replacement is the same all-`None` array.
113            core::mem::replace(&mut state.waiting, [const { None }; WAITERS])
114        });
115
116        // Woken outside the section: a waker may poll immediately, on this
117        // core, and try to register again.
118        for waker in woken.into_iter().flatten() {
119            waker.wake();
120        }
121    }
122
123    fn register(&self, waker: &Waker) {
124        let evicted = critical_section::with(|token| {
125            let mut state = self.inner.borrow(token).borrow_mut();
126
127            for slot in &mut state.waiting {
128                match slot {
129                    Some(existing) if existing.will_wake(waker) => return None,
130                    Some(_) => {}
131                    None => {
132                        *slot = Some(waker.clone());
133
134                        return None;
135                    }
136                }
137            }
138
139            // Full. The occupant of slot 0 — not necessarily the oldest,
140            // since `bump` empties every slot and refills happen in scan
141            // order — is *woken*, not dropped: a dropped waker is a task
142            // nobody will ever poll again, and that is a hang. Woken, it
143            // polls, sees no change, and re-registers.
144            //
145            // Refusing this registration instead would be the same hang with
146            // a different name: `poll` would return `Pending` with no waker
147            // anywhere, and nothing would ever poll the task again.
148            //
149            // With a *steady state* of more waiters than slots, that
150            // re-registration evicts somebody else and it never settles:
151            // measured, five tasks on a four-slot cell trade wake-ups for as
152            // long as anyone watches, with no configuration change between
153            // them, and the executor never reaches its idle loop. No wake-up
154            // is lost — the cost is entirely power. That is why the slot count
155            // is a type parameter: size it to the real number of waiting
156            // tasks, which on a device is a number the firmware knows.
157            //
158            // The counter is what makes that diagnosable instead of a mystery
159            // brown-out. Saturating, because the question it answers is "did
160            // this ever happen", and a wrap could answer "no" to it.
161            state.evictions = state.evictions.saturating_add(1);
162
163            state.waiting[0].replace(waker.clone())
164        });
165
166        // Woken outside the section, same as `bump`: the wake may poll the
167        // evicted task immediately, on this core, and it will want the
168        // critical section for its own re-registration.
169        if let Some(evicted) = evicted {
170            evicted.wake();
171        }
172    }
173}
174
175/// A handle that resolves each time the configuration is replaced.
176///
177/// Runtime-agnostic, because it is a `Future` and nothing more: Embassy, RTIC
178/// and a hand-written `poll` loop all drive it.
179///
180/// The configuration current when this was created counts as already seen, so
181/// the first [`changed`](Self::changed) waits for the *next* one.
182pub struct Changes<T: Clone + 'static, const WAITERS: usize = DEFAULT_WAITERS> {
183    cell: &'static crate::ConfigCell<T, WAITERS>,
184    seen: u32,
185}
186
187impl<T: Clone + 'static, const WAITERS: usize> Changes<T, WAITERS> {
188    pub(crate) fn new(cell: &'static crate::ConfigCell<T, WAITERS>) -> Self {
189        Self {
190            seen: cell.notify().generation(),
191            cell,
192        }
193    }
194
195    /// Resolves with the configuration installed by the next change.
196    ///
197    /// Changes that land while nothing is awaiting are not queued: waking up to
198    /// the *latest* configuration is what a reader wants, and a queue would
199    /// hand it stale ones first — which on a device means acting on a setting
200    /// that has already been superseded.
201    pub fn changed(&mut self) -> impl Future<Output = T> + '_ {
202        Changed { changes: self }
203    }
204
205    /// The generation this handle has already observed.
206    #[must_use]
207    pub const fn seen(&self) -> u32 {
208        self.seen
209    }
210}
211
212impl<T: Clone + 'static, const WAITERS: usize> core::fmt::Debug for Changes<T, WAITERS> {
213    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
214        f.debug_struct("Changes")
215            .field("seen", &self.seen)
216            .finish_non_exhaustive()
217    }
218}
219
220struct Changed<'a, T: Clone + 'static, const WAITERS: usize> {
221    changes: &'a mut Changes<T, WAITERS>,
222}
223
224impl<T: Clone + 'static, const WAITERS: usize> Future for Changed<'_, T, WAITERS> {
225    type Output = T;
226
227    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<T> {
228        let changes = &mut self.get_mut().changes;
229
230        if let Some(value) = take(changes) {
231            return Poll::Ready(value);
232        }
233
234        changes.cell.notify().register(context.waker());
235
236        // Checked again after registering: a store between the first check and
237        // the registration would otherwise be a wake-up nobody receives.
238        match take(changes) {
239            Some(value) => Poll::Ready(value),
240            None => Poll::Pending,
241        }
242    }
243}
244
245fn take<T: Clone + 'static, const WAITERS: usize>(changes: &mut Changes<T, WAITERS>) -> Option<T> {
246    let current = changes.cell.notify().generation();
247
248    if current == changes.seen {
249        return None;
250    }
251
252    changes.seen = current;
253
254    // A non-zero generation means `store` ran, so there is a value.
255    changes.cell.get()
256}