Skip to main content

dynamic_config/
cell.rs

1//! Process-wide storage for one configuration snapshot.
2
3use std::sync::atomic::{AtomicU32, Ordering};
4use std::sync::{Arc, OnceLock};
5use std::time::Instant;
6
7use arc_swap::{ArcSwap, ArcSwapOption};
8
9use crate::error::Error;
10use crate::reload::{ConfigStatus, FailureStatus, ReloadEvent, ReloadReason};
11
12/// A callback run after a reload, with the outgoing and incoming snapshots.
13type Hook<T> = Arc<dyn Fn(&Arc<T>, &Arc<T>) + Send + Sync>;
14
15/// A callback run after every install, with the whole event.
16type EventHook<T> = Arc<dyn Fn(&ReloadEvent<T>) + Send + Sync>;
17
18/// The two hook shapes, in one list.
19///
20/// One list rather than two, so there is one dispatch loop and the two
21/// forms cannot drift on panic isolation, ordering or what counts as an
22/// install. The forms differ in exactly one observable way, and it is a
23/// property of their *signatures*: the pair form has nowhere to put "there
24/// was no previous snapshot", so it does not fire for the first install.
25enum Callback<T> {
26    /// [`ConfigCell::on_reload`]: `(previous, current)`, reloads only.
27    Pair(Hook<T>),
28    /// [`ConfigCell::on_reload_with`]: the whole event, first install
29    /// included.
30    Event(EventHook<T>),
31}
32
33impl<T> Clone for Callback<T> {
34    fn clone(&self) -> Self {
35        match self {
36            Self::Pair(hook) => Self::Pair(Arc::clone(hook)),
37            Self::Event(hook) => Self::Event(Arc::clone(hook)),
38        }
39    }
40}
41
42/// What is true of the snapshot currently installed.
43///
44/// The operator's questions — *which generation is live, how stale is it* —
45/// answered without the program having to record anything itself. Read it
46/// through [`ConfigCell::meta`] or [`Dynamic::meta`](crate::Dynamic::meta).
47/// (Re-exported at the crate root as `dynamic_config::SnapshotMeta`.)
48///
49/// **For operators, not for correctness.** Metadata deliberately does not
50/// live on the read path: [`load`](ConfigCell::load) is one atomic load and
51/// stays that way, so the value and its metadata are two loads and a reload
52/// landing between them leaves the pair one install apart. Code that needs
53/// the value and its generation to agree should carry a generation *inside*
54/// the configuration type.
55///
56/// [`Instant`] rather than a wall clock, because the question this answers
57/// is "how long ago", which is a duration — and a wall clock can go
58/// backwards under NTP while a staleness check must not.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60#[non_exhaustive]
61pub struct SnapshotMeta {
62    /// Installs since the process started. Monotonic; zero before the first.
63    pub generation: u64,
64    /// When this snapshot was installed.
65    pub loaded_at: Instant,
66}
67
68/// One registered hook: the callback plus the token that identifies it for
69/// removal. Permanent hooks get a token too — it is cheaper than two list
70/// types, and nothing ever asks to remove them.
71struct Registered<T> {
72    token: u64,
73    callback: Callback<T>,
74}
75
76impl<T> Clone for Registered<T> {
77    fn clone(&self) -> Self {
78        Self {
79            token: self.token,
80            callback: self.callback.clone(),
81        }
82    }
83}
84
85/// Holds the current configuration snapshot for one type.
86///
87/// `ConfigCell::new()` is `const`, so this lives in a `static` — which is how
88/// `#[dynamic_config]` emits it.
89///
90/// Reads are lock-free. [`load`](Self::load) clones an `Arc` out of an
91/// [`ArcSwap`], so a reload never blocks a request handler and a reader that
92/// already holds an `Arc` keeps observing its own generation until it drops it.
93/// Call it once per unit of work: calling it twice within one request can
94/// straddle a reload and observe two different configurations.
95///
96/// # Example
97///
98/// ```
99/// use dynamic_config::ConfigCell;
100///
101/// static PORT: ConfigCell<u16> = ConfigCell::new();
102///
103/// assert!(PORT.load().is_none());
104///
105/// PORT.store(8080);
106/// assert_eq!(*PORT.load().unwrap(), 8080);
107/// ```
108pub struct ConfigCell<T> {
109    inner: OnceLock<ArcSwap<T>>,
110
111    /// Held as a snapshot rather than behind a lock, so dispatching a reload
112    /// takes no lock a callback could deadlock against by storing again.
113    hooks: OnceLock<ArcSwap<Vec<Registered<T>>>>,
114
115    /// Hands out hook tokens. Plain counter: 2^64 registrations outlives the
116    /// process by some margin.
117    next_token: std::sync::atomic::AtomicU64,
118
119    /// The generation and install time of what `inner` holds, in a slot of
120    /// its own so that reading configuration stays one atomic load with
121    /// nothing to project out of it. Written *after* the value swap, and
122    /// allocated by the same compare-and-swap that publishes it, so the
123    /// number an observer sees never goes backwards even when two stores
124    /// overlap.
125    meta: ArcSwapOption<SnapshotMeta>,
126
127    /// Why the installed snapshot was installed. Beside `meta` rather than
128    /// inside it: a reason owns a `PathBuf`, and `SnapshotMeta` is `Copy`
129    /// precisely so reading it allocates nothing.
130    last_reason: ArcSwapOption<ReloadReason>,
131
132    /// The last reload that installed nothing, and how many have failed
133    /// since one did. Outside the snapshot because a failed reload has no
134    /// snapshot to hang off — that is what makes it a failure.
135    last_failure: ArcSwapOption<FailureStatus>,
136    consecutive_failures: AtomicU32,
137
138    /// Generation counter and parked wakers, so async tasks can await a reload
139    /// instead of polling. No runtime involved: it is an atomic and a list.
140    #[cfg(feature = "async")]
141    notify: crate::asynchronous::Notify,
142}
143
144impl<T> ConfigCell<T> {
145    /// An empty cell.
146    #[must_use]
147    #[cfg(not(loom))]
148    pub const fn new() -> Self {
149        Self {
150            inner: OnceLock::new(),
151            hooks: OnceLock::new(),
152            next_token: std::sync::atomic::AtomicU64::new(0),
153            meta: ArcSwapOption::const_empty(),
154            last_reason: ArcSwapOption::const_empty(),
155            last_failure: ArcSwapOption::const_empty(),
156            consecutive_failures: AtomicU32::new(0),
157            #[cfg(feature = "async")]
158            notify: crate::asynchronous::Notify::new(),
159        }
160    }
161
162    /// The same, minus `const`: loom's constructors are not.
163    #[must_use]
164    #[cfg(loom)]
165    pub fn new() -> Self {
166        Self {
167            inner: OnceLock::new(),
168            hooks: OnceLock::new(),
169            next_token: std::sync::atomic::AtomicU64::new(0),
170            meta: ArcSwapOption::const_empty(),
171            last_reason: ArcSwapOption::const_empty(),
172            last_failure: ArcSwapOption::const_empty(),
173            consecutive_failures: AtomicU32::new(0),
174            #[cfg(feature = "async")]
175            notify: crate::asynchronous::Notify::new(),
176        }
177    }
178
179    /// Atomically installs `value` as the current snapshot.
180    ///
181    /// Reload callbacks run, and with the `async` feature every waiting task is
182    /// woken. Installing the *first* snapshot is not a reload, so
183    /// [`on_reload`](Self::on_reload) callbacks do not fire for it — there is
184    /// nothing to compare against. [`on_reload_with`](Self::on_reload_with)
185    /// does fire, with `previous: None`, which is the difference between
186    /// having somewhere to say that and not.
187    ///
188    /// The install is recorded as [`ReloadReason::Manual`]: something in the
189    /// program stored it. Use [`store_with`](Self::store_with) where more is
190    /// known.
191    pub fn store(&self, value: T) {
192        self.store_with(value, ReloadReason::Manual);
193    }
194
195    /// [`store`](Self::store), stating why — and handing back what it
196    /// installed.
197    ///
198    /// The reason travels from the call site that knows it — the watcher
199    /// knows the file, `init` knows it is the first — to the hooks and to
200    /// [`status`](Self::status). Nothing downstream can reconstruct it: by
201    /// the time a hook runs, every install is the same swap.
202    ///
203    /// The returned `Arc` is **this call's** snapshot, not whatever is
204    /// current when it returns: a reload landing a moment later would make
205    /// a following [`load`](Self::load) answer differently, and the caller
206    /// that installed a configuration means the one it installed. It costs
207    /// nothing — the `Arc` was allocated here anyway — and ignoring it is
208    /// the ordinary case.
209    // Not a `#[must_use]`: every call site in the crate before this one
210    // discarded it, and a warning on `cell.store_with(v, reason);` would say
211    // "you forgot something" about the normal way to use it.
212    #[allow(clippy::must_use_candidate)]
213    pub fn store_with(&self, value: T, reason: ReloadReason) -> Arc<T> {
214        let value = Arc::new(value);
215
216        // `get_or_init` settles the race between two threads installing the
217        // very first snapshot: one initializer wins, and the `swap` below
218        // applies this call's value either way.
219        let slot = self.inner.get_or_init(|| ArcSwap::new(Arc::clone(&value)));
220        let previous = slot.swap(Arc::clone(&value));
221
222        // After the swap, so `meta()` never describes an install a reader
223        // cannot see yet — the pair can lag, never lead. The generation is
224        // allocated inside the compare-and-swap rather than from a counter
225        // read beforehand: two overlapping stores would otherwise be free to
226        // publish their numbers in the opposite order, and a generation that
227        // goes backwards is worse than one that is merely coarse.
228        //
229        // The winning closure call is the last one, so what it leaves in
230        // `installed` is this install's own metadata rather than a
231        // neighbour's — which a `load()` afterwards could not promise.
232        let mut installed = None;
233
234        self.meta.rcu(|before| {
235            let meta = Arc::new(SnapshotMeta {
236                generation: before.as_ref().map_or(0, |meta| meta.generation) + 1,
237                loaded_at: Instant::now(),
238            });
239
240            installed = Some(*meta);
241
242            meta
243        });
244
245        let meta = installed.expect("`rcu` runs its closure at least once");
246
247        // Published after the metadata for the same reason the metadata is
248        // published after the value: a reader crossing the gap must see a
249        // reason that is stale, never one for an install it cannot see.
250        self.last_reason.store(Some(Arc::new(reason.clone())));
251
252        // An install is the only kind of success there is — a load that
253        // installs nothing is not a reload — so this is where the failure
254        // streak ends. `last_failure` stays: it is history, and the counter
255        // is the health.
256        self.consecutive_failures.store(0, Ordering::Relaxed);
257
258        // If `get_or_init` just installed *our* value, `previous` is the very
259        // same `Arc`, and this is the first install: pair-form callbacks do
260        // not fire. Two `store`s racing on a cold cell can still both
261        // dispatch — the loser's swap sees the winner's value as "previous"
262        // — which is the same thing a reload arriving moments after init
263        // would do, so callbacks must tolerate it anyway.
264        // Waiters are woken *before* the hooks run: a task awaiting
265        // `changes()` wants the new snapshot, which is already installed, and
266        // making it wait out every hook would hand one slow callback the power
267        // to delay every async reader.
268        #[cfg(feature = "async")]
269        self.notify.bump();
270
271        let previous = if Arc::ptr_eq(&previous, &value) {
272            None
273        } else {
274            Some(previous)
275        };
276
277        // Entered across the dispatch, so anything a reload hook logs is
278        // attributed to the reload that ran it. Nothing at all without the
279        // feature — not even a stderr line, which every install is far too
280        // many of.
281        #[cfg(feature = "tracing")]
282        let _span = crate::telemetry::installed::<T>(&reason, meta.generation);
283
284        // By reference, so returning the snapshot below costs no refcount
285        // traffic: `dispatch` clones only after it knows a hook is there to
286        // receive an event, which is what it did before this returned
287        // anything.
288        self.dispatch(previous, &value, reason, meta);
289
290        value
291    }
292
293    /// Records a reload that installed nothing.
294    ///
295    /// Called by whatever decided not to install — a load that failed, a
296    /// validation that refused — so that [`status`](Self::status) can answer
297    /// *did the last attempt work* and *how many have failed since one did*.
298    /// The next successful install resets the streak.
299    ///
300    /// Only the failure's category and key path are kept; see
301    /// [`FailureStatus`].
302    pub fn record_failure(&self, error: &Error) {
303        // Saturating rather than wrapping: a counter that rolls over to zero
304        // reads as "healthy" at the worst possible moment. Four billion
305        // consecutive failures is already the alert.
306        //
307        // Spelled as a compare-exchange loop rather than `fetch_update`,
308        // which nightly has deprecated in favour of `try_update` — a name
309        // that does not exist at this crate's 1.71 floor. The loop is what
310        // `fetch_update` does, and it compiles everywhere.
311        let mut count = self.consecutive_failures.load(Ordering::Relaxed);
312
313        while count < u32::MAX {
314            match self.consecutive_failures.compare_exchange_weak(
315                count,
316                count + 1,
317                Ordering::Relaxed,
318                Ordering::Relaxed,
319            ) {
320                Ok(_) => break,
321                Err(actual) => count = actual,
322            }
323        }
324
325        self.last_failure
326            .store(Some(Arc::new(FailureStatus::of(error))));
327
328        // The category and the key path, never the value: see `telemetry`.
329        #[cfg(feature = "tracing")]
330        crate::telemetry::refused::<T>(error);
331    }
332
333    /// What is true of this configuration right now.
334    ///
335    /// A handful of atomic loads and no I/O — nothing is re-read, nothing
336    /// is recomputed — so an exporter can call it per scrape. See
337    /// [`ConfigStatus`] for what it deliberately does not carry.
338    #[must_use]
339    pub fn status(&self) -> ConfigStatus {
340        let meta = self.meta();
341
342        ConfigStatus {
343            generation: meta.map_or(0, |meta| meta.generation),
344            loaded_at: meta.map(|meta| meta.loaded_at),
345            last_reason: self.last_reason.load_full().map(|reason| (*reason).clone()),
346            last_failure: self
347                .last_failure
348                .load_full()
349                .map(|failure| (*failure).clone()),
350            consecutive_failures: self.consecutive_failures.load(Ordering::Relaxed),
351        }
352    }
353
354    /// Registers a callback for every later reload.
355    ///
356    /// The callback receives the outgoing and incoming snapshots, in that
357    /// order, and runs on whichever thread performed the reload — the watcher
358    /// thread, usually. Keep it short, and do not store again from inside one:
359    /// that recurses rather than deadlocking, which is worse.
360    ///
361    /// Callbacks registered this way cannot be removed — a hook for the life
362    /// of the process, which is what a server wants. Anything with a shorter
363    /// life — a test, a plugin, a subsystem that can be torn down — should
364    /// use [`on_reload_scoped`](Self::on_reload_scoped) and hold the guard.
365    ///
366    /// A hook that panics is caught, reported, and skipped for that reload;
367    /// the remaining hooks still run and the watcher thread survives. It is
368    /// not unregistered — a bug in a hook should be loud on every reload, not
369    /// once.
370    ///
371    /// # Concurrent reloads
372    ///
373    /// Each call sees a consistent `(previous, current)` pair: both were
374    /// installed, and `current` was installed after `previous`.
375    ///
376    /// The *order of calls* is not defined when two reloads overlap. Two
377    /// hooks may observe the same pair, and one hook may see `(A, B)` after
378    /// another saw `(B, C)`. A hook that needs a total order should read
379    /// [`generation`](Self::generation) — which is monotonic — rather than
380    /// infer one from its arguments.
381    ///
382    /// Reloads are deliberately not serialised against each other. The same
383    /// store that dispatches these hooks wakes async waiters *before* running
384    /// them, so that one slow callback cannot delay every reader; a lock held
385    /// across user callbacks would undo that on purpose, and a hook that
386    /// blocked would then block reloads.
387    pub fn on_reload(&self, hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static) {
388        let _ = self.register(Callback::Pair(Arc::new(hook)));
389    }
390
391    /// [`on_reload`](Self::on_reload), told *why*.
392    ///
393    /// The callback receives a [`ReloadEvent`]: both snapshots, the
394    /// [`ReloadReason`], and the [`SnapshotMeta`] of the install. Everything
395    /// the pair form promises holds here — same list, same order of
396    /// registration, same panic isolation, same absence of an order across
397    /// overlapping reloads — with one difference the pair form's signature
398    /// makes impossible: **this fires for the first install too**, with
399    /// `previous: None`. A hook that only wants reloads matches on that, or
400    /// registers through [`on_reload`](Self::on_reload).
401    ///
402    /// ```
403    /// use dynamic_config::{ConfigCell, ReloadReason};
404    ///
405    /// static PORT: ConfigCell<u16> = ConfigCell::new();
406    ///
407    /// PORT.on_reload_with(|event| {
408    ///     if let ReloadReason::FileChanged(path) = &event.reason {
409    ///         println!("generation {} came from {}", event.meta.generation, path.display());
410    ///     }
411    /// });
412    /// ```
413    pub fn on_reload_with(&self, hook: impl Fn(&ReloadEvent<T>) + Send + Sync + 'static) {
414        let _ = self.register(Callback::Event(Arc::new(hook)));
415    }
416
417    /// [`on_reload`](Self::on_reload), scoped: dropping the returned guard
418    /// unregisters the hook.
419    ///
420    /// For anything whose life is shorter than the process — the permanent
421    /// variant would keep a torn-down subsystem's callback firing forever.
422    ///
423    /// The same concurrency contract as [`on_reload`](Self::on_reload): a
424    /// consistent pair every call, in no defined order across overlapping
425    /// reloads.
426    #[must_use = "dropping the guard unregisters the hook; bind it for as long \
427                  as the hook should fire, or use `on_reload` for a permanent one"]
428    pub fn on_reload_scoped(
429        &'static self,
430        hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static,
431    ) -> HookGuard<T> {
432        HookGuard {
433            token: self.register(Callback::Pair(Arc::new(hook))),
434            cell: GuardCell::Static(self),
435        }
436    }
437
438    /// [`on_reload_with`](Self::on_reload_with), scoped: dropping the
439    /// returned guard unregisters the hook.
440    #[must_use = "dropping the guard unregisters the hook; bind it for as long \
441                  as the hook should fire, or use `on_reload_with` for a \
442                  permanent one"]
443    pub fn on_reload_with_scoped(
444        &'static self,
445        hook: impl Fn(&ReloadEvent<T>) + Send + Sync + 'static,
446    ) -> HookGuard<T> {
447        HookGuard {
448            token: self.register(Callback::Event(Arc::new(hook))),
449            cell: GuardCell::Static(self),
450        }
451    }
452
453    /// The scoped hook over an instance's shared cell; what
454    /// [`Dynamic::on_reload_scoped`](crate::Dynamic::on_reload_scoped)
455    /// hands out — the guard co-owns the cell, so it outliving the
456    /// `Dynamic` is safe rather than subtle.
457    pub(crate) fn on_reload_scoped_shared(
458        cell: &Arc<Self>,
459        hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static,
460    ) -> HookGuard<T> {
461        HookGuard {
462            token: cell.register(Callback::Pair(Arc::new(hook))),
463            cell: GuardCell::Shared(Arc::clone(cell)),
464        }
465    }
466
467    /// [`on_reload_scoped_shared`](Self::on_reload_scoped_shared), event
468    /// form; what [`Dynamic::on_reload_with_scoped`] hands out.
469    ///
470    /// [`Dynamic::on_reload_with_scoped`]: crate::Dynamic::on_reload_with_scoped
471    pub(crate) fn on_reload_with_scoped_shared(
472        cell: &Arc<Self>,
473        hook: impl Fn(&ReloadEvent<T>) + Send + Sync + 'static,
474    ) -> HookGuard<T> {
475        HookGuard {
476            token: cell.register(Callback::Event(Arc::new(hook))),
477            cell: GuardCell::Shared(Arc::clone(cell)),
478        }
479    }
480
481    fn register(&self, callback: Callback<T>) -> u64 {
482        let token = self
483            .next_token
484            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
485
486        self.hooks
487            .get_or_init(|| ArcSwap::from_pointee(Vec::new()))
488            .rcu(|current| {
489                let mut next = Vec::with_capacity(current.len() + 1);
490
491                next.extend(current.iter().cloned());
492                next.push(Registered {
493                    token,
494                    callback: callback.clone(),
495                });
496
497                next
498            });
499
500        token
501    }
502
503    fn unregister(&self, token: u64) {
504        let Some(hooks) = self.hooks.get() else {
505            return;
506        };
507
508        hooks.rcu(|current| {
509            current
510                .iter()
511                .filter(|registered| registered.token != token)
512                .cloned()
513                .collect::<Vec<_>>()
514        });
515    }
516
517    /// Runs every registered callback for one install.
518    ///
519    /// `previous` is `None` when this install is the first — see
520    /// [`store_with`](Self::store_with).
521    fn dispatch(
522        &self,
523        previous: Option<Arc<T>>,
524        current: &Arc<T>,
525        reason: ReloadReason,
526        meta: SnapshotMeta,
527    ) {
528        let Some(hooks) = self.hooks.get() else {
529            return;
530        };
531
532        // A snapshot of the list, so a callback that registers another one does
533        // not invalidate the iteration.
534        let hooks = hooks.load();
535
536        if hooks.is_empty() {
537            return;
538        }
539
540        // Built once, after the list is known to be non-empty: an event
541        // clones two `Arc`s and a reason's `PathBuf`, and a cell with no
542        // hooks — the common case — must not pay for that on every install.
543        let event = ReloadEvent::new(previous, Arc::clone(current), reason, meta);
544
545        for registered in hooks.iter() {
546            // Caught per hook: a panic in one must neither silence the rest
547            // nor unwind into the watcher thread and kill it — a watcher that
548            // died with a live-looking handle is the failure mode this exists
549            // to prevent. `AssertUnwindSafe` is honest here: the hook gets
550            // shared references it cannot leave half-mutated.
551            let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
552                match &registered.callback {
553                    // The pair form has nowhere to put "there was none", so
554                    // it does not fire for the first install; that contract
555                    // predates the event form and does not move for it.
556                    Callback::Pair(hook) => {
557                        if let Some(previous) = &event.previous {
558                            hook(previous, &event.current);
559                        }
560                    }
561                    Callback::Event(hook) => hook(&event),
562                }
563            }));
564
565            if outcome.is_err() {
566                crate::log::warning!(
567                    "a reload hook panicked; it stays registered and the \
568                     remaining hooks still run"
569                );
570            }
571        }
572    }
573
574    /// The current snapshot, or `None` if nothing has been stored yet.
575    pub fn load(&self) -> Option<Arc<T>> {
576        self.inner.get().map(ArcSwap::load_full)
577    }
578
579    /// Installs since the process started; zero before the first.
580    ///
581    /// Monotonic, so it is the number a reload hook should read when it
582    /// needs a total order — [`on_reload`](Self::on_reload) does not define
583    /// one across overlapping reloads.
584    #[must_use]
585    pub fn generation(&self) -> u64 {
586        self.meta.load().as_ref().map_or(0, |meta| meta.generation)
587    }
588
589    /// What is true of the installed snapshot, or `None` before the first.
590    ///
591    /// A load of its own: [`load`](Self::load) is untouched by this and
592    /// stays one atomic load, which means the value and its metadata can be
593    /// one install apart. See `SnapshotMeta`.
594    #[must_use]
595    pub fn meta(&self) -> Option<SnapshotMeta> {
596        self.meta.load().as_deref().copied()
597    }
598
599    /// The current snapshot, panicking if there is none.
600    ///
601    /// `type_name` is used to build the message; the generated code passes the
602    /// annotated struct's name so the panic names the type the caller wrote.
603    ///
604    /// # Panics
605    ///
606    /// If nothing has been stored yet.
607    pub fn get_or_panic(&self, type_name: &str) -> Arc<T> {
608        self.load().unwrap_or_else(|| {
609            panic!(
610                "{type_name} has no snapshot installed; configure and install \
611                 one first: `{type_name}::builder(\"..\")...init()?`"
612            )
613        })
614    }
615
616    /// A handle woken by every later [`store`](Self::store).
617    ///
618    /// The snapshot current at this call counts as already seen, so the first
619    /// `changed()` waits for the *next* store. Read the value you start from
620    /// with [`load`](Self::load).
621    ///
622    /// Runtime-agnostic: it is a `Future`, and any executor drives it.
623    #[cfg(feature = "async")]
624    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
625    pub fn changes(&'static self) -> crate::Changes<T>
626    where
627        T: Send + Sync,
628    {
629        crate::Changes::new(self)
630    }
631
632    #[cfg(feature = "async")]
633    pub(crate) fn notify(&self) -> &crate::asynchronous::Notify {
634        &self.notify
635    }
636}
637
638/// Unregisters its hook when dropped. From
639/// [`on_reload_scoped`](ConfigCell::on_reload_scoped).
640///
641/// `#[must_use]` on the *type* rather than only on the methods that hand one
642/// out: a guard is the whole registration, and every producer — the cell's
643/// four, [`Dynamic`](crate::Dynamic)'s two, the generated two — has the same
644/// silent failure when the result is dropped at the end of the statement.
645/// Marking the type is the one place that covers a producer nobody has
646/// written yet.
647#[must_use = "dropping the guard unregisters the hook immediately; bind it for \
648              as long as the hook should fire, or register a permanent hook \
649              with `on_reload`"]
650pub struct HookGuard<T: 'static> {
651    cell: GuardCell<T>,
652    token: u64,
653}
654
655/// The cell a guard unregisters from: a type's `static`, or an instance's
656/// own — the same two shapes `Changes` distinguishes, for the same reason.
657enum GuardCell<T: 'static> {
658    Static(&'static ConfigCell<T>),
659    Shared(Arc<ConfigCell<T>>),
660}
661
662impl<T> Drop for HookGuard<T> {
663    fn drop(&mut self) {
664        match &self.cell {
665            GuardCell::Static(cell) => cell.unregister(self.token),
666            GuardCell::Shared(cell) => cell.unregister(self.token),
667        }
668    }
669}
670
671impl<T> std::fmt::Debug for HookGuard<T> {
672    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
673        f.debug_struct("HookGuard")
674            .field("token", &self.token)
675            .finish_non_exhaustive()
676    }
677}
678
679impl<T> Default for ConfigCell<T> {
680    fn default() -> Self {
681        Self::new()
682    }
683}
684
685impl<T: std::fmt::Debug> std::fmt::Debug for ConfigCell<T> {
686    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
687        match self.load() {
688            Some(value) => f.debug_tuple("ConfigCell").field(&value).finish(),
689            None => f.write_str("ConfigCell(uninitialized)"),
690        }
691    }
692}
693
694#[cfg(test)]
695mod tests {
696    use super::*;
697    use std::sync::Mutex;
698    use std::thread;
699
700    #[test]
701    fn a_fresh_cell_is_empty() {
702        let cell = ConfigCell::<u16>::new();
703
704        assert!(cell.load().is_none());
705    }
706
707    #[test]
708    fn a_reader_keeps_the_generation_it_took() {
709        let cell = ConfigCell::new();
710        cell.store(String::from("first"));
711
712        let held = cell.load().unwrap();
713        cell.store(String::from("second"));
714
715        assert_eq!(*held, "first");
716        assert_eq!(*cell.load().unwrap(), "second");
717    }
718
719    #[test]
720    fn concurrent_first_writes_do_not_lose_the_cell() {
721        let cell: &'static ConfigCell<usize> = Box::leak(Box::new(ConfigCell::new()));
722
723        let writers: Vec<_> = (0..8)
724            .map(|value| thread::spawn(move || cell.store(value)))
725            .collect();
726
727        for writer in writers {
728            writer.join().unwrap();
729        }
730
731        let final_value = *cell.load().expect("some writer must have won");
732        assert!(final_value < 8);
733    }
734
735    #[test]
736    fn the_first_store_is_an_initialization_not_a_reload() {
737        let seen = Arc::new(Mutex::new(Vec::new()));
738        let cell = ConfigCell::new();
739
740        let recorder = Arc::clone(&seen);
741        cell.on_reload(move |previous, current| {
742            recorder.lock().unwrap().push((**previous, **current));
743        });
744
745        cell.store(1u16);
746        assert!(
747            seen.lock().unwrap().is_empty(),
748            "there is nothing to compare the first snapshot against"
749        );
750
751        cell.store(2u16);
752        cell.store(3u16);
753
754        assert_eq!(*seen.lock().unwrap(), [(1, 2), (2, 3)]);
755    }
756
757    #[test]
758    fn every_registered_callback_runs() {
759        let count = Arc::new(Mutex::new(0usize));
760        let cell = ConfigCell::new();
761
762        for _ in 0..3 {
763            let counter = Arc::clone(&count);
764            cell.on_reload(move |_, _| *counter.lock().unwrap() += 1);
765        }
766
767        cell.store(1u16);
768        cell.store(2u16);
769
770        assert_eq!(*count.lock().unwrap(), 3);
771    }
772
773    #[test]
774    fn a_panicking_hook_silences_neither_the_rest_nor_the_next_reload() {
775        let count = Arc::new(Mutex::new(0usize));
776        let cell = ConfigCell::new();
777
778        cell.on_reload(|_, _| panic!("a bug in somebody's hook"));
779        {
780            let counter = Arc::clone(&count);
781            cell.on_reload(move |_, _| *counter.lock().unwrap() += 1);
782        }
783
784        cell.store(1u16);
785        cell.store(2u16);
786        cell.store(3u16);
787
788        assert_eq!(
789            *count.lock().unwrap(),
790            2,
791            "the hook after the panicking one must run on every reload"
792        );
793    }
794
795    #[test]
796    fn dropping_the_guard_unregisters_the_hook() {
797        let count = Arc::new(Mutex::new(0usize));
798        let cell: &'static ConfigCell<u16> = Box::leak(Box::new(ConfigCell::new()));
799
800        cell.store(1);
801
802        let guard = {
803            let counter = Arc::clone(&count);
804            cell.on_reload_scoped(move |_, _| *counter.lock().unwrap() += 1)
805        };
806
807        cell.store(2);
808        assert_eq!(*count.lock().unwrap(), 1);
809
810        drop(guard);
811        cell.store(3);
812        assert_eq!(
813            *count.lock().unwrap(),
814            1,
815            "an unregistered hook must not fire"
816        );
817    }
818
819    /// What `store_with` hands back is the snapshot it installed, and it
820    /// stays that one: a later store moves `load()` and not the `Arc` an
821    /// earlier caller is holding. This is what makes `init_and_current`
822    /// answer for its own install rather than for whichever reload won a
823    /// race with it.
824    #[test]
825    fn store_with_hands_back_the_snapshot_it_installed() {
826        let cell = ConfigCell::new();
827
828        let first = cell.store_with(1u16, ReloadReason::Initial);
829        assert!(Arc::ptr_eq(&first, &cell.load().unwrap()));
830
831        let second = cell.store_with(2u16, ReloadReason::Manual);
832
833        assert_eq!(*first, 1, "the earlier install's snapshot is unmoved");
834        assert_eq!(*second, 2);
835        assert!(Arc::ptr_eq(&second, &cell.load().unwrap()));
836    }
837
838    #[test]
839    #[should_panic(expected = "`DbConfig::builder(")]
840    fn get_or_panic_points_at_the_builder() {
841        ConfigCell::<u16>::new().get_or_panic("DbConfig");
842    }
843
844    /// The difference between the two forms, in one test: the pair form has
845    /// nowhere to say "there was none", so it stays silent for the first
846    /// install; the event form says it with `previous: None`.
847    #[test]
848    fn the_event_form_sees_the_first_install_and_the_pair_form_does_not() {
849        let pairs = Arc::new(Mutex::new(0usize));
850        let events = Arc::new(Mutex::new(Vec::new()));
851        let cell = ConfigCell::new();
852
853        {
854            let counter = Arc::clone(&pairs);
855            cell.on_reload(move |_, _| *counter.lock().unwrap() += 1);
856        }
857        {
858            let recorder = Arc::clone(&events);
859            cell.on_reload_with(move |event| {
860                recorder
861                    .lock()
862                    .unwrap()
863                    .push((event.previous.as_deref().copied(), *event.current));
864            });
865        }
866
867        cell.store(1u16);
868        assert_eq!(*pairs.lock().unwrap(), 0);
869        assert_eq!(*events.lock().unwrap(), [(None, 1)]);
870
871        cell.store(2u16);
872        assert_eq!(*pairs.lock().unwrap(), 1);
873        assert_eq!(*events.lock().unwrap(), [(None, 1), (Some(1), 2)]);
874    }
875
876    /// The event carries the install it belongs to, not whatever the cell
877    /// happens to hold by the time a hook reads it.
878    #[test]
879    fn an_event_carries_the_reason_and_the_generation_of_its_own_install() {
880        let seen = Arc::new(Mutex::new(Vec::new()));
881        let cell = ConfigCell::new();
882
883        {
884            let recorder = Arc::clone(&seen);
885            cell.on_reload_with(move |event| {
886                recorder
887                    .lock()
888                    .unwrap()
889                    .push((event.reason.clone(), event.meta.generation));
890            });
891        }
892
893        cell.store_with(1u16, ReloadReason::Initial);
894        cell.store_with(2u16, ReloadReason::RemoteChanged);
895        cell.store(3u16);
896
897        assert_eq!(
898            *seen.lock().unwrap(),
899            [
900                (ReloadReason::Initial, 1),
901                (ReloadReason::RemoteChanged, 2),
902                (ReloadReason::Manual, 3),
903            ]
904        );
905    }
906
907    /// A panicking event hook is isolated exactly like a panicking pair
908    /// hook — one list, one dispatch loop, one `catch_unwind`.
909    #[test]
910    fn a_panicking_event_hook_leaves_the_rest_running() {
911        let count = Arc::new(Mutex::new(0usize));
912        let cell = ConfigCell::new();
913
914        cell.on_reload_with(|_| panic!("a bug in somebody's hook"));
915        {
916            let counter = Arc::clone(&count);
917            cell.on_reload_with(move |_| *counter.lock().unwrap() += 1);
918        }
919
920        cell.store(1u16);
921        cell.store(2u16);
922
923        assert_eq!(*count.lock().unwrap(), 2);
924    }
925
926    #[test]
927    fn a_scoped_event_hook_stops_when_its_guard_drops() {
928        let count = Arc::new(Mutex::new(0usize));
929        let cell: &'static ConfigCell<u16> = Box::leak(Box::new(ConfigCell::new()));
930
931        let guard = {
932            let counter = Arc::clone(&count);
933            cell.on_reload_with_scoped(move |_| *counter.lock().unwrap() += 1)
934        };
935
936        cell.store(1);
937        cell.store(2);
938        assert_eq!(*count.lock().unwrap(), 2, "the first install counts too");
939
940        drop(guard);
941        cell.store(3);
942        assert_eq!(*count.lock().unwrap(), 2);
943    }
944
945    /// Failures accumulate, an install clears the streak, and the record of
946    /// the last one survives it — the counter is the health, the record is
947    /// the history.
948    #[test]
949    fn failures_count_up_and_an_install_resets_the_streak() {
950        let cell = ConfigCell::<u16>::new();
951
952        assert_eq!(cell.status().consecutive_failures, 0);
953        assert!(cell.status().is_healthy());
954        assert!(cell.status().last_failure.is_none());
955        assert!(cell.status().last_reason.is_none());
956
957        for expected in 1..=3 {
958            cell.record_failure(&Error::new(
959                crate::ErrorKind::Parse,
960                "unexpected end of input",
961            ));
962            assert_eq!(cell.status().consecutive_failures, expected);
963        }
964
965        assert!(!cell.status().is_healthy());
966        assert_eq!(
967            cell.status().last_failure.unwrap().kind,
968            crate::ErrorKind::Parse
969        );
970
971        cell.store_with(1, ReloadReason::Recovered);
972
973        let status = cell.status();
974        assert_eq!(status.consecutive_failures, 0);
975        assert!(status.is_healthy());
976        assert_eq!(status.generation, 1);
977        assert_eq!(status.last_reason, Some(ReloadReason::Recovered));
978        assert!(
979            status.last_failure.is_some(),
980            "the streak resets; the record of what went wrong does not"
981        );
982        assert!(status.loaded_at.is_some());
983    }
984}