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}