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}