Skip to main content

dynamic_config/
reload.rs

1//! Why a snapshot was installed, and how the installs since have gone.
2//!
3//! Three questions an operator asks that the crate could always answer and
4//! never did: *why did this change*, *did the last attempt work*, and *how
5//! many have failed since one did*. [`ReloadReason`] is the first,
6//! [`ReloadEvent`] carries it to a hook, and [`ConfigStatus`] is all three
7//! in one cheap struct.
8//!
9//! Nothing here is on the read path. A reason is recorded by the same store
10//! that publishes the snapshot, a failure by the same code that decided not
11//! to publish one — so reading any of it is a load, never a computation,
12//! and `current()` stays the single atomic load it has always been.
13//!
14//! **Values never appear here.** A reason names a *file*, a failure names a
15//! *key path* and an [`ErrorKind`], and a status is counts and timestamps.
16//! Every one of these types exists to be printed into a log, which is
17//! exactly how a configured value escapes, so none of them can hold one.
18
19use std::path::PathBuf;
20use std::sync::Arc;
21use std::time::Instant;
22
23use crate::cell::SnapshotMeta;
24use crate::error::{Error, ErrorKind};
25
26/// What caused a snapshot to be installed.
27///
28/// Recorded at the call site that installs, because nothing downstream can
29/// reconstruct it: by the time a hook runs, a file change and a manual
30/// `reload()` have produced the identical swap.
31///
32/// `#[non_exhaustive]`: the set of things that can install a configuration
33/// grows with the crate, and matching on it must keep compiling when it
34/// does.
35#[derive(Debug, Clone, PartialEq, Eq)]
36#[non_exhaustive]
37pub enum ReloadReason {
38    /// A [`Builder::init`](crate::Builder::init) — the call that establishes
39    /// a configuration, whether or not one was already installed.
40    Initial,
41    /// A watched file changed. Carries the path whose event opened the
42    /// reload's debounce window.
43    ///
44    /// One path, not the set: the debounce collapses a flurry into a single
45    /// reload, and the window that a `..data` symlink swap or a two-file
46    /// edit produces genuinely covers several. The path names *what
47    /// triggered this reload*, which is the question a log line asks; the
48    /// keys that actually moved are
49    /// [`changed_paths`](crate::changed_paths)' job.
50    FileChanged(PathBuf),
51    /// A remote store pushed a document through
52    /// [`RemoteSink::apply`](crate::RemoteSink::apply).
53    RemoteChanged,
54    /// The program installed it: [`reload`](crate::Builder::reload), a
55    /// generated `replace`, [`ConfigCell::store`](crate::ConfigCell::store),
56    /// or a [`ReloadGroup`](crate::ReloadGroup) commit.
57    Manual,
58    /// The last-known-good cache, after the sources refused to load at
59    /// [`init`](crate::Builder::init).
60    Recovered,
61}
62
63impl ReloadReason {
64    /// A short, stable label — the category, without the path.
65    ///
66    /// For a metric dimension or a structured log field, where
67    /// [`FileChanged`](Self::FileChanged)'s path is unbounded cardinality
68    /// and the category is not.
69    #[must_use]
70    pub fn as_str(&self) -> &'static str {
71        match self {
72            Self::Initial => "initial",
73            Self::FileChanged(_) => "file-changed",
74            Self::RemoteChanged => "remote-changed",
75            Self::Manual => "manual",
76            Self::Recovered => "recovered",
77        }
78    }
79}
80
81/// One install, as an event.
82///
83/// What [`on_reload_with`](crate::ConfigCell::on_reload_with) hands a hook,
84/// and what the two-argument [`on_reload`](crate::ConfigCell::on_reload)
85/// cannot say: *why* the snapshot moved, *which* generation it became, and
86/// — through `previous` — that there was nothing before it.
87///
88/// The first install is an event with `previous: None`. The pair form has
89/// nowhere to put that and so does not fire for it at all; this form does,
90/// which is what makes [`ReloadReason::Initial`] reachable from a hook.
91///
92/// # A note on `Debug`
93///
94/// It prints the reason, the metadata and *whether* there was a previous
95/// snapshot — never the snapshots themselves, however `T` renders. An event
96/// is a diagnostic, a `{:?}` of one lands in a log, and a configuration
97/// holds passwords. Reach for the [`current`](Self::current) field when you
98/// want the values; that is a deliberate second step.
99#[non_exhaustive]
100pub struct ReloadEvent<T> {
101    /// The snapshot that was serving until now, or `None` when this install
102    /// is the first — there was no configuration before it.
103    pub previous: Option<Arc<T>>,
104    /// The snapshot now serving.
105    pub current: Arc<T>,
106    /// What caused this install.
107    pub reason: ReloadReason,
108    /// The generation this install became, and when it landed.
109    pub meta: SnapshotMeta,
110}
111
112impl<T> ReloadEvent<T> {
113    /// Not public API: built by the cell that dispatches it.
114    pub(crate) fn new(
115        previous: Option<Arc<T>>,
116        current: Arc<T>,
117        reason: ReloadReason,
118        meta: SnapshotMeta,
119    ) -> Self {
120        Self {
121            previous,
122            current,
123            reason,
124            meta,
125        }
126    }
127}
128
129// Hand-written: `#[derive(Clone)]` would demand `T: Clone`, and an `Arc<T>`
130// clones whatever `T` is.
131impl<T> Clone for ReloadEvent<T> {
132    fn clone(&self) -> Self {
133        Self {
134            previous: self.previous.clone(),
135            current: Arc::clone(&self.current),
136            reason: self.reason.clone(),
137            meta: self.meta,
138        }
139    }
140}
141
142impl<T> std::fmt::Debug for ReloadEvent<T> {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        f.debug_struct("ReloadEvent")
145            .field("reason", &self.reason)
146            .field("generation", &self.meta.generation)
147            // Presence, not content: see the type's documentation.
148            .field("had_previous", &self.previous.is_some())
149            .finish_non_exhaustive()
150    }
151}
152
153/// A reload that did not install anything.
154///
155/// The category and the key path, and deliberately not the message: an
156/// error's `Display` is value-free by policy and enforced by
157/// `tests/security.rs`, but a struct that *stores* free text is one careless
158/// `Error::new` away from carrying a value into every log that prints a
159/// status. What an operator needs to act — when, what kind, which key — is
160/// none of it free text.
161#[derive(Debug, Clone, PartialEq, Eq)]
162#[non_exhaustive]
163pub struct FailureStatus {
164    /// When the failure was recorded.
165    pub at: Instant,
166    /// The failure's category.
167    pub kind: ErrorKind,
168    /// The dotted key path it was reported at; empty when the failure
169    /// belongs to the load as a whole rather than to one key.
170    pub path: String,
171}
172
173impl FailureStatus {
174    /// Not public API: built where the failure is recorded.
175    pub(crate) fn of(error: &Error) -> Self {
176        Self {
177            at: Instant::now(),
178            kind: error.kind(),
179            path: error.path(),
180        }
181    }
182}
183
184/// What is true of a configuration right now, for an operator asking.
185///
186/// Every field is *recorded* where it happens rather than recomputed here,
187/// so building one is a handful of atomic loads: no I/O, no source is
188/// re-read, and nothing here can block. That is the constraint — an
189/// exporter calling this per scrape must cost nothing.
190///
191/// It is assembled from several loads, so a reload landing mid-call can
192/// leave one field an install ahead of another. The same trade
193/// [`SnapshotMeta`] makes, for the same reason: for operators, not for
194/// correctness.
195///
196/// # What it does not carry
197///
198/// **No values, by construction** — see [`FailureStatus`]. And no *source*
199/// list: which sources would be read is a question about the next load, and
200/// [`check`](crate::check) already answers it against the sources rather
201/// than from a cache of them that could go stale. Nor is there a
202/// `last_success`: an install *is* the success, so
203/// [`loaded_at`](Self::loaded_at) is when the last one was.
204///
205/// [`loaded_at`]: Self::loaded_at
206#[derive(Debug, Clone, PartialEq, Eq)]
207#[non_exhaustive]
208pub struct ConfigStatus {
209    /// Installs since the process started; zero before the first.
210    pub generation: u64,
211    /// When the serving snapshot was installed — which is also when the
212    /// last successful load was. `None` before the first install.
213    pub loaded_at: Option<Instant>,
214    /// Why the serving snapshot was installed. `None` before the first.
215    pub last_reason: Option<ReloadReason>,
216    /// The most recent reload that installed nothing, if there has been
217    /// one. Kept after a later success: it is history, and
218    /// [`consecutive_failures`](Self::consecutive_failures) is the health.
219    pub last_failure: Option<FailureStatus>,
220    /// Failures since the last install. **Zero means healthy.**
221    pub consecutive_failures: u32,
222}
223
224impl ConfigStatus {
225    /// Whether the last attempt to load installed something.
226    ///
227    /// Nothing more than `consecutive_failures == 0`, spelled the way the
228    /// question is asked. True before the first load too — nothing has
229    /// failed yet.
230    #[must_use]
231    pub fn is_healthy(&self) -> bool {
232        self.consecutive_failures == 0
233    }
234
235    /// How long ago the serving snapshot was installed.
236    ///
237    /// `None` before the first install. This is why the recorded instant is
238    /// monotonic: a wall clock going backwards under NTP would make a fresh
239    /// configuration look stale.
240    #[must_use]
241    pub fn stale_for(&self) -> Option<std::time::Duration> {
242        self.loaded_at.map(|at| at.elapsed())
243    }
244}