dynamic_config/builder/lifecycle.rs
1//! Loading, installing, recovering: the half of the builder that commits.
2//!
3//! `load` stays pure — read, deserialize, validate, hand over. Everything
4//! that *publishes* lives here too: `init` (and its recovery from the
5//! last-known-good cache), `reload`, the grouped-commit `prepare`, and the
6//! async variants that move the blocking read off the executor.
7
8use serde::de::DeserializeOwned;
9
10use std::path::Path;
11
12use crate::cache::{CacheMode, Recovery};
13use crate::error::{Error, ErrorKind};
14use crate::reload::ReloadReason;
15
16use super::Builder;
17
18impl<T: DeserializeOwned> Builder<T> {
19 /// Reads the sources and deserializes, installing nothing.
20 ///
21 /// # Errors
22 ///
23 /// The same failures as any load: a file that will not parse, a missing
24 /// required value, an unsupported extension.
25 pub fn load(&self) -> Result<T, Error> {
26 let value: T = self.with_spec(crate::loader::load)?;
27
28 if let Some(check) = &self.validate {
29 check(&value)?;
30 }
31
32 Ok(value)
33 }
34
35 /// [`load`](Self::load), keeping the fingerprint of the tree it came
36 /// from.
37 ///
38 /// Used by every path that *installs*, because the resolved tree exists
39 /// only during a load: asking for the fingerprint afterwards would mean
40 /// reading every source again to describe a snapshot already in hand.
41 fn load_and_fingerprint(&self) -> Result<(T, String), Error> {
42 let secrets = self.secrets.clone().unwrap_or_default();
43
44 let (value, snapshot) = self.with_spec(crate::loader::load_with_snapshot)?;
45
46 if let Some(check) = &self.validate {
47 check(&value)?;
48 }
49
50 let refs: Vec<&str> = secrets.iter().map(String::as_str).collect();
51
52 Ok((value, snapshot.fingerprint(&refs)))
53 }
54
55 /// Loads and installs as the type's snapshot.
56 ///
57 /// # Errors
58 ///
59 /// Whatever [`load`](Self::load) reports — and, on a builder made with
60 /// [`Builder::new`] rather than a generated `builder()`, the fact that
61 /// there is no storage to install into.
62 pub fn init(&self) -> Result<(), Error> {
63 self.install().map(|_| ())
64 }
65
66 /// [`init`](Self::init), handing back the snapshot it installed.
67 ///
68 /// The two calls always pair — install a configuration, then read it —
69 /// and writing them apart means naming the type twice and reading the
70 /// second line to learn that the first worked:
71 ///
72 /// ```no_run
73 /// # #[cfg(feature = "json")] {
74 /// # use serde::Deserialize;
75 /// # #[dynamic_config::dynamic_config]
76 /// # #[derive(Deserialize)]
77 /// # struct ServerConfig { host: String }
78 /// let config = ServerConfig::builder("server")
79 /// .file("config.json")
80 /// .init_and_current()?;
81 ///
82 /// println!("{}", config.host);
83 /// # }
84 /// # Ok::<(), dynamic_config::Error>(())
85 /// ```
86 ///
87 /// The snapshot is the one *this* call installed. A reload landing
88 /// between the install and the return would change what `current()`
89 /// answers; it does not change what this returns, which is the
90 /// configuration the program was started with.
91 ///
92 /// # Errors
93 ///
94 /// Exactly [`init`](Self::init)'s.
95 pub fn init_and_current(&self) -> Result<std::sync::Arc<T>, Error> {
96 self.install()
97 }
98
99 /// The whole of `init`, with the installed snapshot still in hand.
100 fn install(&self) -> Result<std::sync::Arc<T>, Error> {
101 // Before the installer check: "your cache cannot redact" is the more
102 // specific mistake, and the more dangerous one to leave unexplained.
103 self.check_cache_mode()?;
104
105 let Some(install) = self.install.as_ref() else {
106 return Err(Error::new(
107 ErrorKind::Backend,
108 "this builder is tied to no config type, so there is nowhere \
109 to install; use `load()` here, or start from the generated \
110 `builder()` on a `#[dynamic_config]` type",
111 ));
112 };
113
114 let outcome = match self.load_and_fingerprint() {
115 Ok((value, fingerprint)) => {
116 let installed = install.install(value, ReloadReason::Initial);
117 install.record_fingerprint(fingerprint);
118 self.write_cache();
119
120 Ok(installed)
121 }
122 // Every exit from here that does not install has to say so: the
123 // recovery is the one place a failed load can still succeed, so
124 // "the load failed" is not yet the answer, and recording it here
125 // would count a start that worked as a failure.
126 Err(failure) => self
127 .recover(failure)
128 .and_then(|recovered| {
129 if let Some(check) = &self.validate {
130 check(&recovered)?;
131 }
132
133 let installed = install.install(recovered, ReloadReason::Recovered);
134 crate::log::warning!(
135 "{}: started from the last known good configuration",
136 self.key
137 );
138
139 Ok(installed)
140 })
141 .inspect_err(|error| {
142 install.record_failure(error);
143 }),
144 };
145
146 if outcome.is_ok() {
147 if let Some(register) = self.register {
148 register(self);
149 }
150 }
151
152 outcome
153 }
154
155 /// The first half of a grouped reload: load and validate now, install
156 /// later — what [`ReloadGroup`](crate::ReloadGroup) drives.
157 ///
158 /// # Errors
159 ///
160 /// The same failures as [`load`](Self::load); a builder with no
161 /// installer has nothing to commit into.
162 pub fn prepare(&self) -> Result<crate::group::Commit, Error>
163 where
164 T: Send + Sync + 'static,
165 {
166 let Some(install) = self.install.as_ref() else {
167 return Err(Error::new(
168 ErrorKind::Backend,
169 "this builder is tied to no config type, so a prepared \
170 commit would have nowhere to install",
171 ));
172 };
173
174 let value = self.load().inspect_err(|error| {
175 install.record_failure(error);
176 })?;
177
178 let install = install.clone();
179
180 Ok(Box::new(move || {
181 install.install(value, ReloadReason::Manual);
182 }))
183 }
184
185 /// Refuses a redaction-dependent cache mode on a builder that cannot
186 /// know which fields are secret.
187 fn check_cache_mode(&self) -> Result<(), Error> {
188 if let Some((_, mode)) = &self.cache {
189 if !matches!(mode, CacheMode::Full) && self.secrets.is_none() {
190 return Err(Error::new(
191 ErrorKind::Backend,
192 "a redacted or fingerprint cache needs to know which \
193 fields are secret, and nothing here has said. A \
194 `#[dynamic_config]` type's generated `builder()` says \
195 it from the declaration; a configuration with no \
196 declaration says it with `.secrets([..])` — the Python \
197 binding spells that `DynamicConfig(..., secrets=[..])`. \
198 Or ask for `CacheMode::Full`, which redacts nothing and \
199 says so — `cache(path, \"full\")` from Python",
200 ));
201 }
202 }
203
204 Ok(())
205 }
206
207 /// Best-effort, exactly like the attribute's cache: a cache that cannot
208 /// be written is a worse tomorrow, not a broken today.
209 pub(super) fn write_cache(&self) {
210 let Some((path, mode)) = &self.cache else {
211 return;
212 };
213
214 // The same refusal `init` makes, as a structural belt: every path
215 // that writes must hold it, not just the one that happens to run
216 // the check first — a file *marked* redacted with nothing redacted
217 // would be the quiet worst case.
218 if !matches!(mode, CacheMode::Full) && self.secrets.is_none() {
219 crate::log::warning!(
220 "{}: not writing the cache at {path}: a redaction-dependent \
221 mode needs to know which fields are secret, and nothing \
222 has said — declare them, or pass them to `.secrets([..])`",
223 self.key
224 );
225
226 return;
227 }
228
229 let secrets = self.secrets.clone().unwrap_or_default();
230 let secret_refs: Vec<&str> = secrets.iter().map(String::as_str).collect();
231
232 let written = self.with_spec(|spec| {
233 let snapshot = crate::loader::snapshot(spec)?;
234
235 #[cfg(feature = "decrypt")]
236 if let Some(encryptor) = &self.cache_encryptor {
237 return crate::cache::write_encrypted(
238 &snapshot,
239 Path::new(path),
240 encryptor.as_ref(),
241 );
242 }
243
244 crate::cache::write(&snapshot, Path::new(path), *mode, &secret_refs)
245 });
246
247 if let Err(error) = written {
248 crate::log::warning!("could not write the configuration cache to {path}: {error}");
249 }
250 }
251
252 /// The last known good configuration, when the sources will not load —
253 /// or the original failure back, when there is nothing to recover from.
254 fn recover(&self, failure: Error) -> Result<T, Error> {
255 let Some((path, mode)) = &self.cache else {
256 return Err(failure);
257 };
258
259 // The configured mode decides, not the file on disk: a value-bearing
260 // cache left behind by an earlier deployment must not resurrect a
261 // configuration the operator deliberately switched away from.
262 // Fingerprint promises to diagnose and still fail.
263 let may_recover = mode.recovers();
264
265 // What the sources resolve to *now*, if they resolve at all — the
266 // drift report needs it, and a parse failure means there is nothing
267 // to compare.
268 let current = self.with_spec(crate::loader::snapshot).ok();
269
270 #[cfg(feature = "decrypt")]
271 let recovered = if let Some(_encryptor) = &self.cache_encryptor {
272 crate::cache::read_encrypted(Path::new(path), current.as_ref())
273 } else {
274 crate::cache::read(Path::new(path), current.as_ref())
275 };
276 #[cfg(not(feature = "decrypt"))]
277 let recovered = crate::cache::read(Path::new(path), current.as_ref());
278
279 match recovered {
280 // Through the loader, not a bare extract: the environment and
281 // `.env` files layer over the cache exactly as they would over
282 // the files, which is what lets a redacted cache work — the
283 // values it dropped come back from wherever they were live.
284 Ok(Recovery::Usable(snapshot)) if may_recover => self
285 .with_spec(|spec| crate::loader::recover::<T>(spec, &snapshot))
286 .map(|(value, _snapshot)| value),
287 Ok(Recovery::Usable(_)) => {
288 crate::log::warning!(
289 "{}: the cache at {path} holds values, but this builder \
290 is configured `Fingerprint`, which diagnoses and never \
291 recovers; refusing to start from it",
292 self.key
293 );
294
295 Err(failure)
296 }
297 // A fingerprint cannot rebuild a configuration, but it can still
298 // say what moved since the last good state — the diagnosis that
299 // makes the failure actionable at three in the morning.
300 Ok(Recovery::Drift(moved)) => {
301 crate::log::warning!(
302 "{}: cannot start: {failure}. Since the last good configuration: {}",
303 self.key,
304 match moved {
305 Some(paths) if paths.is_empty() => "nothing detectably moved".to_owned(),
306 Some(paths) => paths.join(", "),
307 None => "could not compare — the sources do not resolve".to_owned(),
308 }
309 );
310
311 Err(failure)
312 }
313 // A cache that will not read cures nothing: the original failure
314 // is the honest answer (the cache's own trouble is logged by
315 // `read` before this returns).
316 Ok(Recovery::Absent) | Err(_) => Err(failure),
317 }
318 }
319
320 /// One reload: load, validate, install, rewrite the cache.
321 ///
322 /// What a watch iteration and a [`RemoteSink`](crate::RemoteSink)'s
323 /// `apply` both do. A failure
324 /// installs nothing — the previous snapshot keeps serving.
325 ///
326 /// # Errors
327 ///
328 /// The same failures as [`load`](Self::load); a builder with no
329 /// installer has nothing to reload into.
330 pub fn reload(&self) -> Result<(), Error> {
331 self.reload_with(ReloadReason::Manual)
332 }
333
334 /// [`reload`](Self::reload), stating why.
335 ///
336 /// The reason reaches the reload hooks registered through
337 /// `on_reload_with` and the `last_reason` in
338 /// [`ConfigCell::status`](crate::ConfigCell::status). Everything else is
339 /// identical — this is `reload` with the label it would otherwise have
340 /// to guess. Programs that detect their own changes (a store this crate
341 /// has no adapter for, a control plane pushing over a socket) have
342 /// somewhere to say so; a plain `reload()` is
343 /// [`Manual`](crate::ReloadReason::Manual).
344 ///
345 /// # Errors
346 ///
347 /// The same failures as [`reload`](Self::reload).
348 pub fn reload_with(&self, reason: crate::ReloadReason) -> Result<(), Error> {
349 let Some(install) = self.install.as_ref() else {
350 return Err(Error::new(
351 ErrorKind::Backend,
352 "this builder is tied to no config type, so a reload would \
353 have nowhere to install",
354 ));
355 };
356
357 let (value, fingerprint) = self.load_and_fingerprint().inspect_err(|error| {
358 install.record_failure(error);
359 })?;
360
361 install.install(value, reason);
362 install.record_fingerprint(fingerprint);
363 self.write_cache();
364
365 Ok(())
366 }
367}
368
369#[cfg(feature = "async")]
370#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
371// `Sync` joined the bounds when the builder learned to carry a shared
372// cell (`Installer::Cell` holds an `Arc<ConfigCell<T>>`, and moving the
373// builder to the blocking worker moves the cell with it). A config type
374// that is `Send` without `Sync` is a curiosity this crate does not chase.
375impl<T: DeserializeOwned + Send + Sync + 'static> Builder<T> {
376 /// [`load`](Self::load), off the async executor.
377 ///
378 /// # Errors
379 ///
380 /// The same failures as [`load`](Self::load).
381 pub async fn load_async(&self) -> Result<T, Error> {
382 let this = self.clone();
383
384 crate::asynchronous::off_thread(move || this.load()).await
385 }
386
387 /// [`init`](Self::init), off the async executor.
388 ///
389 /// # Errors
390 ///
391 /// The same failures as [`init`](Self::init).
392 pub async fn init_async(&self) -> Result<(), Error> {
393 let this = self.clone();
394
395 crate::asynchronous::off_thread(move || this.init()).await
396 }
397
398 /// [`init_and_current`](Self::init_and_current), off the async executor.
399 ///
400 /// The pair is what an async `main` writes at startup, and it is where
401 /// splitting it costs most: `init_async().await?` on one line and the
402 /// type named again on the next.
403 ///
404 /// # Errors
405 ///
406 /// The same failures as [`init`](Self::init).
407 pub async fn init_and_current_async(&self) -> Result<std::sync::Arc<T>, Error> {
408 let this = self.clone();
409
410 crate::asynchronous::off_thread(move || this.init_and_current()).await
411 }
412}