Skip to main content

dynamic_config/remote/
sink.rs

1//! The fenced door a watch loop pushes through.
2//!
3//! A sink remembers which source was installed when it was taken, and
4//! refuses a delivery from a store that has since been replaced — by
5//! construction rather than by asking a loop to please stop first.
6
7use super::{Fetched, Remote, RemoteStatus};
8use crate::error::Error;
9
10/// A fenced door for a remote watch loop's pushes.
11///
12/// Created by the generated `remote_sink()` *after* the source is
13/// installed, it remembers which source that was. [`apply`](Self::apply)
14/// installs the document and reloads — unless the source has since been
15/// replaced, in which case it refuses: a watch loop serving yesterday's
16/// store cannot overwrite today's, by construction rather than by the old
17/// documentation's request to please stop the loop first.
18///
19/// Cheap to clone; each wiring of a watch loop should take its own —
20/// **once, where the loop starts**. A sink taken per delivery reads the
21/// generation of that moment and fences nothing.
22#[derive(Clone, Copy)]
23pub struct RemoteSink {
24    remote: &'static Remote,
25    generation: u64,
26    reload: fn() -> Result<(), Error>,
27    name: &'static str,
28}
29
30impl RemoteSink {
31    /// Not public API: called by the generated `remote_sink()`.
32    #[doc(hidden)]
33    #[must_use]
34    pub fn new(
35        remote: &'static Remote,
36        reload: fn() -> Result<(), Error>,
37        name: &'static str,
38    ) -> Self {
39        Self {
40            remote,
41            generation: remote.generation(),
42            reload,
43            name,
44        }
45    }
46
47    /// How the fetches from the store behind this sink have gone.
48    ///
49    /// The door a `#[dynamic_config]` type has to its
50    /// [`RemoteStatus`](crate::RemoteStatus): the slot itself is generated private, and a sink is
51    /// the public handle on it — which is also where the question belongs,
52    /// since a sink is what a watch loop holds.
53    ///
54    /// Taking a sink *only* to read this is fine and costs an atomic load:
55    /// the generation a sink captures fences
56    /// [`apply`](Self::apply) and nothing else. A loop that will deliver
57    /// documents still takes its own, once, where it starts.
58    ///
59    /// ```no_run
60    /// # struct DbConfig;
61    /// # impl DbConfig {
62    /// #     fn remote_sink() -> dynamic_config::RemoteSink { unimplemented!() }
63    /// # }
64    /// let status = DbConfig::remote_sink().status();
65    ///
66    /// if status.reachable() == Some(false) {
67    ///     eprintln!("the store has stopped answering");
68    /// }
69    /// ```
70    ///
71    /// With the `telemetry` feature, `Exposition::add_remote` renders the
72    /// same status as Prometheus text; see
73    /// [the telemetry module](crate::telemetry). The example above stays
74    /// feature-free on purpose, because this method is not.
75    #[must_use]
76    pub fn status(&self) -> RemoteStatus {
77        self.remote.status()
78    }
79
80    /// Reports an attempt to reach the store that came back with nothing.
81    ///
82    /// A watch loop is the half of a store this crate cannot see.
83    /// [`apply`](Self::apply) records a delivery, so a *working* watch keeps
84    /// [`RemoteStatus`](crate::RemoteStatus) current — but a loop whose stream broke, whose
85    /// blocking query is erroring or whose credential was refused delivers
86    /// nothing, and would otherwise say nothing: `reachable` would report the
87    /// last delivery rather than the last attempt, and a store that stopped
88    /// answering an hour ago would look healthy until something called
89    /// `refresh`.
90    ///
91    /// What it moves is deliberately narrow — the failure streak and the last
92    /// failure, and nothing else. `fetches`, `last_fetch` and
93    /// `last_fetch_duration` are left alone, so
94    /// `dynamic_config_remote_last_fetch_seconds` keeps *ageing* while
95    /// `dynamic_config_remote_up` goes to zero, which is the pair an alert
96    /// wants. The stored document is untouched: a failed attempt is no reason
97    /// to stop serving what the last good one produced.
98    ///
99    /// Fenced on the sink's generation exactly as [`apply`](Self::apply) is,
100    /// so a loop still winding down after its source was replaced cannot
101    /// charge its failures to the replacement. A stale report is dropped
102    /// silently, and there is nothing to handle: a loop must never have to
103    /// deal with a failure to report a failure.
104    ///
105    /// The error's kind and key path are recorded and nothing else — a
106    /// store's address never enters a [`RemoteStatus`](crate::RemoteStatus), for the reason its
107    /// own documentation gives.
108    pub fn failed(&self, error: &Error) {
109        // The fence is inside `record_fetch_failure`, under the same lock
110        // that reads the generation: a check here and a write there would
111        // leave a window for a replacement to land between them.
112        self.remote.record_fetch_failure(error, self.generation);
113    }
114
115    /// Installs a document the watch pushed, and reloads.
116    ///
117    /// Everything a file change would do happens here too — validation,
118    /// the reload hooks, the cache — because it is the same code path,
119    /// reached with a document instead of a filesystem event. A failure
120    /// leaves the previous snapshot serving.
121    ///
122    /// # Errors
123    ///
124    /// If the source has been replaced since this sink was created —
125    /// checked before the reload *and again after it*, because a
126    /// replacement can land while the reload runs — or if the resulting
127    /// configuration does not load or validate.
128    pub fn apply(&self, document: Fetched) -> Result<(), Error> {
129        self.remote.install_if(self.generation, document)?;
130
131        let outcome = (self.reload)();
132
133        // The reload read the slot as it stood while it ran. If the source
134        // was replaced mid-flight — after `install_if` said yes — what just
135        // installed may derive from this sink's document even though the
136        // fence now belongs to the replacement. Reload once more against
137        // the slot as it stands, so the replacement's state has the last
138        // word, then refuse like any other stale push.
139        if self.remote.generation() != self.generation {
140            let _ = (self.reload)();
141
142            let error = Error::new(
143                crate::ErrorKind::Backend,
144                "the remote source this sink was created for was replaced \
145                 while its delivery reloaded; the replacement's state was \
146                 restored — stop the old watch loop and take a fresh sink \
147                 from `remote_sink()`",
148            );
149            crate::__log_remote_failure(self.name, &error);
150
151            return Err(error);
152        }
153
154        match outcome {
155            Ok(()) => {
156                crate::__log_remote_reload(self.name, None);
157
158                Ok(())
159            }
160            Err(error) => {
161                crate::__log_remote_failure(self.name, &error);
162
163                Err(error)
164            }
165        }
166    }
167}
168
169impl std::fmt::Debug for RemoteSink {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        f.debug_struct("RemoteSink")
172            .field("config", &self.name)
173            .field("generation", &self.generation)
174            .finish_non_exhaustive()
175    }
176}