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//! [`WAITERS`] is the number. Four is chosen for the shape of the problem
8//! rather than as a guess: a device has a handful of tasks that care about
9//! configuration, not a handful of thousands. A fifth waiter replaces the
10//! oldest rather than being dropped — a task that is never woken is a bug that
11//! shows up as a hang, and one that is woken early merely polls again.
12
13use core::cell::RefCell;
14use core::future::Future;
15use core::pin::Pin;
16use core::task::{Context, Poll, Waker};
17
18use critical_section::Mutex;
19
20use crate::DEFAULT_WAITERS;
21
22/// A generation counter and the tasks waiting on it.
23#[derive(Debug)]
24pub(crate) struct Notify<const WAITERS: usize> {
25 inner: Mutex<RefCell<State<WAITERS>>>,
26}
27
28#[derive(Debug)]
29struct State<const WAITERS: usize> {
30 generation: u32,
31 waiting: [Option<Waker>; WAITERS],
32}
33
34impl<const WAITERS: usize> Notify<WAITERS> {
35 pub(crate) const fn new() -> Self {
36 Self {
37 inner: Mutex::new(RefCell::new(State {
38 generation: 0,
39 waiting: [const { None }; WAITERS],
40 })),
41 }
42 }
43
44 pub(crate) fn generation(&self) -> u32 {
45 critical_section::with(|token| self.inner.borrow(token).borrow().generation)
46 }
47
48 /// Records a new configuration and wakes everything waiting.
49 pub(crate) fn bump(&self) {
50 let woken = critical_section::with(|token| {
51 let mut state = self.inner.borrow(token).borrow_mut();
52
53 // Wrapping rather than saturating: a saturated counter stops
54 // moving, and a counter that stops moving means every `changed()`
55 // after reload 4294967295 hangs forever. A wrap can at worst
56 // confuse a handle that slept through exactly 2^32 reloads —
57 // one missed wake-up on a device that has reloaded four billion
58 // times, against a permanent hang for everyone. Not close.
59 state.generation = state.generation.wrapping_add(1);
60
61 // `mem::replace`, not `mem::take`: `Default` for arrays is only
62 // implemented up to fixed lengths, and a const-generic length is
63 // not among them. The replacement is the same all-`None` array.
64 core::mem::replace(&mut state.waiting, [const { None }; WAITERS])
65 });
66
67 // Woken outside the section: a waker may poll immediately, on this
68 // core, and try to register again.
69 for waker in woken.into_iter().flatten() {
70 waker.wake();
71 }
72 }
73
74 fn register(&self, waker: &Waker) {
75 let evicted = critical_section::with(|token| {
76 let mut state = self.inner.borrow(token).borrow_mut();
77
78 for slot in &mut state.waiting {
79 match slot {
80 Some(existing) if existing.will_wake(waker) => return None,
81 Some(_) => {}
82 None => {
83 *slot = Some(waker.clone());
84
85 return None;
86 }
87 }
88 }
89
90 // Full. The occupant of slot 0 — not necessarily the oldest,
91 // since `bump` empties every slot and refills happen in scan
92 // order — is *woken*, not dropped: a dropped waker is a task
93 // nobody will ever poll again, and that is a hang. Woken, it
94 // polls, sees no change, and re-registers.
95 //
96 // With a *steady state* of more waiters than slots, that
97 // re-registration evicts somebody else and the churn never
98 // settles: the executor stays busy waking and re-parking, and a
99 // battery device never reaches its idle loop. That is why the
100 // slot count is a type parameter — size it to the real number of
101 // waiting tasks. A true no-alloc wait queue is on the roadmap.
102 state.waiting[0].replace(waker.clone())
103 });
104
105 // Woken outside the section, same as `bump`: the wake may poll the
106 // evicted task immediately, on this core, and it will want the
107 // critical section for its own re-registration.
108 if let Some(evicted) = evicted {
109 evicted.wake();
110 }
111 }
112}
113
114/// A handle that resolves each time the configuration is replaced.
115///
116/// Runtime-agnostic, because it is a `Future` and nothing more: Embassy, RTIC
117/// and a hand-written `poll` loop all drive it.
118///
119/// The configuration current when this was created counts as already seen, so
120/// the first [`changed`](Self::changed) waits for the *next* one.
121pub struct Changes<T: Clone + 'static, const WAITERS: usize = DEFAULT_WAITERS> {
122 cell: &'static crate::ConfigCell<T, WAITERS>,
123 seen: u32,
124}
125
126impl<T: Clone + 'static, const WAITERS: usize> Changes<T, WAITERS> {
127 pub(crate) fn new(cell: &'static crate::ConfigCell<T, WAITERS>) -> Self {
128 Self {
129 seen: cell.notify().generation(),
130 cell,
131 }
132 }
133
134 /// Resolves with the configuration installed by the next change.
135 ///
136 /// Changes that land while nothing is awaiting are not queued: waking up to
137 /// the *latest* configuration is what a reader wants, and a queue would
138 /// hand it stale ones first — which on a device means acting on a setting
139 /// that has already been superseded.
140 pub fn changed(&mut self) -> impl Future<Output = T> + '_ {
141 Changed { changes: self }
142 }
143
144 /// The generation this handle has already observed.
145 #[must_use]
146 pub const fn seen(&self) -> u32 {
147 self.seen
148 }
149}
150
151impl<T: Clone + 'static, const WAITERS: usize> core::fmt::Debug for Changes<T, WAITERS> {
152 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
153 f.debug_struct("Changes")
154 .field("seen", &self.seen)
155 .finish_non_exhaustive()
156 }
157}
158
159struct Changed<'a, T: Clone + 'static, const WAITERS: usize> {
160 changes: &'a mut Changes<T, WAITERS>,
161}
162
163impl<T: Clone + 'static, const WAITERS: usize> Future for Changed<'_, T, WAITERS> {
164 type Output = T;
165
166 fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<T> {
167 let changes = &mut self.get_mut().changes;
168
169 if let Some(value) = take(changes) {
170 return Poll::Ready(value);
171 }
172
173 changes.cell.notify().register(context.waker());
174
175 // Checked again after registering: a store between the first check and
176 // the registration would otherwise be a wake-up nobody receives.
177 match take(changes) {
178 Some(value) => Poll::Ready(value),
179 None => Poll::Pending,
180 }
181 }
182}
183
184fn take<T: Clone + 'static, const WAITERS: usize>(changes: &mut Changes<T, WAITERS>) -> Option<T> {
185 let current = changes.cell.notify().generation();
186
187 if current == changes.seen {
188 return None;
189 }
190
191 changes.seen = current;
192
193 // A non-zero generation means `store` ran, so there is a value.
194 changes.cell.get()
195}