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/// Holds the current configuration snapshot for one type.
11///
12/// `ConfigCell::new()` is `const`, so this lives in a `static` — which is how
13/// `#[dynamic_config]` emits it.
14///
15/// Reads are lock-free. [`load`](Self::load) clones an `Arc` out of an
16/// [`ArcSwap`], so a reload never blocks a request handler and a reader that
17/// already holds an `Arc` keeps observing its own generation until it drops it.
18/// Call it once per unit of work: calling it twice within one request can
19/// straddle a reload and observe two different configurations.
20///
21/// # Example
22///
23/// ```
24/// use dynamic_config::ConfigCell;
25///
26/// static PORT: ConfigCell<u16> = ConfigCell::new();
27///
28/// assert!(PORT.load().is_none());
29///
30/// PORT.store(8080);
31/// assert_eq!(*PORT.load().unwrap(), 8080);
32/// ```
33pub struct ConfigCell<T> {
34    inner: OnceLock<ArcSwap<T>>,
35
36    /// Held as a snapshot rather than behind a lock, so dispatching a reload
37    /// takes no lock a callback could deadlock against by storing again.
38    hooks: OnceLock<ArcSwap<Vec<Hook<T>>>>,
39
40    /// Generation counter and parked wakers, so async tasks can await a reload
41    /// instead of polling. No runtime involved: it is an atomic and a list.
42    #[cfg(feature = "async")]
43    notify: crate::asynchronous::Notify,
44}
45
46impl<T> ConfigCell<T> {
47    /// An empty cell.
48    #[must_use]
49    pub const fn new() -> Self {
50        Self {
51            inner: OnceLock::new(),
52            hooks: OnceLock::new(),
53            #[cfg(feature = "async")]
54            notify: crate::asynchronous::Notify::new(),
55        }
56    }
57
58    /// Atomically installs `value` as the current snapshot.
59    ///
60    /// Reload callbacks run, and with the `async` feature every waiting task is
61    /// woken. Installing the *first* snapshot is not a reload, so callbacks do
62    /// not fire for it — there is nothing to compare against.
63    pub fn store(&self, value: T) {
64        let value = Arc::new(value);
65
66        // `get_or_init` settles the race between two threads installing the
67        // very first snapshot: one initializer wins, and the `swap` below
68        // applies this call's value either way.
69        let slot = self.inner.get_or_init(|| ArcSwap::new(Arc::clone(&value)));
70        let previous = slot.swap(Arc::clone(&value));
71
72        // If `get_or_init` just installed *our* value, `previous` is the very
73        // same `Arc`, and no callbacks fire. Two `store`s racing on a cold
74        // cell can still both dispatch — the loser's swap sees the winner's
75        // value as "previous" — which is the same thing a reload arriving
76        // moments after init would do, so callbacks must tolerate it anyway.
77        if !Arc::ptr_eq(&previous, &value) {
78            self.dispatch(&previous, &value);
79        }
80
81        #[cfg(feature = "async")]
82        self.notify.bump();
83    }
84
85    /// Registers a callback for every later reload.
86    ///
87    /// The callback receives the outgoing and incoming snapshots, in that
88    /// order, and runs on whichever thread performed the reload — the watcher
89    /// thread, usually. Keep it short, and do not store again from inside one:
90    /// that recurses rather than deadlocking, which is worse.
91    ///
92    /// Callbacks cannot be removed. A reload hook that should stop firing
93    /// should check a flag of its own; the alternative is handing out
94    /// registration tokens nobody would remember to drop.
95    pub fn on_reload(&self, hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static) {
96        let hook: Hook<T> = Arc::new(hook);
97
98        self.hooks
99            .get_or_init(|| ArcSwap::from_pointee(Vec::new()))
100            .rcu(|current| {
101                let mut next = Vec::with_capacity(current.len() + 1);
102
103                next.extend(current.iter().map(Arc::clone));
104                next.push(Arc::clone(&hook));
105
106                next
107            });
108    }
109
110    fn dispatch(&self, previous: &Arc<T>, current: &Arc<T>) {
111        let Some(hooks) = self.hooks.get() else {
112            return;
113        };
114
115        // A snapshot of the list, so a callback that registers another one does
116        // not invalidate the iteration.
117        for hook in hooks.load().iter() {
118            hook(previous, current);
119        }
120    }
121
122    /// The current snapshot, or `None` if nothing has been stored yet.
123    pub fn load(&self) -> Option<Arc<T>> {
124        self.inner.get().map(ArcSwap::load_full)
125    }
126
127    /// The current snapshot, panicking if there is none.
128    ///
129    /// `type_name` is used to build the message; the generated code passes the
130    /// annotated struct's name so the panic names the type the caller wrote.
131    ///
132    /// # Panics
133    ///
134    /// If nothing has been stored yet.
135    pub fn get_or_panic(&self, type_name: &str) -> Arc<T> {
136        self.load().unwrap_or_else(|| {
137            panic!("{type_name} has not been initialized; call `{type_name}::init()` first")
138        })
139    }
140
141    /// A handle woken by every later [`store`](Self::store).
142    ///
143    /// The snapshot current at this call counts as already seen, so the first
144    /// `changed()` waits for the *next* store. Read the value you start from
145    /// with [`load`](Self::load).
146    ///
147    /// Runtime-agnostic: it is a `Future`, and any executor drives it.
148    #[cfg(feature = "async")]
149    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
150    pub fn changes(&'static self) -> crate::Changes<T>
151    where
152        T: Send + Sync,
153    {
154        crate::Changes::new(self)
155    }
156
157    #[cfg(feature = "async")]
158    pub(crate) fn notify(&self) -> &crate::asynchronous::Notify {
159        &self.notify
160    }
161}
162
163impl<T> Default for ConfigCell<T> {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169impl<T: std::fmt::Debug> std::fmt::Debug for ConfigCell<T> {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        match self.load() {
172            Some(value) => f.debug_tuple("ConfigCell").field(&value).finish(),
173            None => f.write_str("ConfigCell(uninitialized)"),
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use std::sync::Mutex;
182    use std::thread;
183
184    #[test]
185    fn a_fresh_cell_is_empty() {
186        let cell = ConfigCell::<u16>::new();
187
188        assert!(cell.load().is_none());
189    }
190
191    #[test]
192    fn a_reader_keeps_the_generation_it_took() {
193        let cell = ConfigCell::new();
194        cell.store(String::from("first"));
195
196        let held = cell.load().unwrap();
197        cell.store(String::from("second"));
198
199        assert_eq!(*held, "first");
200        assert_eq!(*cell.load().unwrap(), "second");
201    }
202
203    #[test]
204    fn concurrent_first_writes_do_not_lose_the_cell() {
205        let cell: &'static ConfigCell<usize> = Box::leak(Box::new(ConfigCell::new()));
206
207        let writers: Vec<_> = (0..8)
208            .map(|value| thread::spawn(move || cell.store(value)))
209            .collect();
210
211        for writer in writers {
212            writer.join().unwrap();
213        }
214
215        let final_value = *cell.load().expect("some writer must have won");
216        assert!(final_value < 8);
217    }
218
219    #[test]
220    fn the_first_store_is_an_initialization_not_a_reload() {
221        let seen = Arc::new(Mutex::new(Vec::new()));
222        let cell = ConfigCell::new();
223
224        let recorder = Arc::clone(&seen);
225        cell.on_reload(move |previous, current| {
226            recorder.lock().unwrap().push((**previous, **current));
227        });
228
229        cell.store(1u16);
230        assert!(
231            seen.lock().unwrap().is_empty(),
232            "there is nothing to compare the first snapshot against"
233        );
234
235        cell.store(2u16);
236        cell.store(3u16);
237
238        assert_eq!(*seen.lock().unwrap(), [(1, 2), (2, 3)]);
239    }
240
241    #[test]
242    fn every_registered_callback_runs() {
243        let count = Arc::new(Mutex::new(0usize));
244        let cell = ConfigCell::new();
245
246        for _ in 0..3 {
247            let counter = Arc::clone(&count);
248            cell.on_reload(move |_, _| *counter.lock().unwrap() += 1);
249        }
250
251        cell.store(1u16);
252        cell.store(2u16);
253
254        assert_eq!(*count.lock().unwrap(), 3);
255    }
256
257    #[test]
258    #[should_panic(expected = "`DbConfig::init()`")]
259    fn get_or_panic_points_at_init() {
260        ConfigCell::<u16>::new().get_or_panic("DbConfig");
261    }
262}