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: CellRef<T>,
160    seen: u64,
161}
162
163/// The cell a `Changes` watches: a type's `static`, or an instance's own.
164///
165/// Two known shapes, so the type-keyed path stays a bare pointer — the
166/// `Arc` exists only where an instance's cell has to outlive the `Dynamic`
167/// that handed the `Changes` out.
168enum CellRef<T: 'static> {
169    Static(&'static crate::ConfigCell<T>),
170    Shared(std::sync::Arc<crate::ConfigCell<T>>),
171}
172
173impl<T> CellRef<T> {
174    fn get(&self) -> &crate::ConfigCell<T> {
175        match self {
176            Self::Static(cell) => cell,
177            Self::Shared(cell) => cell,
178        }
179    }
180}
181
182impl<T: Send + Sync + 'static> Changes<T> {
183    pub(crate) fn new(cell: &'static crate::ConfigCell<T>) -> Self {
184        Self {
185            seen: cell.notify().generation(),
186            cell: CellRef::Static(cell),
187        }
188    }
189
190    /// A `Changes` over an instance's shared cell; what
191    /// [`Dynamic::changes`](crate::Dynamic::changes) hands out.
192    pub(crate) fn new_shared(cell: std::sync::Arc<crate::ConfigCell<T>>) -> Self {
193        Self {
194            seen: cell.notify().generation(),
195            cell: CellRef::Shared(cell),
196        }
197    }
198
199    /// Resolves with the snapshot installed by the next reload.
200    ///
201    /// Reloads that land while nothing is awaiting are not queued: waking up to
202    /// the *latest* configuration is what a reader wants, and a queue would
203    /// hand it stale ones first.
204    pub fn changed(&mut self) -> impl Future<Output = Arc<T>> + '_ {
205        Changed { changes: self }
206    }
207
208    /// The generation this handle has already observed.
209    ///
210    /// Zero before anything has been stored.
211    #[must_use]
212    pub fn seen(&self) -> u64 {
213        self.seen
214    }
215}
216
217impl<T: Send + Sync + 'static> std::fmt::Debug for Changes<T> {
218    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219        f.debug_struct("Changes")
220            .field("seen", &self.seen)
221            // The cell is a `&'static` with no useful rendering.
222            .finish_non_exhaustive()
223    }
224}
225
226struct Changed<'a, T: Send + Sync + 'static> {
227    changes: &'a mut Changes<T>,
228}
229
230impl<T: Send + Sync + 'static> Future for Changed<'_, T> {
231    type Output = Arc<T>;
232
233    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Arc<T>> {
234        let changes = &mut self.get_mut().changes;
235        let cell = changes.cell.get();
236        let notify = cell.notify();
237
238        // The check-register-check protocol lives in `poll_with`; the load
239        // closure supplies the value a moved generation implies.
240        notify.poll_with(&mut changes.seen, context.waker(), || cell.load())
241    }
242}
243
244// ---------------------------------------------------------------------------
245// Running blocking work
246// ---------------------------------------------------------------------------
247
248/// Somewhere to run blocking work from an async context.
249///
250/// Implement this to hand the crate your runtime's blocking pool. Without one
251/// it spawns a thread per call, which is correct everywhere and cheap enough
252/// for work that happens at startup and on reload.
253pub trait BlockingExecutor: Send + Sync + 'static {
254    /// Runs `work` somewhere it is allowed to block.
255    fn execute(&self, work: Box<dyn FnOnce() + Send + 'static>);
256}
257
258static EXECUTOR: OnceLock<Box<dyn BlockingExecutor>> = OnceLock::new();
259
260/// Installs the blocking executor, once per process.
261///
262/// ```
263/// use dynamic_config::BlockingExecutor;
264///
265/// struct Threads;
266///
267/// impl BlockingExecutor for Threads {
268///     fn execute(&self, work: Box<dyn FnOnce() + Send + 'static>) {
269///         // `async_std::task::spawn_blocking(work)` and `smol::unblock`
270///         // slot in here just as well.
271///         std::thread::spawn(work);
272///     }
273/// }
274///
275/// // Once per process; a second call reports that one is already installed.
276/// let _ = dynamic_config::set_blocking_executor(Threads);
277/// ```
278///
279/// # Errors
280///
281/// If one is already installed. The rejected executor is returned rather than
282/// dropped, so a caller that wants to can tell "already set" from "failed".
283pub fn set_blocking_executor(
284    executor: impl BlockingExecutor,
285) -> Result<(), Box<dyn BlockingExecutor>> {
286    // `OnceLock::set` wants the error type to be `Debug`; a trait object is not,
287    // and requiring `Debug` of every executor to satisfy a `Result` would be the
288    // tail wagging the dog.
289    match EXECUTOR.set(Box::new(executor)) {
290        Ok(()) => Ok(()),
291        Err(rejected) => Err(rejected),
292    }
293}
294
295/// Hands `work` to wherever blocking work belongs.
296fn dispatch(work: Box<dyn FnOnce() + Send + 'static>) {
297    if let Some(executor) = EXECUTOR.get() {
298        executor.execute(work);
299
300        return;
301    }
302
303    // A pool beats a fresh thread, and a tokio user has one already — but the
304    // `tokio` *feature* does not prove there is a tokio *runtime*: a program
305    // that enables it and then drives `load_async` from smol would panic
306    // inside `spawn_blocking`. Checked, not assumed.
307    #[cfg(feature = "tokio")]
308    if let Ok(handle) = tokio::runtime::Handle::try_current() {
309        handle.spawn_blocking(work);
310
311        return;
312    }
313
314    // Correct on every runtime. A configuration load is rare enough that the
315    // thread is not the expensive part. If even the thread cannot be spawned,
316    // `work` is dropped — and dropping it is what runs the `Guard` inside,
317    // which wakes the waiter with `ErrorKind::Backend` rather than leaving it
318    // pending for the life of the process. No panic on any path.
319    if let Err(error) = std::thread::Builder::new()
320        .name("dynamic-config-load".to_owned())
321        .spawn(work)
322    {
323        crate::log::warning!("could not spawn a thread to load configuration: {error}");
324    }
325}
326
327/// Runs blocking configuration work without blocking the caller's executor.
328///
329/// # Errors
330///
331/// Whatever `work` returns, plus [`ErrorKind::Backend`] if it never produced a
332/// result — a panic inside it, or a runtime shutting down underneath.
333pub async fn off_thread<T, F>(work: F) -> Result<T, Error>
334where
335    F: FnOnce() -> Result<T, Error> + Send + 'static,
336    T: Send + 'static,
337{
338    let slot = Arc::new(Slot::<Result<T, Error>>::default());
339
340    // The guard is *captured*, not created inside the closure: a closure that
341    // is dropped without ever running — a thread that could not be spawned, a
342    // pool shutting down underneath — never executes its body, so a guard
343    // built there would never exist. A captured guard is dropped with the
344    // closure, and its drop is what wakes the waiter.
345    let guard = Guard {
346        slot: Some(Arc::clone(&slot)),
347    };
348
349    dispatch(Box::new(move || {
350        let mut guard = guard;
351
352        // A panic here drops `guard` during unwinding, which fills the slot
353        // with the Backend error instead of leaving the waiter pending for
354        // the life of the process.
355        let outcome = work();
356
357        guard.disarm().fill(outcome);
358    }));
359
360    Awaiting { slot }.await
361}
362
363/// A place for one value, and the task waiting for it.
364struct Slot<T> {
365    value: Mutex<Option<T>>,
366    waker: Mutex<Option<Waker>>,
367}
368
369impl<T> Default for Slot<T> {
370    fn default() -> Self {
371        Self {
372            value: Mutex::new(None),
373            waker: Mutex::new(None),
374        }
375    }
376}
377
378impl<T> Slot<T> {
379    fn fill(&self, value: T) {
380        *self
381            .value
382            .lock()
383            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value);
384
385        let waker = self
386            .waker
387            .lock()
388            .unwrap_or_else(std::sync::PoisonError::into_inner)
389            .take();
390
391        if let Some(waker) = waker {
392            waker.wake();
393        }
394    }
395
396    fn take(&self) -> Option<T> {
397        self.value
398            .lock()
399            .unwrap_or_else(std::sync::PoisonError::into_inner)
400            .take()
401    }
402}
403
404/// Fills the slot with a failure if the work never got that far.
405///
406/// `Option` rather than a flag: disarming *takes* the slot, so the drop path
407/// cannot fill after a successful hand-off even by mistake.
408struct Guard<T> {
409    slot: Option<Arc<Slot<Result<T, Error>>>>,
410}
411
412impl<T> Guard<T> {
413    /// The work finished; the slot is the caller's to fill with the result.
414    fn disarm(&mut self) -> Arc<Slot<Result<T, Error>>> {
415        self.slot
416            .take()
417            .expect("a guard is disarmed at most once, right before filling")
418    }
419}
420
421impl<T> Drop for Guard<T> {
422    fn drop(&mut self) {
423        if let Some(slot) = self.slot.take() {
424            slot.fill(Err(Error::new(
425                ErrorKind::Backend,
426                "the configuration load did not finish; the task panicked or was cancelled",
427            )));
428        }
429    }
430}
431
432struct Awaiting<T> {
433    slot: Arc<Slot<T>>,
434}
435
436impl<T> Future for Awaiting<T> {
437    type Output = T;
438
439    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<T> {
440        if let Some(value) = self.slot.take() {
441            return Poll::Ready(value);
442        }
443
444        *self
445            .slot
446            .waker
447            .lock()
448            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(context.waker().clone());
449
450        // Checked again after registering, for the same reason as `Changed`.
451        match self.slot.take() {
452            Some(value) => Poll::Ready(value),
453            None => Poll::Pending,
454        }
455    }
456}