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    /// A stable digest of the configuration this instance is serving.
130    ///
131    /// `sha256:…`, or `None` before the first install. **Safe to log**:
132    /// every field marked `#[config(secret)]` is masked by position before
133    /// hashing, so the digest moves when a secret appears or disappears and
134    /// stays put when one merely rotates.
135    ///
136    /// What it is for is comparing two processes without comparing two
137    /// documents. Pod A prints a string, pod B prints a string, and either
138    /// they match or one of them is running a configuration nobody meant to
139    /// ship. [`generation`](Self::generation) counts *this* process's
140    /// installs and cannot answer that — two pods on generation 4 have
141    /// nothing in particular in common.
142    ///
143    /// Computed over the resolved tree rather than any rendering of it, so
144    /// the same configuration fingerprints the same however it was written.
145    #[must_use]
146    pub fn fingerprint(&self) -> Option<Arc<String>> {
147        self.cell.fingerprint()
148    }
149
150    /// Installs since this instance was created; zero before the first.
151    ///
152    /// Monotonic, and the number a reload hook should read when it needs a
153    /// total order — [`on_reload`](Self::on_reload) does not define one
154    /// across overlapping reloads.
155    #[must_use]
156    pub fn generation(&self) -> u64 {
157        self.cell.generation()
158    }
159
160    /// What is true of the installed snapshot, or `None` before the first.
161    ///
162    /// For operators — which generation is live, how long ago it landed —
163    /// and deliberately off the read path: [`current`](Self::current) does
164    /// not consult it, so the value and its metadata are two loads that a
165    /// reload landing between them leaves one install apart. See
166    /// `SnapshotMeta`.
167    #[must_use]
168    pub fn meta(&self) -> Option<crate::SnapshotMeta> {
169        self.cell.meta()
170    }
171
172    /// Reads the sources and deserializes, installing nothing.
173    ///
174    /// # Errors
175    ///
176    /// The same failures as [`init`](Self::init).
177    pub fn load(&self) -> Result<T, Error> {
178        self.builder.load()
179    }
180
181    /// One reload: load, validate, install, rewrite the cache.
182    ///
183    /// A failure installs nothing — the previous snapshot keeps serving.
184    ///
185    /// # Errors
186    ///
187    /// The same failures as [`load`](Self::load).
188    pub fn reload(&self) -> Result<(), Error> {
189        self.builder.reload()
190    }
191
192    /// Runs `hook` after every later install, for the instance's lifetime.
193    ///
194    /// The same contract as the type-level `on_reload`: called with the
195    /// outgoing and incoming snapshots, on whichever thread performed the
196    /// reload — compare, then signal the subsystem that owns the resource.
197    ///
198    /// # Concurrent reloads
199    ///
200    /// Each call sees a consistent `(previous, current)` pair: both were
201    /// installed, and `current` was installed after `previous`.
202    ///
203    /// The *order of calls* is not defined when two reloads overlap. Two
204    /// hooks may observe the same pair, and one hook may see `(A, B)` after
205    /// another saw `(B, C)`. A hook that needs a total order should read
206    /// [`generation`](Self::generation) — which is monotonic — rather than
207    /// infer one from its arguments.
208    ///
209    /// Reloads are not serialised against each other on purpose: a lock held
210    /// across user callbacks would let one slow hook delay every reader, and
211    /// a hook that blocked would then block reloads.
212    pub fn on_reload(&self, hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static) {
213        self.cell.on_reload(hook);
214    }
215
216    /// [`on_reload`](Self::on_reload), until the returned guard drops.
217    ///
218    /// The same concurrency contract: a consistent pair every call, in no
219    /// defined order across overlapping reloads.
220    pub fn on_reload_scoped(
221        &self,
222        hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static,
223    ) -> crate::HookGuard<T> {
224        ConfigCell::on_reload_scoped_shared(&self.cell, hook)
225    }
226
227    /// Registers a callback for every reload that installs nothing — the
228    /// failure twin of [`on_reload`](Self::on_reload), for the process
229    /// lifetime, under the same contract: short callbacks, panics caught,
230    /// the watcher survives. The callback receives the
231    /// [`FailureStatus`](crate::FailureStatus) the refusal published.
232    pub fn on_reload_failed(&self, hook: impl Fn(&crate::FailureStatus) + Send + Sync + 'static) {
233        self.cell.on_reload_failed(hook);
234    }
235
236    /// [`on_reload_failed`](Self::on_reload_failed), until the returned
237    /// guard drops.
238    pub fn on_reload_failed_scoped(
239        &self,
240        hook: impl Fn(&crate::FailureStatus) + Send + Sync + 'static,
241    ) -> crate::HookGuard<T> {
242        crate::ConfigCell::on_reload_failed_scoped_shared(&self.cell, hook)
243    }
244
245    /// [`on_reload`](Self::on_reload), told *why*.
246    ///
247    /// The callback receives a [`ReloadEvent`](crate::ReloadEvent): both
248    /// snapshots, the [`ReloadReason`](crate::ReloadReason), and the
249    /// install's [`SnapshotMeta`](crate::SnapshotMeta). Same list, same
250    /// registration order, same panic isolation as the pair form — and it
251    /// fires for the **first** install too, with `previous: None`, which
252    /// the pair form has nowhere to say.
253    pub fn on_reload_with(&self, hook: impl Fn(&crate::ReloadEvent<T>) + Send + Sync + 'static) {
254        self.cell.on_reload_with(hook);
255    }
256
257    /// [`on_reload_with`](Self::on_reload_with), until the returned guard
258    /// drops.
259    pub fn on_reload_with_scoped(
260        &self,
261        hook: impl Fn(&crate::ReloadEvent<T>) + Send + Sync + 'static,
262    ) -> crate::HookGuard<T> {
263        ConfigCell::on_reload_with_scoped_shared(&self.cell, hook)
264    }
265
266    /// What is true of this instance right now: generation, when it landed,
267    /// why, and how the reloads since have gone.
268    ///
269    /// A handful of atomic loads and **no I/O** — no source is re-read —
270    /// so an exporter can call it per scrape. See
271    /// [`ConfigStatus`](crate::ConfigStatus) for what it carries and, as
272    /// deliberately, what it does not.
273    #[must_use]
274    pub fn status(&self) -> crate::ConfigStatus {
275        self.cell.status()
276    }
277
278    /// This instance's builder, for the diagnostics that answer without
279    /// installing: `source_of`, `is_set`, `check`, `explain`, `snapshot`.
280    ///
281    /// The instance does not re-wrap them — the builder's answers *are*
282    /// the instance's answers, because the builder is where its sources
283    /// live.
284    #[must_use]
285    pub fn builder(&self) -> &Builder<T> {
286        &self.builder
287    }
288
289    /// The section key this instance reads.
290    #[must_use]
291    pub fn key(&self) -> &str {
292        self.builder.key()
293    }
294}
295
296#[cfg(feature = "watch")]
297#[cfg_attr(docsrs, doc(cfg(feature = "watch")))]
298impl<T: DeserializeOwned + Send + Sync + 'static> Dynamic<T> {
299    /// Reloads on file changes until the returned handle is dropped.
300    ///
301    /// The same watcher as everything else — same debounce, same
302    /// directory-level watches — registered under this *instance* rather
303    /// than the type: two instances of one `T` watch side by side, and a
304    /// second watch on the *same* instance is `AlreadyExists`, exactly the
305    /// one-watcher-per-owner contract the type-level surface has.
306    ///
307    /// # Errors
308    ///
309    /// As the builder's `watch`: no watchable directory, a backend that
310    /// cannot start, or this instance already being watched.
311    pub fn watch(
312        &self,
313        options: impl Into<crate::watch::WatchOptions>,
314    ) -> std::io::Result<crate::watch::WatchHandle> {
315        self.watch_with(options, crate::watch::WatchMode::Native)
316    }
317
318    /// [`watch`](Self::watch) with the detection strategy chosen
319    /// explicitly — polling is what network and overlay filesystems need.
320    ///
321    /// # Errors
322    ///
323    /// As [`watch`](Self::watch).
324    pub fn watch_with(
325        &self,
326        options: impl Into<crate::watch::WatchOptions>,
327        mode: crate::watch::WatchMode,
328    ) -> std::io::Result<crate::watch::WatchHandle> {
329        let name = self.watch_name.get_or_init(|| {
330            Box::leak(format!("dynamic:{}#{}", self.builder.key(), self.id).into_boxed_str())
331        });
332
333        self.builder.watch_as(
334            crate::watch::WatchKey::Instance(self.id),
335            name,
336            options.into(),
337            mode,
338        )
339    }
340}
341
342#[cfg(feature = "async")]
343#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
344impl<T: DeserializeOwned + Send + Sync + 'static> Dynamic<T> {
345    /// A handle woken by every later install of *this instance*.
346    ///
347    /// The same contract as the type-level `changes()`: the snapshot
348    /// current at this call counts as already seen, and a handle taken
349    /// before [`init`](Self::init) sees the first install as its first
350    /// change — "wake me when configuration exists". The handle keeps the
351    /// instance's storage alive, so it outliving the `Dynamic` is safe
352    /// rather than subtle.
353    #[must_use]
354    pub fn changes(&self) -> crate::Changes<T> {
355        crate::Changes::new_shared(Arc::clone(&self.cell))
356    }
357
358    /// [`changes`](Self::changes) widened to refusals: a stream of
359    /// [`Event`](crate::Event)s — installs *and* reloads that kept the
360    /// previous snapshot. The push half of [`status`](Self::status).
361    #[cfg(feature = "async")]
362    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
363    #[must_use]
364    pub fn events(&self) -> crate::Events<T> {
365        crate::Events::new_shared(Arc::clone(&self.cell))
366    }
367
368    /// [`load`](Self::load), off the async executor.
369    ///
370    /// # Errors
371    ///
372    /// The same failures as [`load`](Self::load).
373    pub async fn load_async(&self) -> Result<T, Error> {
374        self.builder.load_async().await
375    }
376
377    /// [`init`](Self::init), off the async executor.
378    ///
379    /// # Errors
380    ///
381    /// The same failures as [`init`](Self::init).
382    pub async fn init_async(&self) -> Result<(), Error> {
383        self.builder.init_async().await
384    }
385
386    /// [`init_and_current`](Self::init_and_current), off the async executor.
387    ///
388    /// # Errors
389    ///
390    /// The same failures as [`init`](Self::init).
391    pub async fn init_and_current_async(&self) -> Result<Arc<T>, Error> {
392        self.builder.init_and_current_async().await
393    }
394}
395
396impl<T> std::fmt::Debug for Dynamic<T> {
397    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398        f.debug_struct("Dynamic")
399            .field("id", &self.id)
400            .field("builder", &self.builder)
401            .finish_non_exhaustive()
402    }
403}