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    /// The published install counter last yielded. The wake counter above
162    /// also moves for refusals (so the events stream can deliver them);
163    /// this one is what keeps a refusal from resolving `changed()` with an
164    /// unchanged snapshot.
165    seen_generation: u64,
166}
167
168/// The cell a `Changes` watches: a type's `static`, or an instance's own.
169///
170/// Two known shapes, so the type-keyed path stays a bare pointer — the
171/// `Arc` exists only where an instance's cell has to outlive the `Dynamic`
172/// that handed the `Changes` out.
173enum CellRef<T: 'static> {
174    Static(&'static crate::ConfigCell<T>),
175    Shared(std::sync::Arc<crate::ConfigCell<T>>),
176}
177
178impl<T> CellRef<T> {
179    fn get(&self) -> &crate::ConfigCell<T> {
180        match self {
181            Self::Static(cell) => cell,
182            Self::Shared(cell) => cell,
183        }
184    }
185}
186
187impl<T: Send + Sync + 'static> Changes<T> {
188    pub(crate) fn new(cell: &'static crate::ConfigCell<T>) -> Self {
189        Self {
190            seen: cell.notify().generation(),
191            seen_generation: cell.generation(),
192            cell: CellRef::Static(cell),
193        }
194    }
195
196    /// A `Changes` over an instance's shared cell; what
197    /// [`Dynamic::changes`](crate::Dynamic::changes) hands out.
198    pub(crate) fn new_shared(cell: std::sync::Arc<crate::ConfigCell<T>>) -> Self {
199        Self {
200            seen: cell.notify().generation(),
201            seen_generation: cell.generation(),
202            cell: CellRef::Shared(cell),
203        }
204    }
205
206    /// Resolves with the snapshot installed by the next reload.
207    ///
208    /// Reloads that land while nothing is awaiting are not queued: waking up to
209    /// the *latest* configuration is what a reader wants, and a queue would
210    /// hand it stale ones first.
211    pub fn changed(&mut self) -> impl Future<Output = Arc<T>> + '_ {
212        Changed { changes: self }
213    }
214
215    /// The generation this handle has already observed.
216    ///
217    /// Zero before anything has been stored.
218    #[must_use]
219    pub fn seen(&self) -> u64 {
220        self.seen
221    }
222}
223
224impl<T: Send + Sync + 'static> std::fmt::Debug for Changes<T> {
225    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226        f.debug_struct("Changes")
227            .field("seen", &self.seen)
228            // The cell is a `&'static` with no useful rendering.
229            .finish_non_exhaustive()
230    }
231}
232
233struct Changed<'a, T: Send + Sync + 'static> {
234    changes: &'a mut Changes<T>,
235}
236
237impl<T: Send + Sync + 'static> Future for Changed<'_, T> {
238    type Output = Arc<T>;
239
240    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Arc<T>> {
241        let changes = &mut self.get_mut().changes;
242        let Changes {
243            cell,
244            seen,
245            seen_generation,
246        } = changes;
247        let cell = cell.get();
248        let notify = cell.notify();
249
250        // The check-register-check protocol lives in `poll_with`; the load
251        // closure supplies the value a moved generation implies — filtered
252        // on the PUBLISHED install counter, because the wake counter also
253        // moves for refusals and a refusal must cost a success waiter at
254        // most a re-registration, never a spurious yield.
255        notify.poll_with(seen, context.waker(), || {
256            let generation = cell.generation();
257
258            if generation == *seen_generation {
259                return None;
260            }
261
262            *seen_generation = generation;
263
264            cell.load()
265        })
266    }
267}
268
269// ---------------------------------------------------------------------------
270// Events: installs and refusals, one stream
271// ---------------------------------------------------------------------------
272
273/// One thing that happened to a configuration: an install, or a refusal.
274///
275/// What [`Events`] yields. `Refused` carries the [`FailureStatus`] — the
276/// category and the key path, never a message and never a value, the same
277/// discipline as every diagnostic surface here.
278///
279/// [`FailureStatus`]: crate::FailureStatus
280#[non_exhaustive]
281pub enum Event<T> {
282    /// A reload installed a snapshot.
283    #[non_exhaustive]
284    Reloaded {
285        /// The snapshot now serving.
286        current: Arc<T>,
287        /// The generation it became, and when it landed.
288        meta: crate::SnapshotMeta,
289        /// What caused the install, when the cell recorded one.
290        reason: Option<crate::ReloadReason>,
291    },
292    /// A reload installed nothing; the previous snapshot keeps serving.
293    Refused(crate::FailureStatus),
294}
295
296// Hand-written for the same reason `ReloadEvent`'s is: a derive would
297// demand `T: Clone`.
298impl<T> Clone for Event<T> {
299    fn clone(&self) -> Self {
300        match self {
301            Self::Reloaded {
302                current,
303                meta,
304                reason,
305            } => Self::Reloaded {
306                current: Arc::clone(current),
307                meta: *meta,
308                reason: reason.clone(),
309            },
310            Self::Refused(status) => Self::Refused(status.clone()),
311        }
312    }
313}
314
315impl<T> std::fmt::Debug for Event<T> {
316    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317        // Presence and metadata, never the snapshot: an event is a
318        // diagnostic, and a configuration holds passwords.
319        match self {
320            Self::Reloaded { meta, reason, .. } => f
321                .debug_struct("Reloaded")
322                .field("generation", &meta.generation)
323                .field("reason", reason)
324                .finish_non_exhaustive(),
325            Self::Refused(status) => f.debug_tuple("Refused").field(status).finish(),
326        }
327    }
328}
329
330/// A handle that resolves for installs **and refusals**, where [`Changes`]
331/// resolves for installs alone.
332///
333/// Created by `events()` on a generated type, a [`Dynamic`](crate::Dynamic)
334/// or a [`ConfigCell`](crate::ConfigCell). The push half of the status
335/// surface: a supervisor that wants to react to refused reloads no longer
336/// polls `status()` on a timer.
337///
338/// Latest-wins within each kind, and a refusal is never collapsed into a
339/// success — a refusal raced by an install yields both, refusal first,
340/// mirroring the Python binding's `events()` ordering.
341pub struct Events<T: Send + Sync + 'static> {
342    cell: CellRef<T>,
343    seen_notify: u64,
344    seen_generation: u64,
345    seen_refusals: u64,
346}
347
348impl<T: Send + Sync + 'static> Events<T> {
349    pub(crate) fn new(cell: &'static crate::ConfigCell<T>) -> Self {
350        Self {
351            seen_notify: cell.notify().generation(),
352            seen_generation: cell.generation(),
353            seen_refusals: cell.refusals(),
354            cell: CellRef::Static(cell),
355        }
356    }
357
358    /// An `Events` over an instance's shared cell; what
359    /// [`Dynamic::events`](crate::Dynamic::events) hands out.
360    pub(crate) fn new_shared(cell: std::sync::Arc<crate::ConfigCell<T>>) -> Self {
361        Self {
362            seen_notify: cell.notify().generation(),
363            seen_generation: cell.generation(),
364            seen_refusals: cell.refusals(),
365            cell: CellRef::Shared(cell),
366        }
367    }
368
369    /// Resolves with the next event.
370    pub fn next_event(&mut self) -> impl Future<Output = Event<T>> + '_ {
371        NextEvent { events: self }
372    }
373
374    /// What has not been delivered yet, refusals first.
375    ///
376    /// Reading this ADVANCES the corresponding seen-counter, so one bump
377    /// that carried both a refusal and an install yields two events across
378    /// two polls — the drain-then-park structure in `NextEvent::poll` is
379    /// what keeps the second one from waiting for another wake.
380    fn pending(&mut self) -> Option<Event<T>> {
381        let cell = self.cell.get();
382
383        let refusals = cell.refusals();
384
385        if refusals != self.seen_refusals {
386            self.seen_refusals = refusals;
387
388            if let Some(status) = cell.status().last_failure {
389                return Some(Event::Refused(status));
390            }
391        }
392
393        let generation = cell.generation();
394
395        if generation != self.seen_generation {
396            self.seen_generation = generation;
397
398            // `meta` is stored before the wake that got us here, so a
399            // loaded snapshot always has one; a `None` pair means the
400            // install is still mid-flight and the next poll will see it.
401            if let (Some(current), Some(meta)) = (cell.load(), cell.meta()) {
402                let status = cell.status();
403
404                return Some(Event::Reloaded {
405                    current,
406                    meta,
407                    reason: status.last_reason,
408                });
409            }
410        }
411
412        None
413    }
414}
415
416impl<T: Send + Sync + 'static> std::fmt::Debug for Events<T> {
417    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418        f.debug_struct("Events")
419            .field("seen_generation", &self.seen_generation)
420            .field("seen_refusals", &self.seen_refusals)
421            .finish_non_exhaustive()
422    }
423}
424
425struct NextEvent<'a, T: Send + Sync + 'static> {
426    events: &'a mut Events<T>,
427}
428
429impl<T: Send + Sync + 'static> Future for NextEvent<'_, T> {
430    type Output = Event<T>;
431
432    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Event<T>> {
433        let events = &mut self.get_mut().events;
434
435        // Drain before parking: one notify bump may cover a refusal AND an
436        // install (poll_with reads the counter once), and whichever was not
437        // yielded on the first poll must not wait for another wake.
438        if let Some(event) = events.pending() {
439            return Poll::Ready(event);
440        }
441
442        let Events {
443            cell,
444            seen_notify,
445            seen_generation,
446            seen_refusals,
447        } = events;
448        let cell = cell.get();
449
450        cell.notify().poll_with(seen_notify, context.waker(), || {
451            // Inlined `pending()`: the closure cannot borrow `events`
452            // whole while `seen_notify` is borrowed by `poll_with`.
453            let refusals = cell.refusals();
454
455            if refusals != *seen_refusals {
456                *seen_refusals = refusals;
457
458                if let Some(status) = cell.status().last_failure {
459                    return Some(Event::Refused(status));
460                }
461            }
462
463            let generation = cell.generation();
464
465            if generation != *seen_generation {
466                *seen_generation = generation;
467
468                if let (Some(current), Some(meta)) = (cell.load(), cell.meta()) {
469                    let status = cell.status();
470
471                    return Some(Event::Reloaded {
472                        current,
473                        meta,
474                        reason: status.last_reason,
475                    });
476                }
477            }
478
479            None
480        })
481    }
482}
483
484// ---------------------------------------------------------------------------
485// Running blocking work
486// ---------------------------------------------------------------------------
487
488/// Somewhere to run blocking work from an async context.
489///
490/// Implement this to hand the crate your runtime's blocking pool. Without one
491/// it spawns a thread per call, which is correct everywhere and cheap enough
492/// for work that happens at startup and on reload.
493pub trait BlockingExecutor: Send + Sync + 'static {
494    /// Runs `work` somewhere it is allowed to block.
495    fn execute(&self, work: Box<dyn FnOnce() + Send + 'static>);
496}
497
498static EXECUTOR: OnceLock<Box<dyn BlockingExecutor>> = OnceLock::new();
499
500/// Installs the blocking executor, once per process.
501///
502/// ```
503/// use dynamic_config::BlockingExecutor;
504///
505/// struct Threads;
506///
507/// impl BlockingExecutor for Threads {
508///     fn execute(&self, work: Box<dyn FnOnce() + Send + 'static>) {
509///         // `async_std::task::spawn_blocking(work)` and `smol::unblock`
510///         // slot in here just as well.
511///         std::thread::spawn(work);
512///     }
513/// }
514///
515/// // Once per process; a second call reports that one is already installed.
516/// let _ = dynamic_config::set_blocking_executor(Threads);
517/// ```
518///
519/// # Errors
520///
521/// If one is already installed. The rejected executor is returned rather than
522/// dropped, so a caller that wants to can tell "already set" from "failed".
523pub fn set_blocking_executor(
524    executor: impl BlockingExecutor,
525) -> Result<(), Box<dyn BlockingExecutor>> {
526    // `OnceLock::set` wants the error type to be `Debug`; a trait object is not,
527    // and requiring `Debug` of every executor to satisfy a `Result` would be the
528    // tail wagging the dog.
529    match EXECUTOR.set(Box::new(executor)) {
530        Ok(()) => Ok(()),
531        Err(rejected) => Err(rejected),
532    }
533}
534
535/// Hands `work` to wherever blocking work belongs.
536fn dispatch(work: Box<dyn FnOnce() + Send + 'static>) {
537    if let Some(executor) = EXECUTOR.get() {
538        executor.execute(work);
539
540        return;
541    }
542
543    // A pool beats a fresh thread, and a tokio user has one already — but the
544    // `tokio` *feature* does not prove there is a tokio *runtime*: a program
545    // that enables it and then drives `load_async` from smol would panic
546    // inside `spawn_blocking`. Checked, not assumed.
547    #[cfg(feature = "tokio")]
548    if let Ok(handle) = tokio::runtime::Handle::try_current() {
549        handle.spawn_blocking(work);
550
551        return;
552    }
553
554    // Correct on every runtime. A configuration load is rare enough that the
555    // thread is not the expensive part. If even the thread cannot be spawned,
556    // `work` is dropped — and dropping it is what runs the `Guard` inside,
557    // which wakes the waiter with `ErrorKind::Backend` rather than leaving it
558    // pending for the life of the process. No panic on any path.
559    if let Err(error) = std::thread::Builder::new()
560        .name("dynamic-config-load".to_owned())
561        .spawn(work)
562    {
563        crate::log::warning!("could not spawn a thread to load configuration: {error}");
564    }
565}
566
567/// Runs blocking configuration work without blocking the caller's executor.
568///
569/// # Errors
570///
571/// Whatever `work` returns, plus [`ErrorKind::Backend`] if it never produced a
572/// result — a panic inside it, or a runtime shutting down underneath.
573pub async fn off_thread<T, F>(work: F) -> Result<T, Error>
574where
575    F: FnOnce() -> Result<T, Error> + Send + 'static,
576    T: Send + 'static,
577{
578    let slot = Arc::new(Slot::<Result<T, Error>>::default());
579
580    // The guard is *captured*, not created inside the closure: a closure that
581    // is dropped without ever running — a thread that could not be spawned, a
582    // pool shutting down underneath — never executes its body, so a guard
583    // built there would never exist. A captured guard is dropped with the
584    // closure, and its drop is what wakes the waiter.
585    let guard = Guard {
586        slot: Some(Arc::clone(&slot)),
587    };
588
589    dispatch(Box::new(move || {
590        let mut guard = guard;
591
592        // A panic here drops `guard` during unwinding, which fills the slot
593        // with the Backend error instead of leaving the waiter pending for
594        // the life of the process.
595        let outcome = work();
596
597        guard.disarm().fill(outcome);
598    }));
599
600    Awaiting { slot }.await
601}
602
603/// A place for one value, and the task waiting for it.
604struct Slot<T> {
605    value: Mutex<Option<T>>,
606    waker: Mutex<Option<Waker>>,
607}
608
609impl<T> Default for Slot<T> {
610    fn default() -> Self {
611        Self {
612            value: Mutex::new(None),
613            waker: Mutex::new(None),
614        }
615    }
616}
617
618impl<T> Slot<T> {
619    fn fill(&self, value: T) {
620        *self
621            .value
622            .lock()
623            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value);
624
625        let waker = self
626            .waker
627            .lock()
628            .unwrap_or_else(std::sync::PoisonError::into_inner)
629            .take();
630
631        if let Some(waker) = waker {
632            waker.wake();
633        }
634    }
635
636    fn take(&self) -> Option<T> {
637        self.value
638            .lock()
639            .unwrap_or_else(std::sync::PoisonError::into_inner)
640            .take()
641    }
642}
643
644/// Fills the slot with a failure if the work never got that far.
645///
646/// `Option` rather than a flag: disarming *takes* the slot, so the drop path
647/// cannot fill after a successful hand-off even by mistake.
648struct Guard<T> {
649    slot: Option<Arc<Slot<Result<T, Error>>>>,
650}
651
652impl<T> Guard<T> {
653    /// The work finished; the slot is the caller's to fill with the result.
654    fn disarm(&mut self) -> Arc<Slot<Result<T, Error>>> {
655        self.slot
656            .take()
657            .expect("a guard is disarmed at most once, right before filling")
658    }
659}
660
661impl<T> Drop for Guard<T> {
662    fn drop(&mut self) {
663        if let Some(slot) = self.slot.take() {
664            slot.fill(Err(Error::new(
665                ErrorKind::Backend,
666                "the configuration load did not finish; the task panicked or was cancelled",
667            )));
668        }
669    }
670}
671
672struct Awaiting<T> {
673    slot: Arc<Slot<T>>,
674}
675
676impl<T> Future for Awaiting<T> {
677    type Output = T;
678
679    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<T> {
680        if let Some(value) = self.slot.take() {
681            return Poll::Ready(value);
682        }
683
684        *self
685            .slot
686            .waker
687            .lock()
688            .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(context.waker().clone());
689
690        // Checked again after registering, for the same reason as `Changed`.
691        match self.slot.take() {
692            Some(value) => Poll::Ready(value),
693            None => Poll::Pending,
694        }
695    }
696}