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/// # Example
95///
96/// ```ignore
97/// let mut changes = DbConfig::changes();
98///
99/// while let config = changes.changed().await {
100///     pool.resize(config.pool_size);
101/// }
102/// ```
103pub struct Changes<T: Send + Sync + 'static> {
104    cell: &'static crate::ConfigCell<T>,
105    seen: u64,
106}
107
108impl<T: Send + Sync + 'static> Changes<T> {
109    pub(crate) fn new(cell: &'static crate::ConfigCell<T>) -> Self {
110        Self {
111            seen: cell.notify().generation(),
112            cell,
113        }
114    }
115
116    /// Resolves with the snapshot installed by the next reload.
117    ///
118    /// Reloads that land while nothing is awaiting are not queued: waking up to
119    /// the *latest* configuration is what a reader wants, and a queue would
120    /// hand it stale ones first.
121    pub fn changed(&mut self) -> impl Future<Output = Arc<T>> + '_ {
122        Changed { changes: self }
123    }
124
125    /// The generation this handle has already observed.
126    ///
127    /// Zero before anything has been stored.
128    #[must_use]
129    pub fn seen(&self) -> u64 {
130        self.seen
131    }
132}
133
134impl<T: Send + Sync + 'static> std::fmt::Debug for Changes<T> {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        f.debug_struct("Changes")
137            .field("seen", &self.seen)
138            // The cell is a `&'static` with no useful rendering.
139            .finish_non_exhaustive()
140    }
141}
142
143struct Changed<'a, T: Send + Sync + 'static> {
144    changes: &'a mut Changes<T>,
145}
146
147impl<T: Send + Sync + 'static> Future for Changed<'_, T> {
148    type Output = Arc<T>;
149
150    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Arc<T>> {
151        let changes = &mut self.get_mut().changes;
152        let notify = changes.cell.notify();
153
154        if let Some(value) = take(changes, notify) {
155            return Poll::Ready(value);
156        }
157
158        notify.register(context.waker());
159
160        // Checked again after registering: a store between the first check and
161        // the registration would otherwise be a wake-up nobody receives.
162        match take(changes, notify) {
163            Some(value) => Poll::Ready(value),
164            None => Poll::Pending,
165        }
166    }
167}
168
169fn take<T: Send + Sync + 'static>(changes: &mut Changes<T>, notify: &Notify) -> Option<Arc<T>> {
170    let current = notify.generation();
171
172    if current == changes.seen {
173        return None;
174    }
175
176    changes.seen = current;
177
178    // A non-zero generation means `store` ran, so there is a value.
179    changes.cell.load()
180}
181
182// ---------------------------------------------------------------------------
183// Running blocking work
184// ---------------------------------------------------------------------------
185
186/// Somewhere to run blocking work from an async context.
187///
188/// Implement this to hand the crate your runtime's blocking pool. Without one
189/// it spawns a thread per call, which is correct everywhere and cheap enough
190/// for work that happens at startup and on reload.
191pub trait BlockingExecutor: Send + Sync + 'static {
192    /// Runs `work` somewhere it is allowed to block.
193    fn execute(&self, work: Box<dyn FnOnce() + Send + 'static>);
194}
195
196static EXECUTOR: OnceLock<Box<dyn BlockingExecutor>> = OnceLock::new();
197
198/// Installs the blocking executor, once per process.
199///
200/// ```
201/// use dynamic_config::BlockingExecutor;
202///
203/// struct Threads;
204///
205/// impl BlockingExecutor for Threads {
206///     fn execute(&self, work: Box<dyn FnOnce() + Send + 'static>) {
207///         // `async_std::task::spawn_blocking(work)` and `smol::unblock`
208///         // slot in here just as well.
209///         std::thread::spawn(work);
210///     }
211/// }
212///
213/// // Once per process; a second call reports that one is already installed.
214/// let _ = dynamic_config::set_blocking_executor(Threads);
215/// ```
216///
217/// # Errors
218///
219/// If one is already installed. The rejected executor is returned rather than
220/// dropped, so a caller that wants to can tell "already set" from "failed".
221pub fn set_blocking_executor(
222    executor: impl BlockingExecutor,
223) -> Result<(), Box<dyn BlockingExecutor>> {
224    // `OnceLock::set` wants the error type to be `Debug`; a trait object is not,
225    // and requiring `Debug` of every executor to satisfy a `Result` would be the
226    // tail wagging the dog.
227    match EXECUTOR.set(Box::new(executor)) {
228        Ok(()) => Ok(()),
229        Err(rejected) => Err(rejected),
230    }
231}
232
233/// Hands `work` to wherever blocking work belongs.
234fn dispatch(work: Box<dyn FnOnce() + Send + 'static>) {
235    if let Some(executor) = EXECUTOR.get() {
236        executor.execute(work);
237
238        return;
239    }
240
241    // A pool beats a fresh thread, and a tokio user has one already — but the
242    // `tokio` *feature* does not prove there is a tokio *runtime*: a program
243    // that enables it and then drives `load_async` from smol would panic
244    // inside `spawn_blocking`. Checked, not assumed.
245    #[cfg(feature = "tokio")]
246    if let Ok(handle) = tokio::runtime::Handle::try_current() {
247        handle.spawn_blocking(work);
248
249        return;
250    }
251
252    // Correct on every runtime. A configuration load is rare enough that the
253    // thread is not the expensive part. If even the thread cannot be spawned,
254    // `work` is dropped — and dropping it is what runs the `Guard` inside,
255    // which wakes the waiter with `ErrorKind::Backend` rather than leaving it
256    // pending for the life of the process. No panic on any path.
257    if let Err(error) = std::thread::Builder::new()
258        .name("dynamic-config-load".to_owned())
259        .spawn(work)
260    {
261        crate::log::warning!("could not spawn a thread to load configuration: {error}");
262    }
263}
264
265/// Runs blocking configuration work without blocking the caller's executor.
266///
267/// # Errors
268///
269/// Whatever `work` returns, plus [`ErrorKind::Backend`] if it never produced a
270/// result — a panic inside it, or a runtime shutting down underneath.
271pub async fn off_thread<T, F>(work: F) -> Result<T, Error>
272where
273    F: FnOnce() -> Result<T, Error> + Send + 'static,
274    T: Send + 'static,
275{
276    let slot = Arc::new(Slot::<Result<T, Error>>::default());
277
278    // The guard is *captured*, not created inside the closure: a closure that
279    // is dropped without ever running — a thread that could not be spawned, a
280    // pool shutting down underneath — never executes its body, so a guard
281    // built there would never exist. A captured guard is dropped with the
282    // closure, and its drop is what wakes the waiter.
283    let guard = Guard {
284        slot: Some(Arc::clone(&slot)),
285    };
286
287    dispatch(Box::new(move || {
288        let mut guard = guard;
289
290        // A panic here drops `guard` during unwinding, which fills the slot
291        // with the Backend error instead of leaving the waiter pending for
292        // the life of the process.
293        let outcome = work();
294
295        guard.disarm().fill(outcome);
296    }));
297
298    Awaiting { slot }.await
299}
300
301/// A place for one value, and the task waiting for it.
302struct Slot<T> {
303    value: Mutex<Option<T>>,
304    waker: Mutex<Option<Waker>>,
305}
306
307impl<T> Default for Slot<T> {
308    fn default() -> Self {
309        Self {
310            value: Mutex::new(None),
311            waker: Mutex::new(None),
312        }
313    }
314}
315
316impl<T> Slot<T> {
317    fn fill(&self, value: T) {
318        *self
319            .value
320            .lock()
321            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value);
322
323        let waker = self
324            .waker
325            .lock()
326            .unwrap_or_else(std::sync::PoisonError::into_inner)
327            .take();
328
329        if let Some(waker) = waker {
330            waker.wake();
331        }
332    }
333
334    fn take(&self) -> Option<T> {
335        self.value
336            .lock()
337            .unwrap_or_else(std::sync::PoisonError::into_inner)
338            .take()
339    }
340}
341
342/// Fills the slot with a failure if the work never got that far.
343///
344/// `Option` rather than a flag: disarming *takes* the slot, so the drop path
345/// cannot fill after a successful hand-off even by mistake.
346struct Guard<T> {
347    slot: Option<Arc<Slot<Result<T, Error>>>>,
348}
349
350impl<T> Guard<T> {
351    /// The work finished; the slot is the caller's to fill with the result.
352    fn disarm(&mut self) -> Arc<Slot<Result<T, Error>>> {
353        self.slot
354            .take()
355            .expect("a guard is disarmed at most once, right before filling")
356    }
357}
358
359impl<T> Drop for Guard<T> {
360    fn drop(&mut self) {
361        if let Some(slot) = self.slot.take() {
362            slot.fill(Err(Error::new(
363                ErrorKind::Backend,
364                "the configuration load did not finish; the task panicked or was cancelled",
365            )));
366        }
367    }
368}
369
370struct Awaiting<T> {
371    slot: Arc<Slot<T>>,
372}
373
374impl<T> Future for Awaiting<T> {
375    type Output = T;
376
377    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<T> {
378        if let Some(value) = self.slot.take() {
379            return Poll::Ready(value);
380        }
381
382        *self
383            .slot
384            .waker
385            .lock()
386            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(context.waker().clone());
387
388        // Checked again after registering, for the same reason as `Changed`.
389        match self.slot.take() {
390            Some(value) => Poll::Ready(value),
391            None => Poll::Pending,
392        }
393    }
394}