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//! [`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}