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