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