Skip to main content

dynamic_config/
cell.rs

1//! Process-wide storage for one configuration snapshot.
2
3use std::sync::{Arc, OnceLock};
4
5use arc_swap::ArcSwap;
6
7/// A callback run after a reload, with the outgoing and incoming snapshots.
8type Hook<T> = Arc<dyn Fn(&Arc<T>, &Arc<T>) + Send + Sync>;
9
10/// One registered hook: the callback plus the token that identifies it for
11/// removal. Permanent hooks get a token too — it is cheaper than two list
12/// types, and nothing ever asks to remove them.
13struct Registered<T> {
14    token: u64,
15    hook: Hook<T>,
16}
17
18impl<T> Clone for Registered<T> {
19    fn clone(&self) -> Self {
20        Self {
21            token: self.token,
22            hook: Arc::clone(&self.hook),
23        }
24    }
25}
26
27/// Holds the current configuration snapshot for one type.
28///
29/// `ConfigCell::new()` is `const`, so this lives in a `static` — which is how
30/// `#[dynamic_config]` emits it.
31///
32/// Reads are lock-free. [`load`](Self::load) clones an `Arc` out of an
33/// [`ArcSwap`], so a reload never blocks a request handler and a reader that
34/// already holds an `Arc` keeps observing its own generation until it drops it.
35/// Call it once per unit of work: calling it twice within one request can
36/// straddle a reload and observe two different configurations.
37///
38/// # Example
39///
40/// ```
41/// use dynamic_config::ConfigCell;
42///
43/// static PORT: ConfigCell<u16> = ConfigCell::new();
44///
45/// assert!(PORT.load().is_none());
46///
47/// PORT.store(8080);
48/// assert_eq!(*PORT.load().unwrap(), 8080);
49/// ```
50pub struct ConfigCell<T> {
51    inner: OnceLock<ArcSwap<T>>,
52
53    /// Held as a snapshot rather than behind a lock, so dispatching a reload
54    /// takes no lock a callback could deadlock against by storing again.
55    hooks: OnceLock<ArcSwap<Vec<Registered<T>>>>,
56
57    /// Hands out hook tokens. Plain counter: 2^64 registrations outlives the
58    /// process by some margin.
59    next_token: std::sync::atomic::AtomicU64,
60
61    /// Generation counter and parked wakers, so async tasks can await a reload
62    /// instead of polling. No runtime involved: it is an atomic and a list.
63    #[cfg(feature = "async")]
64    notify: crate::asynchronous::Notify,
65}
66
67impl<T> ConfigCell<T> {
68    /// An empty cell.
69    #[must_use]
70    #[cfg(not(loom))]
71    pub const fn new() -> Self {
72        Self {
73            inner: OnceLock::new(),
74            hooks: OnceLock::new(),
75            next_token: std::sync::atomic::AtomicU64::new(0),
76            #[cfg(feature = "async")]
77            notify: crate::asynchronous::Notify::new(),
78        }
79    }
80
81    /// The same, minus `const`: loom's constructors are not.
82    #[must_use]
83    #[cfg(loom)]
84    pub fn new() -> Self {
85        Self {
86            inner: OnceLock::new(),
87            hooks: OnceLock::new(),
88            next_token: std::sync::atomic::AtomicU64::new(0),
89            #[cfg(feature = "async")]
90            notify: crate::asynchronous::Notify::new(),
91        }
92    }
93
94    /// Atomically installs `value` as the current snapshot.
95    ///
96    /// Reload callbacks run, and with the `async` feature every waiting task is
97    /// woken. Installing the *first* snapshot is not a reload, so callbacks do
98    /// not fire for it — there is nothing to compare against.
99    pub fn store(&self, value: T) {
100        let value = Arc::new(value);
101
102        // `get_or_init` settles the race between two threads installing the
103        // very first snapshot: one initializer wins, and the `swap` below
104        // applies this call's value either way.
105        let slot = self.inner.get_or_init(|| ArcSwap::new(Arc::clone(&value)));
106        let previous = slot.swap(Arc::clone(&value));
107
108        // If `get_or_init` just installed *our* value, `previous` is the very
109        // same `Arc`, and no callbacks fire. Two `store`s racing on a cold
110        // cell can still both dispatch — the loser's swap sees the winner's
111        // value as "previous" — which is the same thing a reload arriving
112        // moments after init would do, so callbacks must tolerate it anyway.
113        // Waiters are woken *before* the hooks run: a task awaiting
114        // `changes()` wants the new snapshot, which is already installed, and
115        // making it wait out every hook would hand one slow callback the power
116        // to delay every async reader.
117        #[cfg(feature = "async")]
118        self.notify.bump();
119
120        if !Arc::ptr_eq(&previous, &value) {
121            self.dispatch(&previous, &value);
122        }
123    }
124
125    /// Registers a callback for every later reload.
126    ///
127    /// The callback receives the outgoing and incoming snapshots, in that
128    /// order, and runs on whichever thread performed the reload — the watcher
129    /// thread, usually. Keep it short, and do not store again from inside one:
130    /// that recurses rather than deadlocking, which is worse.
131    ///
132    /// Callbacks registered this way cannot be removed — a hook for the life
133    /// of the process, which is what a server wants. Anything with a shorter
134    /// life — a test, a plugin, a subsystem that can be torn down — should
135    /// use [`on_reload_scoped`](Self::on_reload_scoped) and hold the guard.
136    ///
137    /// A hook that panics is caught, reported, and skipped for that reload;
138    /// the remaining hooks still run and the watcher thread survives. It is
139    /// not unregistered — a bug in a hook should be loud on every reload, not
140    /// once.
141    pub fn on_reload(&self, hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static) {
142        let _ = self.register(Arc::new(hook));
143    }
144
145    /// [`on_reload`](Self::on_reload), scoped: dropping the returned guard
146    /// unregisters the hook.
147    ///
148    /// For anything whose life is shorter than the process — the permanent
149    /// variant would keep a torn-down subsystem's callback firing forever.
150    #[must_use = "dropping the guard unregisters the hook; bind it for as long \
151                  as the hook should fire, or use `on_reload` for a permanent one"]
152    pub fn on_reload_scoped(
153        &'static self,
154        hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static,
155    ) -> HookGuard<T> {
156        HookGuard {
157            token: self.register(Arc::new(hook)),
158            cell: GuardCell::Static(self),
159        }
160    }
161
162    /// The scoped hook over an instance's shared cell; what
163    /// [`Dynamic::on_reload_scoped`](crate::Dynamic::on_reload_scoped)
164    /// hands out — the guard co-owns the cell, so it outliving the
165    /// `Dynamic` is safe rather than subtle.
166    pub(crate) fn on_reload_scoped_shared(
167        cell: &Arc<Self>,
168        hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static,
169    ) -> HookGuard<T> {
170        HookGuard {
171            token: cell.register(Arc::new(hook)),
172            cell: GuardCell::Shared(Arc::clone(cell)),
173        }
174    }
175
176    fn register(&self, hook: Hook<T>) -> u64 {
177        let token = self
178            .next_token
179            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
180
181        self.hooks
182            .get_or_init(|| ArcSwap::from_pointee(Vec::new()))
183            .rcu(|current| {
184                let mut next = Vec::with_capacity(current.len() + 1);
185
186                next.extend(current.iter().cloned());
187                next.push(Registered {
188                    token,
189                    hook: Arc::clone(&hook),
190                });
191
192                next
193            });
194
195        token
196    }
197
198    fn unregister(&self, token: u64) {
199        let Some(hooks) = self.hooks.get() else {
200            return;
201        };
202
203        hooks.rcu(|current| {
204            current
205                .iter()
206                .filter(|registered| registered.token != token)
207                .cloned()
208                .collect::<Vec<_>>()
209        });
210    }
211
212    fn dispatch(&self, previous: &Arc<T>, current: &Arc<T>) {
213        let Some(hooks) = self.hooks.get() else {
214            return;
215        };
216
217        // A snapshot of the list, so a callback that registers another one does
218        // not invalidate the iteration.
219        for registered in hooks.load().iter() {
220            // Caught per hook: a panic in one must neither silence the rest
221            // nor unwind into the watcher thread and kill it — a watcher that
222            // died with a live-looking handle is the failure mode this exists
223            // to prevent. `AssertUnwindSafe` is honest here: the hook gets
224            // shared references it cannot leave half-mutated.
225            let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
226                (registered.hook)(previous, current);
227            }));
228
229            if outcome.is_err() {
230                crate::log::warning!(
231                    "a reload hook panicked; it stays registered and the \
232                     remaining hooks still run"
233                );
234            }
235        }
236    }
237
238    /// The current snapshot, or `None` if nothing has been stored yet.
239    pub fn load(&self) -> Option<Arc<T>> {
240        self.inner.get().map(ArcSwap::load_full)
241    }
242
243    /// The current snapshot, panicking if there is none.
244    ///
245    /// `type_name` is used to build the message; the generated code passes the
246    /// annotated struct's name so the panic names the type the caller wrote.
247    ///
248    /// # Panics
249    ///
250    /// If nothing has been stored yet.
251    pub fn get_or_panic(&self, type_name: &str) -> Arc<T> {
252        self.load().unwrap_or_else(|| {
253            panic!(
254                "{type_name} has no snapshot installed; configure and install \
255                 one first: `{type_name}::builder(\"..\")...init()?`"
256            )
257        })
258    }
259
260    /// A handle woken by every later [`store`](Self::store).
261    ///
262    /// The snapshot current at this call counts as already seen, so the first
263    /// `changed()` waits for the *next* store. Read the value you start from
264    /// with [`load`](Self::load).
265    ///
266    /// Runtime-agnostic: it is a `Future`, and any executor drives it.
267    #[cfg(feature = "async")]
268    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
269    pub fn changes(&'static self) -> crate::Changes<T>
270    where
271        T: Send + Sync,
272    {
273        crate::Changes::new(self)
274    }
275
276    #[cfg(feature = "async")]
277    pub(crate) fn notify(&self) -> &crate::asynchronous::Notify {
278        &self.notify
279    }
280}
281
282/// Unregisters its hook when dropped. From
283/// [`on_reload_scoped`](ConfigCell::on_reload_scoped).
284pub struct HookGuard<T: 'static> {
285    cell: GuardCell<T>,
286    token: u64,
287}
288
289/// The cell a guard unregisters from: a type's `static`, or an instance's
290/// own — the same two shapes `Changes` distinguishes, for the same reason.
291enum GuardCell<T: 'static> {
292    Static(&'static ConfigCell<T>),
293    Shared(Arc<ConfigCell<T>>),
294}
295
296impl<T> Drop for HookGuard<T> {
297    fn drop(&mut self) {
298        match &self.cell {
299            GuardCell::Static(cell) => cell.unregister(self.token),
300            GuardCell::Shared(cell) => cell.unregister(self.token),
301        }
302    }
303}
304
305impl<T> std::fmt::Debug for HookGuard<T> {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        f.debug_struct("HookGuard")
308            .field("token", &self.token)
309            .finish_non_exhaustive()
310    }
311}
312
313impl<T> Default for ConfigCell<T> {
314    fn default() -> Self {
315        Self::new()
316    }
317}
318
319impl<T: std::fmt::Debug> std::fmt::Debug for ConfigCell<T> {
320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        match self.load() {
322            Some(value) => f.debug_tuple("ConfigCell").field(&value).finish(),
323            None => f.write_str("ConfigCell(uninitialized)"),
324        }
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331    use std::sync::Mutex;
332    use std::thread;
333
334    #[test]
335    fn a_fresh_cell_is_empty() {
336        let cell = ConfigCell::<u16>::new();
337
338        assert!(cell.load().is_none());
339    }
340
341    #[test]
342    fn a_reader_keeps_the_generation_it_took() {
343        let cell = ConfigCell::new();
344        cell.store(String::from("first"));
345
346        let held = cell.load().unwrap();
347        cell.store(String::from("second"));
348
349        assert_eq!(*held, "first");
350        assert_eq!(*cell.load().unwrap(), "second");
351    }
352
353    #[test]
354    fn concurrent_first_writes_do_not_lose_the_cell() {
355        let cell: &'static ConfigCell<usize> = Box::leak(Box::new(ConfigCell::new()));
356
357        let writers: Vec<_> = (0..8)
358            .map(|value| thread::spawn(move || cell.store(value)))
359            .collect();
360
361        for writer in writers {
362            writer.join().unwrap();
363        }
364
365        let final_value = *cell.load().expect("some writer must have won");
366        assert!(final_value < 8);
367    }
368
369    #[test]
370    fn the_first_store_is_an_initialization_not_a_reload() {
371        let seen = Arc::new(Mutex::new(Vec::new()));
372        let cell = ConfigCell::new();
373
374        let recorder = Arc::clone(&seen);
375        cell.on_reload(move |previous, current| {
376            recorder.lock().unwrap().push((**previous, **current));
377        });
378
379        cell.store(1u16);
380        assert!(
381            seen.lock().unwrap().is_empty(),
382            "there is nothing to compare the first snapshot against"
383        );
384
385        cell.store(2u16);
386        cell.store(3u16);
387
388        assert_eq!(*seen.lock().unwrap(), [(1, 2), (2, 3)]);
389    }
390
391    #[test]
392    fn every_registered_callback_runs() {
393        let count = Arc::new(Mutex::new(0usize));
394        let cell = ConfigCell::new();
395
396        for _ in 0..3 {
397            let counter = Arc::clone(&count);
398            cell.on_reload(move |_, _| *counter.lock().unwrap() += 1);
399        }
400
401        cell.store(1u16);
402        cell.store(2u16);
403
404        assert_eq!(*count.lock().unwrap(), 3);
405    }
406
407    #[test]
408    fn a_panicking_hook_silences_neither_the_rest_nor_the_next_reload() {
409        let count = Arc::new(Mutex::new(0usize));
410        let cell = ConfigCell::new();
411
412        cell.on_reload(|_, _| panic!("a bug in somebody's hook"));
413        {
414            let counter = Arc::clone(&count);
415            cell.on_reload(move |_, _| *counter.lock().unwrap() += 1);
416        }
417
418        cell.store(1u16);
419        cell.store(2u16);
420        cell.store(3u16);
421
422        assert_eq!(
423            *count.lock().unwrap(),
424            2,
425            "the hook after the panicking one must run on every reload"
426        );
427    }
428
429    #[test]
430    fn dropping_the_guard_unregisters_the_hook() {
431        let count = Arc::new(Mutex::new(0usize));
432        let cell: &'static ConfigCell<u16> = Box::leak(Box::new(ConfigCell::new()));
433
434        cell.store(1);
435
436        let guard = {
437            let counter = Arc::clone(&count);
438            cell.on_reload_scoped(move |_, _| *counter.lock().unwrap() += 1)
439        };
440
441        cell.store(2);
442        assert_eq!(*count.lock().unwrap(), 1);
443
444        drop(guard);
445        cell.store(3);
446        assert_eq!(
447            *count.lock().unwrap(),
448            1,
449            "an unregistered hook must not fire"
450        );
451    }
452
453    #[test]
454    #[should_panic(expected = "`DbConfig::builder(")]
455    fn get_or_panic_points_at_the_builder() {
456        ConfigCell::<u16>::new().get_or_panic("DbConfig");
457    }
458}