Skip to main content

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}