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) = ®istered.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 ®istered.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}