Skip to main content

dynamic_config/
dynamic.rs

1//! Instance-owned configuration: the engine without the `static`.
2//!
3//! `#[dynamic_config]` gives a *type* one configuration, stored in statics
4//! the macro generates. That is the right default and the wrong ceiling:
5//! multi-tenant programs want one configuration per tenant, tests want two
6//! side by side without type gymnastics, and a host language binding has no
7//! Rust type per user class at all. [`Dynamic<T>`] is the same engine with
8//! the storage owned by the value: its own cell, its own hooks, its own
9//! watcher identity — nothing shared with the type-level surface, and
10//! nothing global.
11//!
12//! ```no_run
13//! # #[cfg(feature = "json")] {
14//! use dynamic_config::{Builder, Dynamic};
15//! use serde::Deserialize;
16//!
17//! #[derive(Debug, Deserialize)]
18//! struct Tenant { name: String }
19//!
20//! let acme = Dynamic::new(Builder::new("tenant").file("acme.json"));
21//! let umbra = Dynamic::new(Builder::new("tenant").file("umbra.json"));
22//!
23//! let a: std::sync::Arc<Tenant> = acme.init_and_current()?;
24//! let u: std::sync::Arc<Tenant> = umbra.init_and_current()?;
25//! # let _ = (a, u);
26//! # }
27//! # Ok::<(), dynamic_config::Error>(())
28//! ```
29
30use std::sync::atomic::{AtomicU64, Ordering};
31use std::sync::Arc;
32
33use serde::de::DeserializeOwned;
34
35use crate::builder::Builder;
36use crate::cell::ConfigCell;
37use crate::error::Error;
38
39/// One process-unique number per instance, for the watcher registry.
40///
41/// A type's watcher is keyed by `TypeId`; every `Dynamic<Value>` is the
42/// same type, so an instance carries a number instead. Starts at one so
43/// zero never names anything — the same "never ambiguous with nothing"
44/// convention the reload generation follows.
45static NEXT_INSTANCE: AtomicU64 = AtomicU64::new(1);
46
47/// A configuration owned by a value rather than a type.
48///
49/// Construct one from a [`Builder`] carrying the sources; everything the
50/// type-level surface does through generated statics happens here through
51/// the instance's own storage. Two instances of the same `T` are fully
52/// independent: separate snapshots, separate reload hooks, separate
53/// watchers, separate caches if configured.
54///
55/// Cloning is deliberately absent: a `Dynamic` is an *owner* — share one
56/// behind an `Arc` when several places read it, which is also what keeps
57/// "who stops the watcher" a question with one answer.
58pub struct Dynamic<T> {
59    cell: Arc<ConfigCell<T>>,
60    builder: Builder<T>,
61    id: u64,
62    /// The registry wants a `&'static str`; leaked once per instance, on
63    /// the first watch, and reused for every stop/start cycle after it.
64    #[cfg(feature = "watch")]
65    watch_name: std::sync::OnceLock<&'static str>,
66}
67
68impl<T: DeserializeOwned + Send + Sync + 'static> Dynamic<T> {
69    /// Wraps `builder` around storage this instance owns.
70    ///
71    /// The builder's sources, cache and validation hook all apply
72    /// unchanged; an installer the builder already carried (a generated
73    /// `builder()`'s static cell) is replaced by this instance's own.
74    #[must_use]
75    pub fn new(builder: Builder<T>) -> Self {
76        let cell = Arc::new(ConfigCell::new());
77
78        Self {
79            builder: builder.with_cell(Arc::clone(&cell)),
80            cell,
81            id: NEXT_INSTANCE.fetch_add(1, Ordering::Relaxed),
82            #[cfg(feature = "watch")]
83            watch_name: std::sync::OnceLock::new(),
84        }
85    }
86
87    /// Loads and installs as this instance's snapshot.
88    ///
89    /// The same lifecycle as a type's `init()`: validation runs before
90    /// anything installs, a configured cache is written after a clean
91    /// load and recovered from when the sources will not load.
92    ///
93    /// # Errors
94    ///
95    /// Whatever the load reports: a file that will not parse, a missing
96    /// required value, a validation refusal with no cache to fall back on.
97    pub fn init(&self) -> Result<(), Error> {
98        self.builder.init()
99    }
100
101    /// [`init`](Self::init), handing back the snapshot it installed.
102    ///
103    /// Worth more here than on the type-level surface: an instance's
104    /// [`current`](Self::current) is an `Option` — nothing can panic with a
105    /// type's name in it — so the split form ends in an `expect` that this
106    /// removes. What comes back is *this* call's snapshot, not whatever a
107    /// reload made current a moment later.
108    ///
109    /// # Errors
110    ///
111    /// Exactly [`init`](Self::init)'s.
112    pub fn init_and_current(&self) -> Result<Arc<T>, Error> {
113        self.builder.init_and_current()
114    }
115
116    /// The installed snapshot, if [`init`](Self::init) has succeeded.
117    ///
118    /// One atomic load, no lock — cheap enough per request, but take it
119    /// once per request and reuse the `Arc`, or a reload landing
120    /// mid-request shows one request two configurations. `None` before the
121    /// first successful install: an instance has no place to panic with
122    /// the type's name in it, so absence is an answer rather than an
123    /// accident.
124    #[must_use]
125    pub fn current(&self) -> Option<Arc<T>> {
126        self.cell.load()
127    }
128
129    /// Installs since this instance was created; zero before the first.
130    ///
131    /// Monotonic, and the number a reload hook should read when it needs a
132    /// total order — [`on_reload`](Self::on_reload) does not define one
133    /// across overlapping reloads.
134    #[must_use]
135    pub fn generation(&self) -> u64 {
136        self.cell.generation()
137    }
138
139    /// What is true of the installed snapshot, or `None` before the first.
140    ///
141    /// For operators — which generation is live, how long ago it landed —
142    /// and deliberately off the read path: [`current`](Self::current) does
143    /// not consult it, so the value and its metadata are two loads that a
144    /// reload landing between them leaves one install apart. See
145    /// `SnapshotMeta`.
146    #[must_use]
147    pub fn meta(&self) -> Option<crate::SnapshotMeta> {
148        self.cell.meta()
149    }
150
151    /// Reads the sources and deserializes, installing nothing.
152    ///
153    /// # Errors
154    ///
155    /// The same failures as [`init`](Self::init).
156    pub fn load(&self) -> Result<T, Error> {
157        self.builder.load()
158    }
159
160    /// One reload: load, validate, install, rewrite the cache.
161    ///
162    /// A failure installs nothing — the previous snapshot keeps serving.
163    ///
164    /// # Errors
165    ///
166    /// The same failures as [`load`](Self::load).
167    pub fn reload(&self) -> Result<(), Error> {
168        self.builder.reload()
169    }
170
171    /// Runs `hook` after every later install, for the instance's lifetime.
172    ///
173    /// The same contract as the type-level `on_reload`: called with the
174    /// outgoing and incoming snapshots, on whichever thread performed the
175    /// reload — compare, then signal the subsystem that owns the resource.
176    ///
177    /// # Concurrent reloads
178    ///
179    /// Each call sees a consistent `(previous, current)` pair: both were
180    /// installed, and `current` was installed after `previous`.
181    ///
182    /// The *order of calls* is not defined when two reloads overlap. Two
183    /// hooks may observe the same pair, and one hook may see `(A, B)` after
184    /// another saw `(B, C)`. A hook that needs a total order should read
185    /// [`generation`](Self::generation) — which is monotonic — rather than
186    /// infer one from its arguments.
187    ///
188    /// Reloads are not serialised against each other on purpose: a lock held
189    /// across user callbacks would let one slow hook delay every reader, and
190    /// a hook that blocked would then block reloads.
191    pub fn on_reload(&self, hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static) {
192        self.cell.on_reload(hook);
193    }
194
195    /// [`on_reload`](Self::on_reload), until the returned guard drops.
196    ///
197    /// The same concurrency contract: a consistent pair every call, in no
198    /// defined order across overlapping reloads.
199    pub fn on_reload_scoped(
200        &self,
201        hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static,
202    ) -> crate::HookGuard<T> {
203        ConfigCell::on_reload_scoped_shared(&self.cell, hook)
204    }
205
206    /// Registers a callback for every reload that installs nothing — the
207    /// failure twin of [`on_reload`](Self::on_reload), for the process
208    /// lifetime, under the same contract: short callbacks, panics caught,
209    /// the watcher survives. The callback receives the
210    /// [`FailureStatus`](crate::FailureStatus) the refusal published.
211    pub fn on_reload_failed(&self, hook: impl Fn(&crate::FailureStatus) + Send + Sync + 'static) {
212        self.cell.on_reload_failed(hook);
213    }
214
215    /// [`on_reload_failed`](Self::on_reload_failed), until the returned
216    /// guard drops.
217    pub fn on_reload_failed_scoped(
218        &self,
219        hook: impl Fn(&crate::FailureStatus) + Send + Sync + 'static,
220    ) -> crate::HookGuard<T> {
221        crate::ConfigCell::on_reload_failed_scoped_shared(&self.cell, hook)
222    }
223
224    /// [`on_reload`](Self::on_reload), told *why*.
225    ///
226    /// The callback receives a [`ReloadEvent`](crate::ReloadEvent): both
227    /// snapshots, the [`ReloadReason`](crate::ReloadReason), and the
228    /// install's [`SnapshotMeta`](crate::SnapshotMeta). Same list, same
229    /// registration order, same panic isolation as the pair form — and it
230    /// fires for the **first** install too, with `previous: None`, which
231    /// the pair form has nowhere to say.
232    pub fn on_reload_with(&self, hook: impl Fn(&crate::ReloadEvent<T>) + Send + Sync + 'static) {
233        self.cell.on_reload_with(hook);
234    }
235
236    /// [`on_reload_with`](Self::on_reload_with), until the returned guard
237    /// drops.
238    pub fn on_reload_with_scoped(
239        &self,
240        hook: impl Fn(&crate::ReloadEvent<T>) + Send + Sync + 'static,
241    ) -> crate::HookGuard<T> {
242        ConfigCell::on_reload_with_scoped_shared(&self.cell, hook)
243    }
244
245    /// What is true of this instance right now: generation, when it landed,
246    /// why, and how the reloads since have gone.
247    ///
248    /// A handful of atomic loads and **no I/O** — no source is re-read —
249    /// so an exporter can call it per scrape. See
250    /// [`ConfigStatus`](crate::ConfigStatus) for what it carries and, as
251    /// deliberately, what it does not.
252    #[must_use]
253    pub fn status(&self) -> crate::ConfigStatus {
254        self.cell.status()
255    }
256
257    /// This instance's builder, for the diagnostics that answer without
258    /// installing: `source_of`, `is_set`, `check`, `explain`, `snapshot`.
259    ///
260    /// The instance does not re-wrap them — the builder's answers *are*
261    /// the instance's answers, because the builder is where its sources
262    /// live.
263    #[must_use]
264    pub fn builder(&self) -> &Builder<T> {
265        &self.builder
266    }
267
268    /// The section key this instance reads.
269    #[must_use]
270    pub fn key(&self) -> &str {
271        self.builder.key()
272    }
273}
274
275#[cfg(feature = "watch")]
276#[cfg_attr(docsrs, doc(cfg(feature = "watch")))]
277impl<T: DeserializeOwned + Send + Sync + 'static> Dynamic<T> {
278    /// Reloads on file changes until the returned handle is dropped.
279    ///
280    /// The same watcher as everything else — same debounce, same
281    /// directory-level watches — registered under this *instance* rather
282    /// than the type: two instances of one `T` watch side by side, and a
283    /// second watch on the *same* instance is `AlreadyExists`, exactly the
284    /// one-watcher-per-owner contract the type-level surface has.
285    ///
286    /// # Errors
287    ///
288    /// As the builder's `watch`: no watchable directory, a backend that
289    /// cannot start, or this instance already being watched.
290    pub fn watch(
291        &self,
292        debounce: core::time::Duration,
293    ) -> std::io::Result<crate::watch::WatchHandle> {
294        self.watch_with(debounce, crate::watch::WatchMode::Native)
295    }
296
297    /// [`watch`](Self::watch) with the detection strategy chosen
298    /// explicitly — polling is what network and overlay filesystems need.
299    ///
300    /// # Errors
301    ///
302    /// As [`watch`](Self::watch).
303    pub fn watch_with(
304        &self,
305        debounce: core::time::Duration,
306        mode: crate::watch::WatchMode,
307    ) -> std::io::Result<crate::watch::WatchHandle> {
308        let name = self.watch_name.get_or_init(|| {
309            Box::leak(format!("dynamic:{}#{}", self.builder.key(), self.id).into_boxed_str())
310        });
311
312        self.builder.watch_as(
313            crate::watch::WatchKey::Instance(self.id),
314            name,
315            debounce,
316            mode,
317        )
318    }
319}
320
321#[cfg(feature = "async")]
322#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
323impl<T: DeserializeOwned + Send + Sync + 'static> Dynamic<T> {
324    /// A handle woken by every later install of *this instance*.
325    ///
326    /// The same contract as the type-level `changes()`: the snapshot
327    /// current at this call counts as already seen, and a handle taken
328    /// before [`init`](Self::init) sees the first install as its first
329    /// change — "wake me when configuration exists". The handle keeps the
330    /// instance's storage alive, so it outliving the `Dynamic` is safe
331    /// rather than subtle.
332    #[must_use]
333    pub fn changes(&self) -> crate::Changes<T> {
334        crate::Changes::new_shared(Arc::clone(&self.cell))
335    }
336
337    /// [`changes`](Self::changes) widened to refusals: a stream of
338    /// [`Event`](crate::Event)s — installs *and* reloads that kept the
339    /// previous snapshot. The push half of [`status`](Self::status).
340    #[cfg(feature = "async")]
341    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
342    pub fn events(&self) -> crate::Events<T> {
343        crate::Events::new_shared(Arc::clone(&self.cell))
344    }
345
346    /// [`load`](Self::load), off the async executor.
347    ///
348    /// # Errors
349    ///
350    /// The same failures as [`load`](Self::load).
351    pub async fn load_async(&self) -> Result<T, Error> {
352        self.builder.load_async().await
353    }
354
355    /// [`init`](Self::init), off the async executor.
356    ///
357    /// # Errors
358    ///
359    /// The same failures as [`init`](Self::init).
360    pub async fn init_async(&self) -> Result<(), Error> {
361        self.builder.init_async().await
362    }
363
364    /// [`init_and_current`](Self::init_and_current), off the async executor.
365    ///
366    /// # Errors
367    ///
368    /// The same failures as [`init`](Self::init).
369    pub async fn init_and_current_async(&self) -> Result<Arc<T>, Error> {
370        self.builder.init_and_current_async().await
371    }
372}
373
374impl<T> std::fmt::Debug for Dynamic<T> {
375    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376        f.debug_struct("Dynamic")
377            .field("id", &self.id)
378            .field("builder", &self.builder)
379            .finish_non_exhaustive()
380    }
381}