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