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