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