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