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