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 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(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(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 + 'static,
98    {
99        let Some(install) = self.install 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        Ok(Box::new(move || install(value)))
110    }
111
112    /// Refuses a redaction-dependent cache mode on a builder that cannot
113    /// know which fields are secret.
114    fn check_cache_mode(&self) -> Result<(), Error> {
115        if let Some((_, mode)) = &self.cache {
116            if !matches!(mode, CacheMode::Full) && self.secrets.is_none() {
117                return Err(Error::new(
118                    ErrorKind::Backend,
119                    "a redacted or fingerprint cache needs to know which \
120                     fields are secret, and only the generated `builder()` on \
121                     a `#[dynamic_config]` type knows; use that, or \
122                     `CacheMode::Full`, spelled out",
123                ));
124            }
125        }
126
127        Ok(())
128    }
129
130    /// Best-effort, exactly like the attribute's cache: a cache that cannot
131    /// be written is a worse tomorrow, not a broken today.
132    pub(super) fn write_cache(&self) {
133        let Some((path, mode)) = &self.cache else {
134            return;
135        };
136
137        // The same refusal `init` makes, as a structural belt: every path
138        // that writes must hold it, not just the one that happens to run
139        // the check first — a file *marked* redacted with nothing redacted
140        // would be the quiet worst case.
141        if !matches!(mode, CacheMode::Full) && self.secrets.is_none() {
142            crate::log::warning!(
143                "{}: not writing the cache at {path}: a redaction-dependent \
144                 mode needs the generated builder's secret knowledge",
145                self.key
146            );
147
148            return;
149        }
150
151        let secrets = self.secrets.clone().unwrap_or_default();
152        let secret_refs: Vec<&str> = secrets.iter().map(String::as_str).collect();
153
154        let written = self.with_spec(|spec| {
155            let snapshot = crate::loader::snapshot(spec)?;
156
157            crate::cache::write(&snapshot, Path::new(path), *mode, &secret_refs)
158        });
159
160        if let Err(error) = written {
161            crate::log::warning!("could not write the configuration cache to {path}: {error}");
162        }
163    }
164
165    /// The last known good configuration, when the sources will not load —
166    /// or the original failure back, when there is nothing to recover from.
167    fn recover(&self, failure: Error) -> Result<T, Error> {
168        let Some((path, mode)) = &self.cache else {
169            return Err(failure);
170        };
171
172        // The configured mode decides, not the file on disk: a value-bearing
173        // cache left behind by an earlier deployment must not resurrect a
174        // configuration the operator deliberately switched away from.
175        // Fingerprint promises to diagnose and still fail.
176        let may_recover = mode.recovers();
177
178        // What the sources resolve to *now*, if they resolve at all — the
179        // drift report needs it, and a parse failure means there is nothing
180        // to compare.
181        let current = self.with_spec(crate::loader::snapshot).ok();
182
183        match crate::cache::read(Path::new(path), current.as_ref()) {
184            // Through the loader, not a bare extract: the environment and
185            // `.env` files layer over the cache exactly as they would over
186            // the files, which is what lets a redacted cache work — the
187            // values it dropped come back from wherever they were live.
188            Ok(Recovery::Usable(snapshot)) if may_recover => self
189                .with_spec(|spec| crate::loader::recover::<T>(spec, &snapshot))
190                .map(|(value, _snapshot)| value),
191            Ok(Recovery::Usable(_)) => {
192                crate::log::warning!(
193                    "{}: the cache at {path} holds values, but this builder \
194                     is configured `Fingerprint`, which diagnoses and never \
195                     recovers; refusing to start from it",
196                    self.key
197                );
198
199                Err(failure)
200            }
201            // A fingerprint cannot rebuild a configuration, but it can still
202            // say what moved since the last good state — the diagnosis that
203            // makes the failure actionable at three in the morning.
204            Ok(Recovery::Drift(moved)) => {
205                crate::log::warning!(
206                    "{}: cannot start: {failure}. Since the last good configuration: {}",
207                    self.key,
208                    match moved {
209                        Some(paths) if paths.is_empty() => "nothing detectably moved".to_owned(),
210                        Some(paths) => paths.join(", "),
211                        None => "could not compare — the sources do not resolve".to_owned(),
212                    }
213                );
214
215                Err(failure)
216            }
217            // A cache that will not read cures nothing: the original failure
218            // is the honest answer (the cache's own trouble is logged by
219            // `read` before this returns).
220            Ok(Recovery::Absent) | Err(_) => Err(failure),
221        }
222    }
223
224    /// One reload: load, validate, install, rewrite the cache.
225    ///
226    /// What a watch iteration and a [`RemoteSink`](crate::RemoteSink)'s
227    /// `apply` both do. A failure
228    /// installs nothing — the previous snapshot keeps serving.
229    ///
230    /// # Errors
231    ///
232    /// The same failures as [`load`](Self::load); a builder with no
233    /// installer has nothing to reload into.
234    pub fn reload(&self) -> Result<(), Error> {
235        let Some(install) = self.install else {
236            return Err(Error::new(
237                ErrorKind::Backend,
238                "this builder is tied to no config type, so a reload would \
239                 have nowhere to install",
240            ));
241        };
242
243        install(self.load()?);
244        self.write_cache();
245
246        Ok(())
247    }
248}
249
250#[cfg(feature = "async")]
251#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
252impl<T: DeserializeOwned + Send + 'static> Builder<T> {
253    /// [`load`](Self::load), off the async executor.
254    ///
255    /// # Errors
256    ///
257    /// The same failures as [`load`](Self::load).
258    pub async fn load_async(&self) -> Result<T, Error> {
259        let this = self.clone();
260
261        crate::asynchronous::off_thread(move || this.load()).await
262    }
263
264    /// [`init`](Self::init), off the async executor.
265    ///
266    /// # Errors
267    ///
268    /// The same failures as [`init`](Self::init).
269    pub async fn init_async(&self) -> Result<(), Error> {
270        let this = self.clone();
271
272        crate::asynchronous::off_thread(move || this.init()).await
273    }
274}