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!(
226                "{type_name} has no snapshot installed; configure and install \
227                 one first: `{type_name}::builder(\"..\")...init()?`"
228            )
229        })
230    }
231
232    /// A handle woken by every later [`store`](Self::store).
233    ///
234    /// The snapshot current at this call counts as already seen, so the first
235    /// `changed()` waits for the *next* store. Read the value you start from
236    /// with [`load`](Self::load).
237    ///
238    /// Runtime-agnostic: it is a `Future`, and any executor drives it.
239    #[cfg(feature = "async")]
240    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
241    pub fn changes(&'static self) -> crate::Changes<T>
242    where
243        T: Send + Sync,
244    {
245        crate::Changes::new(self)
246    }
247
248    #[cfg(feature = "async")]
249    pub(crate) fn notify(&self) -> &crate::asynchronous::Notify {
250        &self.notify
251    }
252}
253
254/// Unregisters its hook when dropped. From
255/// [`on_reload_scoped`](ConfigCell::on_reload_scoped).
256pub struct HookGuard<T: 'static> {
257    cell: &'static ConfigCell<T>,
258    token: u64,
259}
260
261impl<T> Drop for HookGuard<T> {
262    fn drop(&mut self) {
263        self.cell.unregister(self.token);
264    }
265}
266
267impl<T> std::fmt::Debug for HookGuard<T> {
268    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269        f.debug_struct("HookGuard")
270            .field("token", &self.token)
271            .finish_non_exhaustive()
272    }
273}
274
275impl<T> Default for ConfigCell<T> {
276    fn default() -> Self {
277        Self::new()
278    }
279}
280
281impl<T: std::fmt::Debug> std::fmt::Debug for ConfigCell<T> {
282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283        match self.load() {
284            Some(value) => f.debug_tuple("ConfigCell").field(&value).finish(),
285            None => f.write_str("ConfigCell(uninitialized)"),
286        }
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use std::sync::Mutex;
294    use std::thread;
295
296    #[test]
297    fn a_fresh_cell_is_empty() {
298        let cell = ConfigCell::<u16>::new();
299
300        assert!(cell.load().is_none());
301    }
302
303    #[test]
304    fn a_reader_keeps_the_generation_it_took() {
305        let cell = ConfigCell::new();
306        cell.store(String::from("first"));
307
308        let held = cell.load().unwrap();
309        cell.store(String::from("second"));
310
311        assert_eq!(*held, "first");
312        assert_eq!(*cell.load().unwrap(), "second");
313    }
314
315    #[test]
316    fn concurrent_first_writes_do_not_lose_the_cell() {
317        let cell: &'static ConfigCell<usize> = Box::leak(Box::new(ConfigCell::new()));
318
319        let writers: Vec<_> = (0..8)
320            .map(|value| thread::spawn(move || cell.store(value)))
321            .collect();
322
323        for writer in writers {
324            writer.join().unwrap();
325        }
326
327        let final_value = *cell.load().expect("some writer must have won");
328        assert!(final_value < 8);
329    }
330
331    #[test]
332    fn the_first_store_is_an_initialization_not_a_reload() {
333        let seen = Arc::new(Mutex::new(Vec::new()));
334        let cell = ConfigCell::new();
335
336        let recorder = Arc::clone(&seen);
337        cell.on_reload(move |previous, current| {
338            recorder.lock().unwrap().push((**previous, **current));
339        });
340
341        cell.store(1u16);
342        assert!(
343            seen.lock().unwrap().is_empty(),
344            "there is nothing to compare the first snapshot against"
345        );
346
347        cell.store(2u16);
348        cell.store(3u16);
349
350        assert_eq!(*seen.lock().unwrap(), [(1, 2), (2, 3)]);
351    }
352
353    #[test]
354    fn every_registered_callback_runs() {
355        let count = Arc::new(Mutex::new(0usize));
356        let cell = ConfigCell::new();
357
358        for _ in 0..3 {
359            let counter = Arc::clone(&count);
360            cell.on_reload(move |_, _| *counter.lock().unwrap() += 1);
361        }
362
363        cell.store(1u16);
364        cell.store(2u16);
365
366        assert_eq!(*count.lock().unwrap(), 3);
367    }
368
369    #[test]
370    fn a_panicking_hook_silences_neither_the_rest_nor_the_next_reload() {
371        let count = Arc::new(Mutex::new(0usize));
372        let cell = ConfigCell::new();
373
374        cell.on_reload(|_, _| panic!("a bug in somebody's hook"));
375        {
376            let counter = Arc::clone(&count);
377            cell.on_reload(move |_, _| *counter.lock().unwrap() += 1);
378        }
379
380        cell.store(1u16);
381        cell.store(2u16);
382        cell.store(3u16);
383
384        assert_eq!(
385            *count.lock().unwrap(),
386            2,
387            "the hook after the panicking one must run on every reload"
388        );
389    }
390
391    #[test]
392    fn dropping_the_guard_unregisters_the_hook() {
393        let count = Arc::new(Mutex::new(0usize));
394        let cell: &'static ConfigCell<u16> = Box::leak(Box::new(ConfigCell::new()));
395
396        cell.store(1);
397
398        let guard = {
399            let counter = Arc::clone(&count);
400            cell.on_reload_scoped(move |_, _| *counter.lock().unwrap() += 1)
401        };
402
403        cell.store(2);
404        assert_eq!(*count.lock().unwrap(), 1);
405
406        drop(guard);
407        cell.store(3);
408        assert_eq!(
409            *count.lock().unwrap(),
410            1,
411            "an unregistered hook must not fire"
412        );
413    }
414
415    #[test]
416    #[should_panic(expected = "`DbConfig::builder(")]
417    fn get_or_panic_points_at_the_builder() {
418        ConfigCell::<u16>::new().get_or_panic("DbConfig");
419    }
420}