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}