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}