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};
14
15use super::Builder;
16
17impl<T: DeserializeOwned> Builder<T> {
18    /// Reads the sources and deserializes, installing nothing.
19    ///
20    /// # Errors
21    ///
22    /// The same failures as any load: a file that will not parse, a missing
23    /// required value, an unsupported extension.
24    pub fn load(&self) -> Result<T, Error> {
25        let value: T = self.with_spec(crate::loader::load)?;
26
27        if let Some(check) = self.validate {
28            check(&value)?;
29        }
30
31        Ok(value)
32    }
33
34    /// Loads and installs as the type's snapshot.
35    ///
36    /// # Errors
37    ///
38    /// Whatever [`load`](Self::load) reports — and, on a builder made with
39    /// [`Builder::new`] rather than a generated `builder()`, the fact that
40    /// there is no storage to install into.
41    pub fn init(&self) -> Result<(), Error> {
42        // Before the installer check: "your cache cannot redact" is the more
43        // specific mistake, and the more dangerous one to leave unexplained.
44        self.check_cache_mode()?;
45
46        let Some(install) = self.install.as_ref() else {
47            return Err(Error::new(
48                ErrorKind::Backend,
49                "this builder is tied to no config type, so there is nowhere \
50                 to install; use `load()` here, or start from the generated \
51                 `builder()` on a `#[dynamic_config]` type",
52            ));
53        };
54
55        let outcome = match self.load() {
56            Ok(value) => {
57                install.install(value);
58                self.write_cache();
59
60                Ok(())
61            }
62            Err(failure) => {
63                let recovered = self.recover(failure)?;
64
65                if let Some(check) = self.validate {
66                    check(&recovered)?;
67                }
68
69                install.install(recovered);
70                crate::log::warning!(
71                    "{}: started from the last known good configuration",
72                    self.key
73                );
74
75                Ok(())
76            }
77        };
78
79        if outcome.is_ok() {
80            if let Some(register) = self.register {
81                register(self);
82            }
83        }
84
85        outcome
86    }
87
88    /// The first half of a grouped reload: load and validate now, install
89    /// later — what [`ReloadGroup`](crate::ReloadGroup) drives.
90    ///
91    /// # Errors
92    ///
93    /// The same failures as [`load`](Self::load); a builder with no
94    /// installer has nothing to commit into.
95    pub fn prepare(&self) -> Result<crate::group::Commit, Error>
96    where
97        T: Send + Sync + 'static,
98    {
99        let Some(install) = self.install.as_ref() else {
100            return Err(Error::new(
101                ErrorKind::Backend,
102                "this builder is tied to no config type, so a prepared \
103                 commit would have nowhere to install",
104            ));
105        };
106
107        let value = self.load()?;
108
109        let install = install.clone();
110
111        Ok(Box::new(move || install.install(value)))
112    }
113
114    /// Refuses a redaction-dependent cache mode on a builder that cannot
115    /// know which fields are secret.
116    fn check_cache_mode(&self) -> Result<(), Error> {
117        if let Some((_, mode)) = &self.cache {
118            if !matches!(mode, CacheMode::Full) && self.secrets.is_none() {
119                return Err(Error::new(
120                    ErrorKind::Backend,
121                    "a redacted or fingerprint cache needs to know which \
122                     fields are secret, and only the generated `builder()` on \
123                     a `#[dynamic_config]` type knows; use that, or \
124                     `CacheMode::Full`, spelled out",
125                ));
126            }
127        }
128
129        Ok(())
130    }
131
132    /// Best-effort, exactly like the attribute's cache: a cache that cannot
133    /// be written is a worse tomorrow, not a broken today.
134    pub(super) fn write_cache(&self) {
135        let Some((path, mode)) = &self.cache else {
136            return;
137        };
138
139        // The same refusal `init` makes, as a structural belt: every path
140        // that writes must hold it, not just the one that happens to run
141        // the check first — a file *marked* redacted with nothing redacted
142        // would be the quiet worst case.
143        if !matches!(mode, CacheMode::Full) && self.secrets.is_none() {
144            crate::log::warning!(
145                "{}: not writing the cache at {path}: a redaction-dependent \
146                 mode needs the generated builder's secret knowledge",
147                self.key
148            );
149
150            return;
151        }
152
153        let secrets = self.secrets.clone().unwrap_or_default();
154        let secret_refs: Vec<&str> = secrets.iter().map(String::as_str).collect();
155
156        let written = self.with_spec(|spec| {
157            let snapshot = crate::loader::snapshot(spec)?;
158
159            #[cfg(feature = "decrypt")]
160            if let Some(encryptor) = &self.cache_encryptor {
161                return crate::cache::write_encrypted(
162                    &snapshot,
163                    Path::new(path),
164                    encryptor.as_ref(),
165                );
166            }
167
168            crate::cache::write(&snapshot, Path::new(path), *mode, &secret_refs)
169        });
170
171        if let Err(error) = written {
172            crate::log::warning!("could not write the configuration cache to {path}: {error}");
173        }
174    }
175
176    /// The last known good configuration, when the sources will not load —
177    /// or the original failure back, when there is nothing to recover from.
178    fn recover(&self, failure: Error) -> Result<T, Error> {
179        let Some((path, mode)) = &self.cache else {
180            return Err(failure);
181        };
182
183        // The configured mode decides, not the file on disk: a value-bearing
184        // cache left behind by an earlier deployment must not resurrect a
185        // configuration the operator deliberately switched away from.
186        // Fingerprint promises to diagnose and still fail.
187        let may_recover = mode.recovers();
188
189        // What the sources resolve to *now*, if they resolve at all — the
190        // drift report needs it, and a parse failure means there is nothing
191        // to compare.
192        let current = self.with_spec(crate::loader::snapshot).ok();
193
194        #[cfg(feature = "decrypt")]
195        let recovered = if let Some(_encryptor) = &self.cache_encryptor {
196            crate::cache::read_encrypted(Path::new(path), current.as_ref())
197        } else {
198            crate::cache::read(Path::new(path), current.as_ref())
199        };
200        #[cfg(not(feature = "decrypt"))]
201        let recovered = crate::cache::read(Path::new(path), current.as_ref());
202
203        match recovered {
204            // Through the loader, not a bare extract: the environment and
205            // `.env` files layer over the cache exactly as they would over
206            // the files, which is what lets a redacted cache work — the
207            // values it dropped come back from wherever they were live.
208            Ok(Recovery::Usable(snapshot)) if may_recover => self
209                .with_spec(|spec| crate::loader::recover::<T>(spec, &snapshot))
210                .map(|(value, _snapshot)| value),
211            Ok(Recovery::Usable(_)) => {
212                crate::log::warning!(
213                    "{}: the cache at {path} holds values, but this builder \
214                     is configured `Fingerprint`, which diagnoses and never \
215                     recovers; refusing to start from it",
216                    self.key
217                );
218
219                Err(failure)
220            }
221            // A fingerprint cannot rebuild a configuration, but it can still
222            // say what moved since the last good state — the diagnosis that
223            // makes the failure actionable at three in the morning.
224            Ok(Recovery::Drift(moved)) => {
225                crate::log::warning!(
226                    "{}: cannot start: {failure}. Since the last good configuration: {}",
227                    self.key,
228                    match moved {
229                        Some(paths) if paths.is_empty() => "nothing detectably moved".to_owned(),
230                        Some(paths) => paths.join(", "),
231                        None => "could not compare — the sources do not resolve".to_owned(),
232                    }
233                );
234
235                Err(failure)
236            }
237            // A cache that will not read cures nothing: the original failure
238            // is the honest answer (the cache's own trouble is logged by
239            // `read` before this returns).
240            Ok(Recovery::Absent) | Err(_) => Err(failure),
241        }
242    }
243
244    /// One reload: load, validate, install, rewrite the cache.
245    ///
246    /// What a watch iteration and a [`RemoteSink`](crate::RemoteSink)'s
247    /// `apply` both do. A failure
248    /// installs nothing — the previous snapshot keeps serving.
249    ///
250    /// # Errors
251    ///
252    /// The same failures as [`load`](Self::load); a builder with no
253    /// installer has nothing to reload into.
254    pub fn reload(&self) -> Result<(), Error> {
255        let Some(install) = self.install.as_ref() else {
256            return Err(Error::new(
257                ErrorKind::Backend,
258                "this builder is tied to no config type, so a reload would \
259                 have nowhere to install",
260            ));
261        };
262
263        install.install(self.load()?);
264        self.write_cache();
265
266        Ok(())
267    }
268}
269
270#[cfg(feature = "async")]
271#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
272// `Sync` joined the bounds when the builder learned to carry a shared
273// cell (`Installer::Cell` holds an `Arc<ConfigCell<T>>`, and moving the
274// builder to the blocking worker moves the cell with it). A config type
275// that is `Send` without `Sync` is a curiosity this crate does not chase.
276impl<T: DeserializeOwned + Send + Sync + 'static> Builder<T> {
277    /// [`load`](Self::load), off the async executor.
278    ///
279    /// # Errors
280    ///
281    /// The same failures as [`load`](Self::load).
282    pub async fn load_async(&self) -> Result<T, Error> {
283        let this = self.clone();
284
285        crate::asynchronous::off_thread(move || this.load()).await
286    }
287
288    /// [`init`](Self::init), off the async executor.
289    ///
290    /// # Errors
291    ///
292    /// The same failures as [`init`](Self::init).
293    pub async fn init_async(&self) -> Result<(), Error> {
294        let this = self.clone();
295
296        crate::asynchronous::off_thread(move || this.init()).await
297    }
298}