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            cell: self,
158            token: self.register(Arc::new(hook)),
159        }
160    }
161
162    fn register(&self, hook: Hook<T>) -> u64 {
163        let token = self
164            .next_token
165            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
166
167        self.hooks
168            .get_or_init(|| ArcSwap::from_pointee(Vec::new()))
169            .rcu(|current| {
170                let mut next = Vec::with_capacity(current.len() + 1);
171
172                next.extend(current.iter().cloned());
173                next.push(Registered {
174                    token,
175                    hook: Arc::clone(&hook),
176                });
177
178                next
179            });
180
181        token
182    }
183
184    fn unregister(&self, token: u64) {
185        let Some(hooks) = self.hooks.get() else {
186            return;
187        };
188
189        hooks.rcu(|current| {
190            current
191                .iter()
192                .filter(|registered| registered.token != token)
193                .cloned()
194                .collect::<Vec<_>>()
195        });
196    }
197
198    fn dispatch(&self, previous: &Arc<T>, current: &Arc<T>) {
199        let Some(hooks) = self.hooks.get() else {
200            return;
201        };
202
203        // A snapshot of the list, so a callback that registers another one does
204        // not invalidate the iteration.
205        for registered in hooks.load().iter() {
206            // Caught per hook: a panic in one must neither silence the rest
207            // nor unwind into the watcher thread and kill it — a watcher that
208            // died with a live-looking handle is the failure mode this exists
209            // to prevent. `AssertUnwindSafe` is honest here: the hook gets
210            // shared references it cannot leave half-mutated.
211            let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
212                (registered.hook)(previous, current);
213            }));
214
215            if outcome.is_err() {
216                crate::log::warning!(
217                    "a reload hook panicked; it stays registered and the \
218                     remaining hooks still run"
219                );
220            }
221        }
222    }
223
224    /// The current snapshot, or `None` if nothing has been stored yet.
225    pub fn load(&self) -> Option<Arc<T>> {
226        self.inner.get().map(ArcSwap::load_full)
227    }
228
229    /// The current snapshot, panicking if there is none.
230    ///
231    /// `type_name` is used to build the message; the generated code passes the
232    /// annotated struct's name so the panic names the type the caller wrote.
233    ///
234    /// # Panics
235    ///
236    /// If nothing has been stored yet.
237    pub fn get_or_panic(&self, type_name: &str) -> Arc<T> {
238        self.load().unwrap_or_else(|| {
239            panic!(
240                "{type_name} has no snapshot installed; configure and install \
241                 one first: `{type_name}::builder(\"..\")...init()?`"
242            )
243        })
244    }
245
246    /// A handle woken by every later [`store`](Self::store).
247    ///
248    /// The snapshot current at this call counts as already seen, so the first
249    /// `changed()` waits for the *next* store. Read the value you start from
250    /// with [`load`](Self::load).
251    ///
252    /// Runtime-agnostic: it is a `Future`, and any executor drives it.
253    #[cfg(feature = "async")]
254    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
255    pub fn changes(&'static self) -> crate::Changes<T>
256    where
257        T: Send + Sync,
258    {
259        crate::Changes::new(self)
260    }
261
262    #[cfg(feature = "async")]
263    pub(crate) fn notify(&self) -> &crate::asynchronous::Notify {
264        &self.notify
265    }
266}
267
268/// Unregisters its hook when dropped. From
269/// [`on_reload_scoped`](ConfigCell::on_reload_scoped).
270pub struct HookGuard<T: 'static> {
271    cell: &'static ConfigCell<T>,
272    token: u64,
273}
274
275impl<T> Drop for HookGuard<T> {
276    fn drop(&mut self) {
277        self.cell.unregister(self.token);
278    }
279}
280
281impl<T> std::fmt::Debug for HookGuard<T> {
282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283        f.debug_struct("HookGuard")
284            .field("token", &self.token)
285            .finish_non_exhaustive()
286    }
287}
288
289impl<T> Default for ConfigCell<T> {
290    fn default() -> Self {
291        Self::new()
292    }
293}
294
295impl<T: std::fmt::Debug> std::fmt::Debug for ConfigCell<T> {
296    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297        match self.load() {
298            Some(value) => f.debug_tuple("ConfigCell").field(&value).finish(),
299            None => f.write_str("ConfigCell(uninitialized)"),
300        }
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307    use std::sync::Mutex;
308    use std::thread;
309
310    #[test]
311    fn a_fresh_cell_is_empty() {
312        let cell = ConfigCell::<u16>::new();
313
314        assert!(cell.load().is_none());
315    }
316
317    #[test]
318    fn a_reader_keeps_the_generation_it_took() {
319        let cell = ConfigCell::new();
320        cell.store(String::from("first"));
321
322        let held = cell.load().unwrap();
323        cell.store(String::from("second"));
324
325        assert_eq!(*held, "first");
326        assert_eq!(*cell.load().unwrap(), "second");
327    }
328
329    #[test]
330    fn concurrent_first_writes_do_not_lose_the_cell() {
331        let cell: &'static ConfigCell<usize> = Box::leak(Box::new(ConfigCell::new()));
332
333        let writers: Vec<_> = (0..8)
334            .map(|value| thread::spawn(move || cell.store(value)))
335            .collect();
336
337        for writer in writers {
338            writer.join().unwrap();
339        }
340
341        let final_value = *cell.load().expect("some writer must have won");
342        assert!(final_value < 8);
343    }
344
345    #[test]
346    fn the_first_store_is_an_initialization_not_a_reload() {
347        let seen = Arc::new(Mutex::new(Vec::new()));
348        let cell = ConfigCell::new();
349
350        let recorder = Arc::clone(&seen);
351        cell.on_reload(move |previous, current| {
352            recorder.lock().unwrap().push((**previous, **current));
353        });
354
355        cell.store(1u16);
356        assert!(
357            seen.lock().unwrap().is_empty(),
358            "there is nothing to compare the first snapshot against"
359        );
360
361        cell.store(2u16);
362        cell.store(3u16);
363
364        assert_eq!(*seen.lock().unwrap(), [(1, 2), (2, 3)]);
365    }
366
367    #[test]
368    fn every_registered_callback_runs() {
369        let count = Arc::new(Mutex::new(0usize));
370        let cell = ConfigCell::new();
371
372        for _ in 0..3 {
373            let counter = Arc::clone(&count);
374            cell.on_reload(move |_, _| *counter.lock().unwrap() += 1);
375        }
376
377        cell.store(1u16);
378        cell.store(2u16);
379
380        assert_eq!(*count.lock().unwrap(), 3);
381    }
382
383    #[test]
384    fn a_panicking_hook_silences_neither_the_rest_nor_the_next_reload() {
385        let count = Arc::new(Mutex::new(0usize));
386        let cell = ConfigCell::new();
387
388        cell.on_reload(|_, _| panic!("a bug in somebody's hook"));
389        {
390            let counter = Arc::clone(&count);
391            cell.on_reload(move |_, _| *counter.lock().unwrap() += 1);
392        }
393
394        cell.store(1u16);
395        cell.store(2u16);
396        cell.store(3u16);
397
398        assert_eq!(
399            *count.lock().unwrap(),
400            2,
401            "the hook after the panicking one must run on every reload"
402        );
403    }
404
405    #[test]
406    fn dropping_the_guard_unregisters_the_hook() {
407        let count = Arc::new(Mutex::new(0usize));
408        let cell: &'static ConfigCell<u16> = Box::leak(Box::new(ConfigCell::new()));
409
410        cell.store(1);
411
412        let guard = {
413            let counter = Arc::clone(&count);
414            cell.on_reload_scoped(move |_, _| *counter.lock().unwrap() += 1)
415        };
416
417        cell.store(2);
418        assert_eq!(*count.lock().unwrap(), 1);
419
420        drop(guard);
421        cell.store(3);
422        assert_eq!(
423            *count.lock().unwrap(),
424            1,
425            "an unregistered hook must not fire"
426        );
427    }
428
429    #[test]
430    #[should_panic(expected = "`DbConfig::builder(")]
431    fn get_or_panic_points_at_the_builder() {
432        ConfigCell::<u16>::new().get_or_panic("DbConfig");
433    }
434}