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