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}