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}