dynamic_config/remote/status.rs
1//! What is true of a remote source right now, for an operator asking.
2//!
3//! The fetch half of the picture `ConfigStatus` starts, in the same
4//! vocabulary rather than a second one. What it does *not* carry is the
5//! point: no document, no key, and no description of the store — a store
6//! URL routinely embeds `user:password@host`.
7
8use std::time::{Duration, Instant};
9
10use crate::reload::FailureStatus;
11
12/// What is true of a remote source right now, for an operator asking.
13///
14/// The fetch half of the picture [`ConfigStatus`](crate::ConfigStatus)
15/// starts, and deliberately the *same* picture rather than a second one:
16/// the same [`FailureStatus`] type, the same `consecutive_failures` meaning
17/// zero-is-healthy, the same recorded-where-it-happens rule, and the same
18/// rendering through [`telemetry::Exposition`](crate::telemetry::Exposition).
19/// Two vocabularies for one question is how two surfaces come to disagree
20/// after the first bug.
21///
22/// The two do not overlap, and the split is worth stating because it is the
23/// distinction an operator is actually asking about:
24///
25/// | Question | Where it is answered |
26/// |---|---|
27/// | did the **store** answer | here |
28/// | did the **document** install | [`ConfigStatus`](crate::ConfigStatus) |
29///
30/// A fetch that returned an unchanged document is a success here and is not
31/// an install there, which is exactly the case neither surface could report
32/// before this type existed.
33///
34/// # What it does not carry
35///
36/// **No document, no key, and no description of the store.** A store's
37/// description is its URL, and a store URL routinely embeds
38/// `user:password@host` — so nothing here is derived from
39/// [`describe`](crate::Remote::describe), and the name a metric is labelled with
40/// is the caller's own, exactly as it is for a `ConfigStatus`.
41#[derive(Debug, Clone, Default, PartialEq, Eq)]
42#[non_exhaustive]
43pub struct RemoteStatus {
44 /// Documents this slot has received since the process started, whether
45 /// pulled by [`refresh`](crate::Remote::refresh) or pushed through
46 /// [`RemoteSink::apply`](crate::RemoteSink::apply).
47 pub fetches: u64,
48 /// When the last of them arrived. `None` before the first.
49 pub last_fetch: Option<Instant>,
50 /// How long the last *pulled* fetch took.
51 ///
52 /// `None` before the first pull, and `None` again after a document
53 /// arrives by push: a watch loop's own round trip is timed by the store
54 /// crate that made it, and reporting the previous pull's duration beside
55 /// a push's timestamp would be a number that is not about the fetch it
56 /// appears to describe.
57 pub last_fetch_duration: Option<Duration>,
58 /// The most recent fetch that returned nothing, if there has been one.
59 /// Kept after a later success: it is history, and
60 /// [`consecutive_failures`](Self::consecutive_failures) is the health.
61 pub last_failure: Option<FailureStatus>,
62 /// Fetches that returned nothing since one returned a document.
63 /// **Zero means healthy.**
64 pub consecutive_failures: u32,
65}
66
67impl RemoteStatus {
68 /// Whether the store answered the last time it was asked.
69 ///
70 /// Three states rather than two, and the third is the point: `None`
71 /// before anything has been asked of the store at all. A source that has
72 /// been installed and never fetched is not *down* — reporting it as down
73 /// is how a scrape at startup pages somebody — so the metric is absent
74 /// rather than zero, exactly as `last_success_seconds` is.
75 #[must_use]
76 pub fn reachable(&self) -> Option<bool> {
77 if self.fetches == 0 && self.consecutive_failures == 0 {
78 return None;
79 }
80
81 Some(self.consecutive_failures == 0)
82 }
83
84 /// A status with nothing recorded yet.
85 ///
86 /// `const`, because [`Remote::new`] is: a `Remote` lives in a `static`.
87 /// `Default` cannot be, which is the only reason this exists.
88 pub(super) const fn empty() -> Self {
89 Self {
90 fetches: 0,
91 last_fetch: None,
92 last_fetch_duration: None,
93 last_failure: None,
94 consecutive_failures: 0,
95 }
96 }
97
98 /// How long ago the last document arrived from the store.
99 ///
100 /// `None` before the first. Monotonic, for the reason
101 /// [`ConfigStatus::stale_for`](crate::ConfigStatus::stale_for) is: a wall
102 /// clock going backwards under NTP would make a fresh fetch look stale.
103 #[must_use]
104 pub fn stale_for(&self) -> Option<Duration> {
105 self.last_fetch.map(|at| at.elapsed())
106 }
107}