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