Skip to main content

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}