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}