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