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