dynamic_config/remote/source.rs
1//! What a store hands back, and the two traits a store implements.
2//!
3//! Two traits rather than one because a client is either async to begin
4//! with or it is not, and making the wrong half pretend costs a `block_on`
5//! in somebody's runtime. Both are object-safe: a configuration type holds
6//! one without being generic over it.
7
8use std::time::Duration;
9
10use crate::error::Error;
11use crate::source::Format;
12
13use super::watch::{Pace, Watching};
14
15/// How a store finds out that its document changed.
16///
17/// What a store answers here is a fact about the protocol, not a promise
18/// about the implementation: it tells a caller what a watch is going to
19/// cost, so an agent can decide whether to run one at all and an operator
20/// can read why a change took as long as it did.
21///
22/// ```text
23/// Native the store says so a blocking query, a stream, a subscription
24/// Conditional the store answers cheaply a version, an ETag, a revision — a header, not a document
25/// Interval nothing but re-reading the whole document, on a timer
26/// ```
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28#[non_exhaustive]
29pub enum WatchCapability {
30 /// The store pushes. A change is delivered as soon as it happens, and
31 /// the interval is only a resync — a stream can stall without saying so.
32 Native,
33 /// The store answers "has it changed?" without sending the document.
34 /// A poll costs a round trip and almost no bytes.
35 Conditional,
36 /// Nothing but re-reading the whole document on a timer.
37 Interval,
38}
39
40impl std::fmt::Display for WatchCapability {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.write_str(match self {
43 Self::Native => "native",
44 Self::Conditional => "conditional",
45 Self::Interval => "interval",
46 })
47 }
48}
49
50/// A document a remote store handed back.
51#[derive(Clone, PartialEq, Eq)]
52pub struct Fetched {
53 /// The document text, in `format`.
54 pub text: String,
55 /// How to parse it.
56 pub format: Format,
57}
58
59impl Fetched {
60 /// A document and the format it is written in.
61 #[must_use]
62 pub fn new(text: impl Into<String>, format: Format) -> Self {
63 Self {
64 text: text.into(),
65 format,
66 }
67 }
68}
69
70// The document is the one thing a `Debug` of this type must never print:
71// a remote store's flagship use case is serving secrets, and `Fetched` is
72// what every watch callback receives — one `tracing::debug!(?document)` away
73// from a log. The length is enough to debug with.
74impl std::fmt::Debug for Fetched {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.debug_struct("Fetched")
77 .field("format", &self.format)
78 .field("bytes", &self.text.len())
79 .finish()
80 }
81}
82
83/// A remote store that can be read without an async runtime.
84///
85/// The right trait for anything with a plain HTTP API — Consul and Vault both
86/// are — because implementing it needs no runtime and using it needs no
87/// runtime either. `fetch` may block; it is called from
88/// `refresh_remote()`, never from `load()`.
89pub trait RemoteSource: Send + Sync + 'static {
90 /// Reads the current document.
91 ///
92 /// # Errors
93 ///
94 /// Whatever going wrong looks like for this store. Use
95 /// [`Error::remote`](crate::Error::remote) so the failure is categorised
96 /// consistently, or [`Error::auth`](crate::Error::auth) for a credential
97 /// the store itself refused — that is the distinction a watch loop backs
98 /// off on rather than stopping.
99 fn fetch(&self) -> Result<Fetched, Error>;
100
101 /// How to name this source in an error or a report.
102 fn describe(&self) -> String;
103
104 /// How this store learns that its document changed.
105 ///
106 /// [`Interval`](WatchCapability::Interval) unless a store says
107 /// otherwise, which is the honest default: a store that has not been
108 /// asked the question has no push to offer.
109 fn watch_capability(&self) -> WatchCapability {
110 WatchCapability::Interval
111 }
112
113 /// Watches until the handle is dropped, calling `on_change` with every
114 /// document that differs from the last one delivered.
115 ///
116 /// **Override this** with the store's own mechanism — a blocking query,
117 /// a stream, a subscription — and say so in
118 /// [`watch_capability`](Self::watch_capability). An override may ignore
119 /// `interval`: a store that reports
120 /// [`Native`](WatchCapability::Native) gets its resync from
121 /// [`Remote::watch`](crate::Remote::watch), which reads on the interval
122 /// alongside the store's own watch. That is not belt and braces — the
123 /// failure mode of a stream is *silence*, and a subscription the broker
124 /// forgot looks exactly like a store where nothing has changed.
125 ///
126 /// The default polls: fetch, deliver anything new, wait, repeat. The
127 /// waits are spread so a fleet does not poll in lockstep, and they grow
128 /// after a failure so a store that is down is not hammered by everything
129 /// that depends on it — [`Pace`] is that policy, and an implementation
130 /// with its own loop should use it rather than sleep a flat interval.
131 ///
132 /// Called from a thread the caller owns. It returns when the watch is
133 /// stopped, or when `on_change` refuses.
134 ///
135 /// # Errors
136 ///
137 /// If `on_change` refuses a document. A *fetch* failing is not an error
138 /// here: a watch outlives an outage by design, so it is backed off from
139 /// rather than returned.
140 ///
141 /// **Nothing here records it.** A source is handed a store and a
142 /// callback; the status a [`Remote`](crate::Remote) keeps is not
143 /// reachable from either, so a loop that wants
144 /// `status().reachable()` to tell the truth through an outage reports
145 /// failures itself — [`RemoteSink::failed`](crate::RemoteSink::failed)
146 /// is that call, and the store crates' `reporting_to` wires it. Said
147 /// here because the alternative reading is expensive: a watch that has
148 /// been failing for an hour while its status says the store is fine.
149 fn watch(
150 &self,
151 watching: &Watching,
152 interval: Duration,
153 on_change: &mut dyn FnMut(Fetched) -> Result<(), Error>,
154 ) -> Result<(), Error> {
155 let mut pace = Pace::new(interval);
156 let mut last: Option<Fetched> = None;
157
158 while watching.keep_going() {
159 match self.fetch() {
160 Ok(fetched) => {
161 pace.succeeded();
162
163 if last.as_ref() != Some(&fetched) {
164 last = Some(fetched.clone());
165 on_change(fetched)?;
166 }
167 }
168 // Swallowed on purpose: a watch is what keeps a program
169 // running through an outage, and a store that is down is a
170 // reason to wait longer rather than to stop watching.
171 Err(_) => pace.failed(),
172 }
173
174 pace.wait(watching);
175 }
176
177 Ok(())
178 }
179}
180
181/// A remote store that is read asynchronously.
182///
183/// The right trait for a client that is async to begin with — etcd speaks gRPC
184/// and NATS is a streaming protocol, so both are. Used through
185/// `refresh_remote_async().await`.
186///
187/// The lifetime-bound boxed future rather than `async fn`: this trait is
188/// object-safe on purpose, so a configuration type can hold one without being
189/// generic over it.
190#[cfg(feature = "async")]
191#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
192pub trait AsyncRemoteSource: Send + Sync + 'static {
193 /// Reads the current document.
194 ///
195 /// # Errors
196 ///
197 /// As [`RemoteSource::fetch`].
198 fn fetch(
199 &self,
200 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Fetched, Error>> + Send + '_>>;
201
202 /// How to name this source in an error or a report.
203 fn describe(&self) -> String;
204
205 /// How this store learns that its document changed.
206 ///
207 /// As [`RemoteSource::watch_capability`].
208 fn watch_capability(&self) -> WatchCapability {
209 WatchCapability::Interval
210 }
211
212 /// Watches until the future is dropped, calling `on_change` with every
213 /// document that differs from the last one delivered.
214 ///
215 /// As [`RemoteSource::watch`], with two differences that matter.
216 /// Cancellation is dropping the future, so a `Watching` is accepted but
217 /// an async watch does not need one. And the resync a native store gets
218 /// for free on the blocking side is the caller's here: an async caller
219 /// has a runtime, and racing a timer against this future is a line of
220 /// its own code rather than a thread this crate would have to spawn.
221 ///
222 /// **The default polls only with the `tokio` feature on.** This crate
223 /// picks no runtime, and a poll needs a timer — so with the feature off
224 /// the default refuses, naming the store and saying what to do about
225 /// it. That is rarely the interesting case: a store is async because
226 /// its protocol is, and a streaming protocol has a watch of its own to
227 /// override this with.
228 ///
229 /// # Errors
230 ///
231 /// If `on_change` refuses a document, or if this build has no timer and
232 /// the store did not override this.
233 fn watch<'a>(
234 &'a self,
235 watching: &'a Watching,
236 interval: Duration,
237 on_change: &'a mut (dyn FnMut(Fetched) -> Result<(), Error> + Send),
238 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), Error>> + Send + 'a>> {
239 Box::pin(async move {
240 #[cfg(not(feature = "tokio"))]
241 {
242 let _ = (watching, interval, on_change);
243
244 Err(Error::new(
245 crate::ErrorKind::Remote,
246 format!(
247 "`{}` has no watch of its own, and this build has no timer to poll it \
248 with; add features = [\"tokio\"] to your dynamic-config dependency, \
249 or call `refresh_remote_async` on a timer of your own",
250 self.describe()
251 ),
252 ))
253 }
254
255 #[cfg(feature = "tokio")]
256 {
257 let mut pace = Pace::new(interval);
258 let mut last: Option<Fetched> = None;
259
260 while watching.keep_going() {
261 match self.fetch().await {
262 Ok(fetched) => {
263 pace.succeeded();
264
265 if last.as_ref() != Some(&fetched) {
266 last = Some(fetched.clone());
267 on_change(fetched)?;
268 }
269 }
270 // Swallowed on purpose, as in the blocking twin.
271 Err(_) => pace.failed(),
272 }
273
274 tokio::time::sleep(pace.next_wait()).await;
275 }
276
277 Ok(())
278 }
279 })
280 }
281}