Skip to main content

dynamic_config/
asynchronous.rs

1//! Async support that does not name a runtime.
2//!
3//! Two things here needed a runtime, and neither actually does.
4//!
5//! **Waiting for a reload.** The obvious implementation returns a
6//! `tokio::sync::watch::Receiver`, and then the crate only works on tokio. But
7//! a change notification is a generation counter and a list of wakers, and both
8//! of those are `std`. [`Changes`] is that, and any executor drives it.
9//!
10//! **Running the load off the async thread.** This one is genuinely
11//! runtime-specific — a blocking pool belongs to a runtime. So it is pluggable
12//! instead: with the `tokio` feature it uses `spawn_blocking`, with an executor
13//! installed by [`set_blocking_executor`] it uses that, and otherwise it spawns
14//! a thread. A configuration load happens at startup and on reload, so a thread
15//! per call is a real answer rather than a placeholder.
16
17use std::future::Future;
18use std::pin::Pin;
19use std::sync::{Arc, OnceLock};
20
21use crate::sync::atomic::{AtomicU64, Ordering};
22use crate::sync::Mutex;
23use std::task::{Context, Poll, Waker};
24
25use crate::error::{Error, ErrorKind};
26
27// ---------------------------------------------------------------------------
28// Change notification
29// ---------------------------------------------------------------------------
30
31/// A generation counter and the tasks waiting on it.
32///
33/// Const-constructible, so it lives inside a `ConfigCell` in a `static`.
34#[derive(Debug)]
35pub struct Notify {
36    /// Bumped on every store. Zero means nothing has been stored yet.
37    generation: AtomicU64,
38    waiting: Mutex<Vec<Waker>>,
39}
40
41impl Notify {
42    #[cfg(not(loom))]
43    pub const fn new() -> Self {
44        Self {
45            generation: AtomicU64::new(0),
46            waiting: Mutex::new(Vec::new()),
47        }
48    }
49
50    /// The same, minus `const`: loom's constructors are not.
51    #[cfg(loom)]
52    pub fn new() -> Self {
53        Self {
54            generation: AtomicU64::new(0),
55            waiting: Mutex::new(Vec::new()),
56        }
57    }
58
59    /// The current change counter — each bump is one reload observed.
60    pub fn generation(&self) -> u64 {
61        self.generation.load(Ordering::Acquire)
62    }
63
64    /// Records a new snapshot and wakes everything waiting.
65    pub fn bump(&self) {
66        self.generation.fetch_add(1, Ordering::Release);
67
68        let woken = {
69            let mut waiting = self.lock();
70
71            std::mem::take(&mut *waiting)
72        };
73
74        // Woken outside the lock: a waker may poll immediately, on this thread,
75        // and try to register again.
76        for waker in woken {
77            waker.wake();
78        }
79    }
80
81    /// The whole wait step: has the generation moved past `seen`, and did
82    /// `load` produce the value it implies?
83    ///
84    /// Check, register, check again — a bump landing between the first
85    /// check and the registration would otherwise be a wake-up nobody
86    /// receives. This is the one copy of that protocol; `Changes` polls
87    /// through it, and the loom suite drives exactly this function.
88    pub fn poll_with<T>(
89        &self,
90        seen: &mut u64,
91        waker: &Waker,
92        mut load: impl FnMut() -> Option<T>,
93    ) -> std::task::Poll<T> {
94        let mut attempt = |seen: &mut u64| -> Option<T> {
95            let current = self.generation();
96
97            if current == *seen {
98                return None;
99            }
100
101            *seen = current;
102
103            load()
104        };
105
106        if let Some(value) = attempt(seen) {
107            return std::task::Poll::Ready(value);
108        }
109
110        self.register(waker);
111
112        match attempt(seen) {
113            Some(value) => std::task::Poll::Ready(value),
114            None => std::task::Poll::Pending,
115        }
116    }
117
118    fn register(&self, waker: &Waker) {
119        let mut waiting = self.lock();
120
121        if waiting.iter().any(|existing| existing.will_wake(waker)) {
122            return;
123        }
124
125        waiting.push(waker.clone());
126    }
127
128    fn lock(&self) -> crate::sync::MutexGuard<'_, Vec<Waker>> {
129        self.waiting
130            .lock()
131            .unwrap_or_else(std::sync::PoisonError::into_inner)
132    }
133}
134
135/// A handle that resolves each time the configuration is replaced.
136///
137/// Runtime-agnostic: tokio, async-std, smol and a hand-written executor all
138/// drive it the same way, because it is a `Future` and nothing more.
139///
140/// The snapshot current when this was created counts as already seen, so the
141/// first [`changed`](Self::changed) waits for the *next* reload. Read the value
142/// you start from with `current()`.
143///
144/// A handle created **before** `init()` has seen nothing, so the initial
145/// install is its first change — which makes `changes()` double as "wake me
146/// when configuration exists". That is contract, not accident: a task can be
147/// spawned before the configuration loads and pick up the moment it does.
148///
149/// # Example
150///
151/// ```ignore
152/// let mut changes = DbConfig::changes();
153///
154/// while let config = changes.changed().await {
155///     pool.resize(config.pool_size);
156/// }
157/// ```
158pub struct Changes<T: Send + Sync + 'static> {
159    cell: &'static crate::ConfigCell<T>,
160    seen: u64,
161}
162
163impl<T: Send + Sync + 'static> Changes<T> {
164    pub(crate) fn new(cell: &'static crate::ConfigCell<T>) -> Self {
165        Self {
166            seen: cell.notify().generation(),
167            cell,
168        }
169    }
170
171    /// Resolves with the snapshot installed by the next reload.
172    ///
173    /// Reloads that land while nothing is awaiting are not queued: waking up to
174    /// the *latest* configuration is what a reader wants, and a queue would
175    /// hand it stale ones first.
176    pub fn changed(&mut self) -> impl Future<Output = Arc<T>> + '_ {
177        Changed { changes: self }
178    }
179
180    /// The generation this handle has already observed.
181    ///
182    /// Zero before anything has been stored.
183    #[must_use]
184    pub fn seen(&self) -> u64 {
185        self.seen
186    }
187}
188
189impl<T: Send + Sync + 'static> std::fmt::Debug for Changes<T> {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        f.debug_struct("Changes")
192            .field("seen", &self.seen)
193            // The cell is a `&'static` with no useful rendering.
194            .finish_non_exhaustive()
195    }
196}
197
198struct Changed<'a, T: Send + Sync + 'static> {
199    changes: &'a mut Changes<T>,
200}
201
202impl<T: Send + Sync + 'static> Future for Changed<'_, T> {
203    type Output = Arc<T>;
204
205    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Arc<T>> {
206        let changes = &mut self.get_mut().changes;
207        let cell = changes.cell;
208        let notify = cell.notify();
209
210        // The check-register-check protocol lives in `poll_with`; the load
211        // closure supplies the value a moved generation implies.
212        notify.poll_with(&mut changes.seen, context.waker(), || cell.load())
213    }
214}
215
216// ---------------------------------------------------------------------------
217// Running blocking work
218// ---------------------------------------------------------------------------
219
220/// Somewhere to run blocking work from an async context.
221///
222/// Implement this to hand the crate your runtime's blocking pool. Without one
223/// it spawns a thread per call, which is correct everywhere and cheap enough
224/// for work that happens at startup and on reload.
225pub trait BlockingExecutor: Send + Sync + 'static {
226    /// Runs `work` somewhere it is allowed to block.
227    fn execute(&self, work: Box<dyn FnOnce() + Send + 'static>);
228}
229
230static EXECUTOR: OnceLock<Box<dyn BlockingExecutor>> = OnceLock::new();
231
232/// Installs the blocking executor, once per process.
233///
234/// ```
235/// use dynamic_config::BlockingExecutor;
236///
237/// struct Threads;
238///
239/// impl BlockingExecutor for Threads {
240///     fn execute(&self, work: Box<dyn FnOnce() + Send + 'static>) {
241///         // `async_std::task::spawn_blocking(work)` and `smol::unblock`
242///         // slot in here just as well.
243///         std::thread::spawn(work);
244///     }
245/// }
246///
247/// // Once per process; a second call reports that one is already installed.
248/// let _ = dynamic_config::set_blocking_executor(Threads);
249/// ```
250///
251/// # Errors
252///
253/// If one is already installed. The rejected executor is returned rather than
254/// dropped, so a caller that wants to can tell "already set" from "failed".
255pub fn set_blocking_executor(
256    executor: impl BlockingExecutor,
257) -> Result<(), Box<dyn BlockingExecutor>> {
258    // `OnceLock::set` wants the error type to be `Debug`; a trait object is not,
259    // and requiring `Debug` of every executor to satisfy a `Result` would be the
260    // tail wagging the dog.
261    match EXECUTOR.set(Box::new(executor)) {
262        Ok(()) => Ok(()),
263        Err(rejected) => Err(rejected),
264    }
265}
266
267/// Hands `work` to wherever blocking work belongs.
268fn dispatch(work: Box<dyn FnOnce() + Send + 'static>) {
269    if let Some(executor) = EXECUTOR.get() {
270        executor.execute(work);
271
272        return;
273    }
274
275    // A pool beats a fresh thread, and a tokio user has one already — but the
276    // `tokio` *feature* does not prove there is a tokio *runtime*: a program
277    // that enables it and then drives `load_async` from smol would panic
278    // inside `spawn_blocking`. Checked, not assumed.
279    #[cfg(feature = "tokio")]
280    if let Ok(handle) = tokio::runtime::Handle::try_current() {
281        handle.spawn_blocking(work);
282
283        return;
284    }
285
286    // Correct on every runtime. A configuration load is rare enough that the
287    // thread is not the expensive part. If even the thread cannot be spawned,
288    // `work` is dropped — and dropping it is what runs the `Guard` inside,
289    // which wakes the waiter with `ErrorKind::Backend` rather than leaving it
290    // pending for the life of the process. No panic on any path.
291    if let Err(error) = std::thread::Builder::new()
292        .name("dynamic-config-load".to_owned())
293        .spawn(work)
294    {
295        crate::log::warning!("could not spawn a thread to load configuration: {error}");
296    }
297}
298
299/// Runs blocking configuration work without blocking the caller's executor.
300///
301/// # Errors
302///
303/// Whatever `work` returns, plus [`ErrorKind::Backend`] if it never produced a
304/// result — a panic inside it, or a runtime shutting down underneath.
305pub async fn off_thread<T, F>(work: F) -> Result<T, Error>
306where
307    F: FnOnce() -> Result<T, Error> + Send + 'static,
308    T: Send + 'static,
309{
310    let slot = Arc::new(Slot::<Result<T, Error>>::default());
311
312    // The guard is *captured*, not created inside the closure: a closure that
313    // is dropped without ever running — a thread that could not be spawned, a
314    // pool shutting down underneath — never executes its body, so a guard
315    // built there would never exist. A captured guard is dropped with the
316    // closure, and its drop is what wakes the waiter.
317    let guard = Guard {
318        slot: Some(Arc::clone(&slot)),
319    };
320
321    dispatch(Box::new(move || {
322        let mut guard = guard;
323
324        // A panic here drops `guard` during unwinding, which fills the slot
325        // with the Backend error instead of leaving the waiter pending for
326        // the life of the process.
327        let outcome = work();
328
329        guard.disarm().fill(outcome);
330    }));
331
332    Awaiting { slot }.await
333}
334
335/// A place for one value, and the task waiting for it.
336struct Slot<T> {
337    value: Mutex<Option<T>>,
338    waker: Mutex<Option<Waker>>,
339}
340
341impl<T> Default for Slot<T> {
342    fn default() -> Self {
343        Self {
344            value: Mutex::new(None),
345            waker: Mutex::new(None),
346        }
347    }
348}
349
350impl<T> Slot<T> {
351    fn fill(&self, value: T) {
352        *self
353            .value
354            .lock()
355            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value);
356
357        let waker = self
358            .waker
359            .lock()
360            .unwrap_or_else(std::sync::PoisonError::into_inner)
361            .take();
362
363        if let Some(waker) = waker {
364            waker.wake();
365        }
366    }
367
368    fn take(&self) -> Option<T> {
369        self.value
370            .lock()
371            .unwrap_or_else(std::sync::PoisonError::into_inner)
372            .take()
373    }
374}
375
376/// Fills the slot with a failure if the work never got that far.
377///
378/// `Option` rather than a flag: disarming *takes* the slot, so the drop path
379/// cannot fill after a successful hand-off even by mistake.
380struct Guard<T> {
381    slot: Option<Arc<Slot<Result<T, Error>>>>,
382}
383
384impl<T> Guard<T> {
385    /// The work finished; the slot is the caller's to fill with the result.
386    fn disarm(&mut self) -> Arc<Slot<Result<T, Error>>> {
387        self.slot
388            .take()
389            .expect("a guard is disarmed at most once, right before filling")
390    }
391}
392
393impl<T> Drop for Guard<T> {
394    fn drop(&mut self) {
395        if let Some(slot) = self.slot.take() {
396            slot.fill(Err(Error::new(
397                ErrorKind::Backend,
398                "the configuration load did not finish; the task panicked or was cancelled",
399            )));
400        }
401    }
402}
403
404struct Awaiting<T> {
405    slot: Arc<Slot<T>>,
406}
407
408impl<T> Future for Awaiting<T> {
409    type Output = T;
410
411    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<T> {
412        if let Some(value) = self.slot.take() {
413            return Poll::Ready(value);
414        }
415
416        *self
417            .slot
418            .waker
419            .lock()
420            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(context.waker().clone());
421
422        // Checked again after registering, for the same reason as `Changed`.
423        match self.slot.take() {
424            Some(value) => Poll::Ready(value),
425            None => Poll::Pending,
426        }
427    }
428}