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    pub const fn new() -> Self {
71        Self {
72            inner: OnceLock::new(),
73            hooks: OnceLock::new(),
74            next_token: std::sync::atomic::AtomicU64::new(0),
75            #[cfg(feature = "async")]
76            notify: crate::asynchronous::Notify::new(),
77        }
78    }
79
80    /// Atomically installs `value` as the current snapshot.
81    ///
82    /// Reload callbacks run, and with the `async` feature every waiting task is
83    /// woken. Installing the *first* snapshot is not a reload, so callbacks do
84    /// not fire for it — there is nothing to compare against.
85    pub fn store(&self, value: T) {
86        let value = Arc::new(value);
87
88        // `get_or_init` settles the race between two threads installing the
89        // very first snapshot: one initializer wins, and the `swap` below
90        // applies this call's value either way.
91        let slot = self.inner.get_or_init(|| ArcSwap::new(Arc::clone(&value)));
92        let previous = slot.swap(Arc::clone(&value));
93
94        // If `get_or_init` just installed *our* value, `previous` is the very
95        // same `Arc`, and no callbacks fire. Two `store`s racing on a cold
96        // cell can still both dispatch — the loser's swap sees the winner's
97        // value as "previous" — which is the same thing a reload arriving
98        // moments after init would do, so callbacks must tolerate it anyway.
99        // Waiters are woken *before* the hooks run: a task awaiting
100        // `changes()` wants the new snapshot, which is already installed, and
101        // making it wait out every hook would hand one slow callback the power
102        // to delay every async reader.
103        #[cfg(feature = "async")]
104        self.notify.bump();
105
106        if !Arc::ptr_eq(&previous, &value) {
107            self.dispatch(&previous, &value);
108        }
109    }
110
111    /// Registers a callback for every later reload.
112    ///
113    /// The callback receives the outgoing and incoming snapshots, in that
114    /// order, and runs on whichever thread performed the reload — the watcher
115    /// thread, usually. Keep it short, and do not store again from inside one:
116    /// that recurses rather than deadlocking, which is worse.
117    ///
118    /// Callbacks registered this way cannot be removed — a hook for the life
119    /// of the process, which is what a server wants. Anything with a shorter
120    /// life — a test, a plugin, a subsystem that can be torn down — should
121    /// use [`on_reload_scoped`](Self::on_reload_scoped) and hold the guard.
122    ///
123    /// A hook that panics is caught, reported, and skipped for that reload;
124    /// the remaining hooks still run and the watcher thread survives. It is
125    /// not unregistered — a bug in a hook should be loud on every reload, not
126    /// once.
127    pub fn on_reload(&self, hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static) {
128        let _ = self.register(Arc::new(hook));
129    }
130
131    /// [`on_reload`](Self::on_reload), scoped: dropping the returned guard
132    /// unregisters the hook.
133    ///
134    /// For anything whose life is shorter than the process — the permanent
135    /// variant would keep a torn-down subsystem's callback firing forever.
136    #[must_use = "dropping the guard unregisters the hook; bind it for as long \
137                  as the hook should fire, or use `on_reload` for a permanent one"]
138    pub fn on_reload_scoped(
139        &'static self,
140        hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static,
141    ) -> HookGuard<T> {
142        HookGuard {
143            cell: self,
144            token: self.register(Arc::new(hook)),
145        }
146    }
147
148    fn register(&self, hook: Hook<T>) -> u64 {
149        let token = self
150            .next_token
151            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
152
153        self.hooks
154            .get_or_init(|| ArcSwap::from_pointee(Vec::new()))
155            .rcu(|current| {
156                let mut next = Vec::with_capacity(current.len() + 1);
157
158                next.extend(current.iter().cloned());
159                next.push(Registered {
160                    token,
161                    hook: Arc::clone(&hook),
162                });
163
164                next
165            });
166
167        token
168    }
169
170    fn unregister(&self, token: u64) {
171        let Some(hooks) = self.hooks.get() else {
172            return;
173        };
174
175        hooks.rcu(|current| {
176            current
177                .iter()
178                .filter(|registered| registered.token != token)
179                .cloned()
180                .collect::<Vec<_>>()
181        });
182    }
183
184    fn dispatch(&self, previous: &Arc<T>, current: &Arc<T>) {
185        let Some(hooks) = self.hooks.get() else {
186            return;
187        };
188
189        // A snapshot of the list, so a callback that registers another one does
190        // not invalidate the iteration.
191        for registered in hooks.load().iter() {
192            // Caught per hook: a panic in one must neither silence the rest
193            // nor unwind into the watcher thread and kill it — a watcher that
194            // died with a live-looking handle is the failure mode this exists
195            // to prevent. `AssertUnwindSafe` is honest here: the hook gets
196            // shared references it cannot leave half-mutated.
197            let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
198                (registered.hook)(previous, current);
199            }));
200
201            if outcome.is_err() {
202                crate::log::warning!(
203                    "a reload hook panicked; it stays registered and the \
204                     remaining hooks still run"
205                );
206            }
207        }
208    }
209
210    /// The current snapshot, or `None` if nothing has been stored yet.
211    pub fn load(&self) -> Option<Arc<T>> {
212        self.inner.get().map(ArcSwap::load_full)
213    }
214
215    /// The current snapshot, panicking if there is none.
216    ///
217    /// `type_name` is used to build the message; the generated code passes the
218    /// annotated struct's name so the panic names the type the caller wrote.
219    ///
220    /// # Panics
221    ///
222    /// If nothing has been stored yet.
223    pub fn get_or_panic(&self, type_name: &str) -> Arc<T> {
224        self.load().unwrap_or_else(|| {
225            panic!("{type_name} has not been initialized; call `{type_name}::init()` first")
226        })
227    }
228
229    /// A handle woken by every later [`store`](Self::store).
230    ///
231    /// The snapshot current at this call counts as already seen, so the first
232    /// `changed()` waits for the *next* store. Read the value you start from
233    /// with [`load`](Self::load).
234    ///
235    /// Runtime-agnostic: it is a `Future`, and any executor drives it.
236    #[cfg(feature = "async")]
237    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
238    pub fn changes(&'static self) -> crate::Changes<T>
239    where
240        T: Send + Sync,
241    {
242        crate::Changes::new(self)
243    }
244
245    #[cfg(feature = "async")]
246    pub(crate) fn notify(&self) -> &crate::asynchronous::Notify {
247        &self.notify
248    }
249}
250
251/// Unregisters its hook when dropped. From
252/// [`on_reload_scoped`](ConfigCell::on_reload_scoped).
253pub struct HookGuard<T: 'static> {
254    cell: &'static ConfigCell<T>,
255    token: u64,
256}
257
258impl<T> Drop for HookGuard<T> {
259    fn drop(&mut self) {
260        self.cell.unregister(self.token);
261    }
262}
263
264impl<T> std::fmt::Debug for HookGuard<T> {
265    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266        f.debug_struct("HookGuard")
267            .field("token", &self.token)
268            .finish_non_exhaustive()
269    }
270}
271
272impl<T> Default for ConfigCell<T> {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278impl<T: std::fmt::Debug> std::fmt::Debug for ConfigCell<T> {
279    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280        match self.load() {
281            Some(value) => f.debug_tuple("ConfigCell").field(&value).finish(),
282            None => f.write_str("ConfigCell(uninitialized)"),
283        }
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290    use std::sync::Mutex;
291    use std::thread;
292
293    #[test]
294    fn a_fresh_cell_is_empty() {
295        let cell = ConfigCell::<u16>::new();
296
297        assert!(cell.load().is_none());
298    }
299
300    #[test]
301    fn a_reader_keeps_the_generation_it_took() {
302        let cell = ConfigCell::new();
303        cell.store(String::from("first"));
304
305        let held = cell.load().unwrap();
306        cell.store(String::from("second"));
307
308        assert_eq!(*held, "first");
309        assert_eq!(*cell.load().unwrap(), "second");
310    }
311
312    #[test]
313    fn concurrent_first_writes_do_not_lose_the_cell() {
314        let cell: &'static ConfigCell<usize> = Box::leak(Box::new(ConfigCell::new()));
315
316        let writers: Vec<_> = (0..8)
317            .map(|value| thread::spawn(move || cell.store(value)))
318            .collect();
319
320        for writer in writers {
321            writer.join().unwrap();
322        }
323
324        let final_value = *cell.load().expect("some writer must have won");
325        assert!(final_value < 8);
326    }
327
328    #[test]
329    fn the_first_store_is_an_initialization_not_a_reload() {
330        let seen = Arc::new(Mutex::new(Vec::new()));
331        let cell = ConfigCell::new();
332
333        let recorder = Arc::clone(&seen);
334        cell.on_reload(move |previous, current| {
335            recorder.lock().unwrap().push((**previous, **current));
336        });
337
338        cell.store(1u16);
339        assert!(
340            seen.lock().unwrap().is_empty(),
341            "there is nothing to compare the first snapshot against"
342        );
343
344        cell.store(2u16);
345        cell.store(3u16);
346
347        assert_eq!(*seen.lock().unwrap(), [(1, 2), (2, 3)]);
348    }
349
350    #[test]
351    fn every_registered_callback_runs() {
352        let count = Arc::new(Mutex::new(0usize));
353        let cell = ConfigCell::new();
354
355        for _ in 0..3 {
356            let counter = Arc::clone(&count);
357            cell.on_reload(move |_, _| *counter.lock().unwrap() += 1);
358        }
359
360        cell.store(1u16);
361        cell.store(2u16);
362
363        assert_eq!(*count.lock().unwrap(), 3);
364    }
365
366    #[test]
367    fn a_panicking_hook_silences_neither_the_rest_nor_the_next_reload() {
368        let count = Arc::new(Mutex::new(0usize));
369        let cell = ConfigCell::new();
370
371        cell.on_reload(|_, _| panic!("a bug in somebody's hook"));
372        {
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        cell.store(3u16);
380
381        assert_eq!(
382            *count.lock().unwrap(),
383            2,
384            "the hook after the panicking one must run on every reload"
385        );
386    }
387
388    #[test]
389    fn dropping_the_guard_unregisters_the_hook() {
390        let count = Arc::new(Mutex::new(0usize));
391        let cell: &'static ConfigCell<u16> = Box::leak(Box::new(ConfigCell::new()));
392
393        cell.store(1);
394
395        let guard = {
396            let counter = Arc::clone(&count);
397            cell.on_reload_scoped(move |_, _| *counter.lock().unwrap() += 1)
398        };
399
400        cell.store(2);
401        assert_eq!(*count.lock().unwrap(), 1);
402
403        drop(guard);
404        cell.store(3);
405        assert_eq!(
406            *count.lock().unwrap(),
407            1,
408            "an unregistered hook must not fire"
409        );
410    }
411
412    #[test]
413    #[should_panic(expected = "`DbConfig::init()`")]
414    fn get_or_panic_points_at_init() {
415        ConfigCell::<u16>::new().get_or_panic("DbConfig");
416    }
417}