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    /// [`on_reload`](Self::on_reload), told *why*.
207    ///
208    /// The callback receives a [`ReloadEvent`](crate::ReloadEvent): both
209    /// snapshots, the [`ReloadReason`](crate::ReloadReason), and the
210    /// install's [`SnapshotMeta`](crate::SnapshotMeta). Same list, same
211    /// registration order, same panic isolation as the pair form — and it
212    /// fires for the **first** install too, with `previous: None`, which
213    /// the pair form has nowhere to say.
214    pub fn on_reload_with(&self, hook: impl Fn(&crate::ReloadEvent<T>) + Send + Sync + 'static) {
215        self.cell.on_reload_with(hook);
216    }
217
218    /// [`on_reload_with`](Self::on_reload_with), until the returned guard
219    /// drops.
220    pub fn on_reload_with_scoped(
221        &self,
222        hook: impl Fn(&crate::ReloadEvent<T>) + Send + Sync + 'static,
223    ) -> crate::HookGuard<T> {
224        ConfigCell::on_reload_with_scoped_shared(&self.cell, hook)
225    }
226
227    /// What is true of this instance right now: generation, when it landed,
228    /// why, and how the reloads since have gone.
229    ///
230    /// A handful of atomic loads and **no I/O** — no source is re-read —
231    /// so an exporter can call it per scrape. See
232    /// [`ConfigStatus`](crate::ConfigStatus) for what it carries and, as
233    /// deliberately, what it does not.
234    #[must_use]
235    pub fn status(&self) -> crate::ConfigStatus {
236        self.cell.status()
237    }
238
239    /// This instance's builder, for the diagnostics that answer without
240    /// installing: `source_of`, `is_set`, `check`, `explain`, `snapshot`.
241    ///
242    /// The instance does not re-wrap them — the builder's answers *are*
243    /// the instance's answers, because the builder is where its sources
244    /// live.
245    #[must_use]
246    pub fn builder(&self) -> &Builder<T> {
247        &self.builder
248    }
249
250    /// The section key this instance reads.
251    #[must_use]
252    pub fn key(&self) -> &str {
253        self.builder.key()
254    }
255}
256
257#[cfg(feature = "watch")]
258#[cfg_attr(docsrs, doc(cfg(feature = "watch")))]
259impl<T: DeserializeOwned + Send + Sync + 'static> Dynamic<T> {
260    /// Reloads on file changes until the returned handle is dropped.
261    ///
262    /// The same watcher as everything else — same debounce, same
263    /// directory-level watches — registered under this *instance* rather
264    /// than the type: two instances of one `T` watch side by side, and a
265    /// second watch on the *same* instance is `AlreadyExists`, exactly the
266    /// one-watcher-per-owner contract the type-level surface has.
267    ///
268    /// # Errors
269    ///
270    /// As the builder's `watch`: no watchable directory, a backend that
271    /// cannot start, or this instance already being watched.
272    pub fn watch(
273        &self,
274        debounce: core::time::Duration,
275    ) -> std::io::Result<crate::watch::WatchHandle> {
276        self.watch_with(debounce, crate::watch::WatchMode::Native)
277    }
278
279    /// [`watch`](Self::watch) with the detection strategy chosen
280    /// explicitly — polling is what network and overlay filesystems need.
281    ///
282    /// # Errors
283    ///
284    /// As [`watch`](Self::watch).
285    pub fn watch_with(
286        &self,
287        debounce: core::time::Duration,
288        mode: crate::watch::WatchMode,
289    ) -> std::io::Result<crate::watch::WatchHandle> {
290        let name = self.watch_name.get_or_init(|| {
291            Box::leak(format!("dynamic:{}#{}", self.builder.key(), self.id).into_boxed_str())
292        });
293
294        self.builder.watch_as(
295            crate::watch::WatchKey::Instance(self.id),
296            name,
297            debounce,
298            mode,
299        )
300    }
301}
302
303#[cfg(feature = "async")]
304#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
305impl<T: DeserializeOwned + Send + Sync + 'static> Dynamic<T> {
306    /// A handle woken by every later install of *this instance*.
307    ///
308    /// The same contract as the type-level `changes()`: the snapshot
309    /// current at this call counts as already seen, and a handle taken
310    /// before [`init`](Self::init) sees the first install as its first
311    /// change — "wake me when configuration exists". The handle keeps the
312    /// instance's storage alive, so it outliving the `Dynamic` is safe
313    /// rather than subtle.
314    #[must_use]
315    pub fn changes(&self) -> crate::Changes<T> {
316        crate::Changes::new_shared(Arc::clone(&self.cell))
317    }
318
319    /// [`load`](Self::load), off the async executor.
320    ///
321    /// # Errors
322    ///
323    /// The same failures as [`load`](Self::load).
324    pub async fn load_async(&self) -> Result<T, Error> {
325        self.builder.load_async().await
326    }
327
328    /// [`init`](Self::init), off the async executor.
329    ///
330    /// # Errors
331    ///
332    /// The same failures as [`init`](Self::init).
333    pub async fn init_async(&self) -> Result<(), Error> {
334        self.builder.init_async().await
335    }
336
337    /// [`init_and_current`](Self::init_and_current), off the async executor.
338    ///
339    /// # Errors
340    ///
341    /// The same failures as [`init`](Self::init).
342    pub async fn init_and_current_async(&self) -> Result<Arc<T>, Error> {
343        self.builder.init_and_current_async().await
344    }
345}
346
347impl<T> std::fmt::Debug for Dynamic<T> {
348    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349        f.debug_struct("Dynamic")
350            .field("id", &self.id)
351            .field("builder", &self.builder)
352            .finish_non_exhaustive()
353    }
354}