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