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}