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}