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//! acme.init()?;
24//! umbra.init()?;
25//!
26//! let a: std::sync::Arc<Tenant> = acme.current().expect("initialised above");
27//! let u: std::sync::Arc<Tenant> = umbra.current().expect("initialised above");
28//! # let _ = (a, u);
29//! # }
30//! # Ok::<(), dynamic_config::Error>(())
31//! ```
32
33use std::sync::atomic::{AtomicU64, Ordering};
34use std::sync::Arc;
35
36use serde::de::DeserializeOwned;
37
38use crate::builder::Builder;
39use crate::cell::ConfigCell;
40use crate::error::Error;
41
42/// One process-unique number per instance, for the watcher registry.
43///
44/// A type's watcher is keyed by `TypeId`; every `Dynamic<Value>` is the
45/// same type, so an instance carries a number instead. Starts at one so
46/// zero never names anything — the same "never ambiguous with nothing"
47/// convention the reload generation follows.
48static NEXT_INSTANCE: AtomicU64 = AtomicU64::new(1);
49
50/// A configuration owned by a value rather than a type.
51///
52/// Construct one from a [`Builder`] carrying the sources; everything the
53/// type-level surface does through generated statics happens here through
54/// the instance's own storage. Two instances of the same `T` are fully
55/// independent: separate snapshots, separate reload hooks, separate
56/// watchers, separate caches if configured.
57///
58/// Cloning is deliberately absent: a `Dynamic` is an *owner* — share one
59/// behind an `Arc` when several places read it, which is also what keeps
60/// "who stops the watcher" a question with one answer.
61pub struct Dynamic<T> {
62 cell: Arc<ConfigCell<T>>,
63 builder: Builder<T>,
64 id: u64,
65 /// The registry wants a `&'static str`; leaked once per instance, on
66 /// the first watch, and reused for every stop/start cycle after it.
67 #[cfg(feature = "watch")]
68 watch_name: std::sync::OnceLock<&'static str>,
69}
70
71impl<T: DeserializeOwned + Send + Sync + 'static> Dynamic<T> {
72 /// Wraps `builder` around storage this instance owns.
73 ///
74 /// The builder's sources, cache and validation hook all apply
75 /// unchanged; an installer the builder already carried (a generated
76 /// `builder()`'s static cell) is replaced by this instance's own.
77 #[must_use]
78 pub fn new(builder: Builder<T>) -> Self {
79 let cell = Arc::new(ConfigCell::new());
80
81 Self {
82 builder: builder.with_cell(Arc::clone(&cell)),
83 cell,
84 id: NEXT_INSTANCE.fetch_add(1, Ordering::Relaxed),
85 #[cfg(feature = "watch")]
86 watch_name: std::sync::OnceLock::new(),
87 }
88 }
89
90 /// Loads and installs as this instance's snapshot.
91 ///
92 /// The same lifecycle as a type's `init()`: validation runs before
93 /// anything installs, a configured cache is written after a clean
94 /// load and recovered from when the sources will not load.
95 ///
96 /// # Errors
97 ///
98 /// Whatever the load reports: a file that will not parse, a missing
99 /// required value, a validation refusal with no cache to fall back on.
100 pub fn init(&self) -> Result<(), Error> {
101 self.builder.init()
102 }
103
104 /// The installed snapshot, if [`init`](Self::init) has succeeded.
105 ///
106 /// One atomic load, no lock — cheap enough per request, but take it
107 /// once per request and reuse the `Arc`, or a reload landing
108 /// mid-request shows one request two configurations. `None` before the
109 /// first successful install: an instance has no place to panic with
110 /// the type's name in it, so absence is an answer rather than an
111 /// accident.
112 #[must_use]
113 pub fn current(&self) -> Option<Arc<T>> {
114 self.cell.load()
115 }
116
117 /// Reads the sources and deserializes, installing nothing.
118 ///
119 /// # Errors
120 ///
121 /// The same failures as [`init`](Self::init).
122 pub fn load(&self) -> Result<T, Error> {
123 self.builder.load()
124 }
125
126 /// One reload: load, validate, install, rewrite the cache.
127 ///
128 /// A failure installs nothing — the previous snapshot keeps serving.
129 ///
130 /// # Errors
131 ///
132 /// The same failures as [`load`](Self::load).
133 pub fn reload(&self) -> Result<(), Error> {
134 self.builder.reload()
135 }
136
137 /// Runs `hook` after every later install, for the instance's lifetime.
138 ///
139 /// The same contract as the type-level `on_reload`: called with the
140 /// outgoing and incoming snapshots, on whichever thread performed the
141 /// reload — compare, then signal the subsystem that owns the resource.
142 pub fn on_reload(&self, hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static) {
143 self.cell.on_reload(hook);
144 }
145
146 /// [`on_reload`](Self::on_reload), until the returned guard drops.
147 pub fn on_reload_scoped(
148 &self,
149 hook: impl Fn(&Arc<T>, &Arc<T>) + Send + Sync + 'static,
150 ) -> crate::HookGuard<T> {
151 ConfigCell::on_reload_scoped_shared(&self.cell, hook)
152 }
153
154 /// This instance's builder, for the diagnostics that answer without
155 /// installing: `source_of`, `is_set`, `check`, `explain`, `snapshot`.
156 ///
157 /// The instance does not re-wrap them — the builder's answers *are*
158 /// the instance's answers, because the builder is where its sources
159 /// live.
160 #[must_use]
161 pub fn builder(&self) -> &Builder<T> {
162 &self.builder
163 }
164
165 /// The section key this instance reads.
166 #[must_use]
167 pub fn key(&self) -> &str {
168 self.builder.key()
169 }
170}
171
172#[cfg(feature = "watch")]
173#[cfg_attr(docsrs, doc(cfg(feature = "watch")))]
174impl<T: DeserializeOwned + Send + Sync + 'static> Dynamic<T> {
175 /// Reloads on file changes until the returned handle is dropped.
176 ///
177 /// The same watcher as everything else — same debounce, same
178 /// directory-level watches — registered under this *instance* rather
179 /// than the type: two instances of one `T` watch side by side, and a
180 /// second watch on the *same* instance is `AlreadyExists`, exactly the
181 /// one-watcher-per-owner contract the type-level surface has.
182 ///
183 /// # Errors
184 ///
185 /// As the builder's `watch`: no watchable directory, a backend that
186 /// cannot start, or this instance already being watched.
187 pub fn watch(
188 &self,
189 debounce: core::time::Duration,
190 ) -> std::io::Result<crate::watch::WatchHandle> {
191 self.watch_with(debounce, crate::watch::WatchMode::Native)
192 }
193
194 /// [`watch`](Self::watch) with the detection strategy chosen
195 /// explicitly — polling is what network and overlay filesystems need.
196 ///
197 /// # Errors
198 ///
199 /// As [`watch`](Self::watch).
200 pub fn watch_with(
201 &self,
202 debounce: core::time::Duration,
203 mode: crate::watch::WatchMode,
204 ) -> std::io::Result<crate::watch::WatchHandle> {
205 let name = self.watch_name.get_or_init(|| {
206 Box::leak(format!("dynamic:{}#{}", self.builder.key(), self.id).into_boxed_str())
207 });
208
209 self.builder.watch_as(
210 crate::watch::WatchKey::Instance(self.id),
211 name,
212 debounce,
213 mode,
214 )
215 }
216}
217
218#[cfg(feature = "async")]
219#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
220impl<T: DeserializeOwned + Send + Sync + 'static> Dynamic<T> {
221 /// A handle woken by every later install of *this instance*.
222 ///
223 /// The same contract as the type-level `changes()`: the snapshot
224 /// current at this call counts as already seen, and a handle taken
225 /// before [`init`](Self::init) sees the first install as its first
226 /// change — "wake me when configuration exists". The handle keeps the
227 /// instance's storage alive, so it outliving the `Dynamic` is safe
228 /// rather than subtle.
229 #[must_use]
230 pub fn changes(&self) -> crate::Changes<T> {
231 crate::Changes::new_shared(Arc::clone(&self.cell))
232 }
233
234 /// [`load`](Self::load), off the async executor.
235 ///
236 /// # Errors
237 ///
238 /// The same failures as [`load`](Self::load).
239 pub async fn load_async(&self) -> Result<T, Error> {
240 self.builder.load_async().await
241 }
242
243 /// [`init`](Self::init), off the async executor.
244 ///
245 /// # Errors
246 ///
247 /// The same failures as [`init`](Self::init).
248 pub async fn init_async(&self) -> Result<(), Error> {
249 self.builder.init_async().await
250 }
251}
252
253impl<T> std::fmt::Debug for Dynamic<T> {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 f.debug_struct("Dynamic")
256 .field("id", &self.id)
257 .field("builder", &self.builder)
258 .finish_non_exhaustive()
259 }
260}