dynamic_config/remote/mod.rs
1//! Configuration served from somewhere other than this machine.
2//!
3//! etcd, Consul, NATS, Vault — a document fetched over a network and merged
4//! like a file. The companion crates implement one of the two traits here;
5//! this module is the part that never changes.
6//!
7//! ## Fetching is explicit
8//!
9//! A remote source is **not** read on every `load()`. Configuration is read on
10//! nearly every request; a network round trip there would be indefensible, and
11//! it is also what forces every async question to become a blocking one.
12//!
13//! ```text
14//! refresh_remote() → fetch, keep the document
15//! load() → merge the kept document, no I/O
16//! ```
17//!
18//! That one decision is what lets a blocking source and an async source sit
19//! side by side without `block_on` anywhere, and without the crate caring which
20//! runtime — if any — the program is built on.
21//!
22//! ## Where it sits
23//!
24//! ```text
25//! defaults < files < remote < environment < flags < overrides
26//! ```
27//!
28//! Above the files, because centrally distributed configuration should beat
29//! what a package shipped. Below the environment, because a machine's own
30//! settings should beat what a central store thinks it wants.
31//!
32//! ## Timeouts
33//!
34//! Every companion crate that takes a `with_timeout` means the same thing by
35//! it: **the deadline for a single fetch attempt, excluding retries the
36//! underlying client performs.** One sentence, seven stores, whatever each
37//! client happens to call the knob underneath.
38//!
39//! The exclusion is the part that surprises. Where a client retries beneath us
40//! — the AWS SDK does, by default — a fetch can take the timeout multiplied by
41//! the attempt count, and that store's README says so rather than quietly
42//! tuning the retries away.
43//!
44//! ## Two ways a fetch fails
45//!
46//! [`ErrorKind::Remote`] is the store being unreachable; [`ErrorKind::Auth`] is
47//! a credential it refused. The difference is exactly what a watch loop needs:
48//! the first may fix itself while the loop waits, the second will not. So a
49//! store crate reaches for [`Error::auth`] only where the store's own answer
50//! says so — a 401, a 403, a token that could not be replaced — and stays on
51//! [`Error::remote`] wherever a proxy could have been the one talking.
52//!
53//! ## What a fetch reports about itself
54//!
55//! [`Remote`] records a [`RemoteStatus`](crate::RemoteStatus) — how many documents have arrived,
56//! when the last one did, how long the last pull took, and how many fetches
57//! have returned nothing since one returned a document. It is the fetch half
58//! of the picture [`ConfigStatus`](crate::ConfigStatus) starts, in the same
59//! vocabulary rather than a second one: *did the store answer* here, *did the
60//! document install* there.
61//!
62//! Neither the document nor the store's description can reach it. A store's
63//! description is its URL and a store URL routinely embeds
64//! `user:password@host`, so nothing derived from
65//! [`describe`](crate::Remote::describe) is recorded, spanned or labelled — the name
66//! a metric carries is supplied by whoever renders it, exactly as it is for a
67//! `ConfigStatus`.
68//!
69//! With the `tracing` feature a pull is also a `dynamic_config.fetch` span
70//! around the round trip, with an event inside it carrying the outcome and,
71//! on a failure, the [`ErrorKind`]. Nothing is on the read path: `load()`
72//! reads [`Remote::document`], which none of this touches.
73//!
74//! ## Watching
75//!
76//! Polling a store on a timer works and is what [`Vault`] has to do, but three
77//! of the four can tell you the moment a value moves — etcd has a watch stream,
78//! NATS KV has one too, and Consul answers a blocking query. Each companion
79//! crate owns that loop, because a watch is long-lived and protocol-shaped in a
80//! way a single trait cannot honestly cover.
81//!
82//! What the loop pushes through is here: a document arrives, [`Remote::install`]
83//! puts it in the slot, and a [`RemoteSink`](crate::RemoteSink)'s `apply` reloads exactly the way
84//! a file change does — hooks, diffing, validation, the cache.
85//!
86//! The two halves are cancelled differently, and neither imposes a runtime:
87//!
88//! - **An async loop is a future.** Drop it and the watch stops. That is the
89//! whole cancellation story, and it works on any executor.
90//! - **A blocking loop is a thread**, which cannot be dropped from outside, so
91//! it takes a [`Watching`] and checks it between requests. The caller holds
92//! the matching [`RemoteWatch`].
93//!
94//! [`Vault`]: https://docs.rs/dynamic-config-vault
95//!
96//! ## The files
97//!
98//! One concern each, and the split follows the vocabulary rather than the
99//! line count: `Fetched` and the two traits are what a *store* implements
100//! (`source`); `RemoteStatus` is what an operator reads (`status`);
101//! `Remote` is the slot a configuration type owns (here); `RemoteSink` is
102//! the door a watch loop pushes through (`sink`); and the blocking watch
103//! handle is `watch`. Every name below is re-exported at the crate root
104//! exactly where it was.
105
106mod sink;
107mod source;
108mod status;
109mod watch;
110
111pub use sink::RemoteSink;
112#[cfg(feature = "async")]
113pub use source::AsyncRemoteSource;
114pub use source::{Fetched, Lease, RemoteSource, RenewableSource, Revision, WatchCapability};
115pub use status::RemoteStatus;
116pub use watch::{Pace, RemoteWatch, Watching};
117
118use std::sync::Arc;
119
120use crate::sync::Mutex;
121use std::time::{Duration, Instant};
122
123use crate::error::{Error, ErrorKind};
124use crate::reload::FailureStatus;
125
126/// The remote source for one configuration type, and its last document.
127///
128/// `Remote::new()` is `const`, so this lives in a `static` — which is how
129/// `#[dynamic_config]` emits it.
130///
131/// # What it records about itself
132///
133/// Every fetch this type performs and every delivery it accepts is counted
134/// into a [`RemoteStatus`](crate::RemoteStatus), on the same terms `ConfigCell` records a
135/// [`ConfigStatus`](crate::ConfigStatus): recorded where it happens, read by
136/// an atomic-cheap [`status`](Self::status), and never on the read path —
137/// `load()` reads [`document`](Self::document), which this does not touch.
138/// The cost is one `Instant::now()` per fetch, beside a network round trip.
139#[derive(Default)]
140pub struct Remote {
141 /// One lock for the whole state, deliberately. Two separate locks — one
142 /// for the source, one for the document — allowed an interleaving where
143 /// a slow fetch from the *old* source committed its result after `set`
144 /// had installed a new one: new source, old store's document. The
145 /// generation counter is the fence that makes that impossible.
146 state: Mutex<State>,
147 /// Held across a *blocking* round trip, and across nothing else.
148 ///
149 /// Separate from `state` on purpose: `state` must stay free while a
150 /// fetch is out, or every `load()` would queue behind the network. This
151 /// one is only ever taken by [`refresh`](Remote::refresh), so the lock
152 /// order is `fetching` then `state`, never the reverse.
153 ///
154 /// The async path does not take it. A future cannot hold a blocking
155 /// mutex across an await without parking an executor thread, and this
156 /// crate picks no runtime it could borrow an async mutex from — so
157 /// `refresh_async` still spends a round trip per caller, and says so.
158 fetching: Mutex<()>,
159}
160
161struct State {
162 source: Option<Kind>,
163 fetched: Option<Fetched>,
164 /// The ceiling a document has to fit under. Here rather than beside the
165 /// source, because it is the *slot's* tolerance: replacing the store
166 /// does not change how much this configuration is willing to hold.
167 limit: usize,
168 /// Bumped on every source change. A fetch snapshots it before the network
169 /// round trip and commits only if it has not moved — a result from a
170 /// source that is no longer installed is discarded, never stored.
171 ///
172 /// It is *source identity*, and that is the whole of it: a
173 /// [`RemoteSink`](crate::RemoteSink) holds one for the life of a watch loop, so anything
174 /// that moves this number ends that loop.
175 generation: u64,
176 /// Bumped by [`clear`](crate::Remote::clear), and by nothing else.
177 ///
178 /// A counter of its own rather than a bump of `generation`, because the
179 /// two questions differ: clearing drops the *document* and leaves the
180 /// source installed. Folding it into `generation` made every live
181 /// [`RemoteSink`](crate::RemoteSink) permanently stale — a watch loop whose store had not
182 /// changed and whose stream was still delivering would have every later
183 /// push refused for belonging to a source that had been "replaced". The
184 /// in-flight fetch a `clear` must still discard is fenced on this.
185 cleared: u64,
186 /// How the fetches have gone. Under the same lock as everything else
187 /// here, so a scrape cannot read a count that belongs to one source
188 /// beside a document that belongs to another.
189 status: RemoteStatus,
190}
191
192// Written out rather than derived: a derived `Default` would zero `limit`,
193// and a ceiling of zero refuses every document there is.
194impl Default for State {
195 fn default() -> Self {
196 Self {
197 source: None,
198 fetched: None,
199 limit: MAX_DOCUMENT_BYTES,
200 generation: 0,
201 cleared: 0,
202 status: RemoteStatus::empty(),
203 }
204 }
205}
206
207/// The state a fetch started under, in the two numbers that can invalidate
208/// its result: the source it was fetching from, and the document epoch it
209/// was fetching into.
210///
211/// Captured before the round trip and compared after it. Both halves are
212/// needed and neither is enough: a replaced source must discard the result,
213/// and so must a `clear` — but only the first ends a watch, which is why
214/// they are counted apart.
215#[derive(Clone, Copy, PartialEq, Eq)]
216struct Fence {
217 generation: u64,
218 cleared: u64,
219}
220
221impl Fence {
222 fn of(state: &State) -> Self {
223 Self {
224 generation: state.generation,
225 cleared: state.cleared,
226 }
227 }
228}
229
230/// `Arc` rather than `Box`: an async fetch borrows the source across an await
231/// point, and cloning the handle out of the lock first is what keeps a `std`
232/// mutex from being held across one.
233#[derive(Clone)]
234enum Kind {
235 Blocking(Arc<dyn RemoteSource>),
236 #[cfg(feature = "async")]
237 Asynchronous(Arc<dyn AsyncRemoteSource>),
238}
239
240impl Remote {
241 /// An empty slot: no source, no document.
242 #[must_use]
243 #[cfg(not(loom))]
244 pub const fn new() -> Self {
245 Self {
246 state: Mutex::new(State {
247 source: None,
248 fetched: None,
249 limit: MAX_DOCUMENT_BYTES,
250 generation: 0,
251 cleared: 0,
252 status: RemoteStatus::empty(),
253 }),
254 fetching: Mutex::new(()),
255 }
256 }
257
258 /// The same, minus `const`: loom's constructors are not.
259 #[must_use]
260 #[cfg(loom)]
261 pub fn new() -> Self {
262 Self {
263 state: Mutex::new(State {
264 source: None,
265 fetched: None,
266 limit: MAX_DOCUMENT_BYTES,
267 generation: 0,
268 cleared: 0,
269 status: RemoteStatus::empty(),
270 }),
271 fetching: Mutex::new(()),
272 }
273 }
274
275 /// Installs a blocking source, replacing any previous one.
276 ///
277 /// The document already fetched, if any, is dropped with it — a new source
278 /// answering with an old store's values would be a puzzle nobody needs.
279 /// A fetch from the previous source that is still in flight is discarded
280 /// when it lands, for the same reason.
281 ///
282 /// The recorded [`status`](Self::status) is dropped with the document:
283 /// `remote_up` for the *previous* store says nothing about this one, and
284 /// a stale `1` describing a store nobody is talking to any more is worse
285 /// than no sample at all.
286 pub fn set(&self, source: impl RemoteSource) {
287 let mut state = self.state();
288 state.source = Some(Kind::Blocking(Arc::new(source)));
289 state.fetched = None;
290 state.status = RemoteStatus::empty();
291 state.generation = state.generation.wrapping_add(1);
292 }
293
294 /// Changes how large a document this slot will accept.
295 ///
296 /// [`MAX_DOCUMENT_BYTES`] unless this is called. Raise it for a
297 /// configuration that genuinely is enormous; lower it where the
298 /// container is small and a surprise is worth refusing early.
299 ///
300 /// Survives a source change, because it describes what this
301 /// configuration can hold rather than what any one store tends to send.
302 pub fn set_max_document_bytes(&self, limit: usize) {
303 self.state().limit = limit;
304 }
305
306 /// Installs an async source, replacing any previous one.
307 #[cfg(feature = "async")]
308 #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
309 pub fn set_async(&self, source: impl AsyncRemoteSource) {
310 let mut state = self.state();
311 state.source = Some(Kind::Asynchronous(Arc::new(source)));
312 state.fetched = None;
313 state.status = RemoteStatus::empty();
314 state.generation = state.generation.wrapping_add(1);
315 }
316
317 /// Fetches, and keeps what came back.
318 ///
319 /// The network round trip happens with no lock held: a slow store cannot
320 /// make `load()` — which reads this state for provenance — wait for it.
321 /// If the source is replaced while the fetch is in flight, the result is
322 /// discarded and `Ok` is returned: the fetch *did* succeed, and the new
323 /// source's own refresh is the one that matters now.
324 ///
325 /// # Errors
326 ///
327 /// If no source is installed, if the installed one is async — use
328 /// [`refresh_async`](Self::refresh_async) — or if the fetch fails.
329 pub fn refresh(&self) -> Result<(), Error> {
330 // One round trip for N callers who all want the same thing.
331 //
332 // A resync timer and a store's own watch loop routinely ask at the
333 // same moment, and before this they each paid a network call for an
334 // answer the other was already fetching. Whoever arrives second
335 // waits here, and then finds the first one's result already
336 // installed — so `refresh` still returns having genuinely refreshed,
337 // which is the promise a caller is owed.
338 let queued = self.state().status.fetches;
339 let _single_flight = self
340 .fetching
341 .lock()
342 .unwrap_or_else(std::sync::PoisonError::into_inner);
343
344 let (source, fence) = {
345 let state = self.state();
346
347 if state.status.fetches != queued {
348 // Somebody else's fetch landed while this one waited. Its
349 // answer is at least as new as ours would have been, and
350 // asking again would only spend a second round trip to
351 // learn the same thing.
352 return Ok(());
353 }
354
355 match state.source.as_ref() {
356 Some(Kind::Blocking(source)) => (Arc::clone(source), Fence::of(&state)),
357
358 #[cfg(feature = "async")]
359 Some(Kind::Asynchronous(source)) => {
360 return Err(Error::new(
361 ErrorKind::Remote,
362 format!(
363 "`{}` is an async source; refresh it with `refresh_remote_async`",
364 source.describe()
365 ),
366 ))
367 }
368
369 None => return Err(none_installed()),
370 }
371 };
372
373 // The span covers the round trip rather than following it, which is
374 // the only arrangement that gives a trace a duration to draw. It
375 // carries no name for the store: the one string a source has is its
376 // description, and a store URL routinely embeds `user:password@host`.
377 #[cfg(feature = "tracing")]
378 let span = crate::telemetry::fetching();
379
380 let started = Instant::now();
381
382 match source.fetch() {
383 Ok(fetched) => {
384 let elapsed = started.elapsed();
385
386 // A document too large to hold is a fetch that reached the
387 // store and came back unusable, so it is recorded the way
388 // every other unusable answer is: the store is up, this
389 // configuration is not.
390 if let Err(error) = self.commit(fetched, fence) {
391 self.record_fetch_failure(&error, fence.generation);
392
393 #[cfg(feature = "tracing")]
394 crate::telemetry::fetch_failed(&span, &error);
395
396 return Err(error);
397 }
398
399 self.record_fetch(Some(elapsed), fence.generation);
400
401 #[cfg(feature = "tracing")]
402 crate::telemetry::fetched(&span, elapsed);
403
404 Ok(())
405 }
406 Err(error) => {
407 self.record_fetch_failure(&error, fence.generation);
408
409 #[cfg(feature = "tracing")]
410 crate::telemetry::fetch_failed(&span, &error);
411
412 Err(error)
413 }
414 }
415 }
416
417 /// Fetches from an async source, and keeps what came back.
418 ///
419 /// A *blocking* source is not refused — swapping one implementation for
420 /// the other must not be a breaking change for the caller — but it is not
421 /// run on the executor either: it goes through
422 /// [`off_thread`](crate::off_thread), so an async caller's worker thread
423 /// never sits inside a blocking network call.
424 ///
425 /// The same replaced-mid-fetch rule as [`refresh`](Self::refresh)
426 /// applies, and matters more here: the unlocked window spans an await.
427 ///
428 /// # Errors
429 ///
430 /// If no source is installed, or the fetch fails.
431 #[cfg(feature = "async")]
432 #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
433 pub async fn refresh_async(&self) -> Result<(), Error> {
434 // Cloned out of the lock before anything is awaited: holding a `std`
435 // mutex across an await point is how an executor deadlocks itself.
436 let (source, fence) = {
437 let state = self.state();
438
439 match state.source.as_ref() {
440 Some(source) => (source.clone(), Fence::of(&state)),
441 None => return Err(none_installed()),
442 }
443 };
444
445 // Not entered: this span is held across an await, and an
446 // `EnteredSpan` is `!Send`. `Span::in_scope` cannot wrap an await
447 // either, so what a subscriber gets here is the span's own timing
448 // and its fields rather than an ambient context — which is what a
449 // fetch has to report anyway, since nothing else runs inside it.
450 #[cfg(feature = "tracing")]
451 let span = crate::telemetry::fetching_async();
452
453 let started = Instant::now();
454
455 let outcome = match source {
456 Kind::Blocking(source) => crate::asynchronous::off_thread(move || source.fetch()).await,
457 Kind::Asynchronous(source) => source.fetch().await,
458 };
459
460 match outcome {
461 Ok(fetched) => {
462 let elapsed = started.elapsed();
463
464 // A document too large to hold is a fetch that reached the
465 // store and came back unusable, so it is recorded the way
466 // every other unusable answer is: the store is up, this
467 // configuration is not.
468 if let Err(error) = self.commit(fetched, fence) {
469 self.record_fetch_failure(&error, fence.generation);
470
471 #[cfg(feature = "tracing")]
472 crate::telemetry::fetch_failed(&span, &error);
473
474 return Err(error);
475 }
476
477 self.record_fetch(Some(elapsed), fence.generation);
478
479 #[cfg(feature = "tracing")]
480 crate::telemetry::fetched(&span, elapsed);
481
482 Ok(())
483 }
484 Err(error) => {
485 self.record_fetch_failure(&error, fence.generation);
486
487 #[cfg(feature = "tracing")]
488 crate::telemetry::fetch_failed(&span, &error);
489
490 Err(error)
491 }
492 }
493 }
494
495 /// The generation a sink created now would carry; see [`RemoteSink`](crate::RemoteSink).
496 pub(crate) fn generation(&self) -> u64 {
497 self.state().generation
498 }
499
500 /// Installs `document` if the source it came from is still the one
501 /// installed, and if it is not older than what is already here — the
502 /// push-side twin of the fetch fence.
503 ///
504 /// # Errors
505 ///
506 /// When the source has been replaced since `generation` was captured:
507 /// the document belongs to a store nobody asked about any more, and
508 /// installing it would hand a stale watcher the last word.
509 pub(crate) fn install_if(&self, generation: u64, document: Fetched) -> Result<(), Error> {
510 let mut state = self.state();
511
512 if state.generation != generation {
513 return Err(Error::new(
514 crate::ErrorKind::Backend,
515 "the remote source this sink was created for has been \
516 replaced; stop the old watch loop and take a fresh sink \
517 from `remote_sink()`",
518 ));
519 }
520
521 // A push is a fetch somebody else performed: the store answered, and
522 // that is the whole question `RemoteStatus` reports on. Whether the
523 // document then *installs* is `ConfigStatus`'s business, and
524 // `RemoteSink::apply` records it there through the reload it runs.
525 // Counted before the recency test for exactly that reason — a store
526 // that answered is reachable whether or not it said anything new.
527 state.status.fetches = state.status.fetches.saturating_add(1);
528 state.status.last_fetch = Some(Instant::now());
529 state.status.last_fetch_duration = None;
530 state.status.consecutive_failures = 0;
531
532 if document.text.len() > state.limit {
533 return Err(too_large(document.text.len(), state.limit));
534 }
535
536 if superseded(&document, state.fetched.as_ref()) {
537 state.fetched = Some(document);
538 }
539
540 Ok(())
541 }
542
543 /// Not public API: the loom suite's door to the fence internals.
544 #[cfg(loom)]
545 #[doc(hidden)]
546 #[must_use]
547 pub fn generation_for_loom(&self) -> u64 {
548 self.generation()
549 }
550
551 /// Not public API: the loom suite's door to the fence internals.
552 ///
553 /// # Errors
554 ///
555 /// As `install_if`.
556 #[cfg(loom)]
557 #[doc(hidden)]
558 pub fn install_if_for_loom(&self, generation: u64, document: Fetched) -> Result<(), Error> {
559 self.install_if(generation, document)
560 }
561
562 /// How the installed store learns that its document changed.
563 ///
564 /// `None` when nothing is installed. What an agent reads to decide
565 /// whether to run a watch at all, and what a report prints so an
566 /// operator can see why a change took as long as it did.
567 #[must_use]
568 pub fn watch_capability(&self) -> Option<WatchCapability> {
569 match self.state().source.as_ref()? {
570 Kind::Blocking(source) => Some(source.watch_capability()),
571
572 #[cfg(feature = "async")]
573 Kind::Asynchronous(source) => Some(source.watch_capability()),
574 }
575 }
576
577 /// Watches the installed store, keeping every document it delivers.
578 ///
579 /// The store's own mechanism where it has one — a blocking query, a
580 /// stream, a subscription — and a jittered, backing-off poll where it
581 /// does not. `interval` is the poll period, and a *resync* period for a
582 /// store that pushes: a stream that has silently stalled looks exactly
583 /// like a store where nothing has changed.
584 ///
585 /// Blocks until the watch is stopped, so it belongs on a thread of its
586 /// own. Each document is stored the way a
587 /// [`refresh`](Self::refresh) stores one, generation fence included —
588 /// a document from a source that has since been replaced is discarded
589 /// rather than installed.
590 ///
591 /// **It keeps the document; it does not reload the configuration.** A
592 /// `Remote` is a store handle and has no configuration to reload: no
593 /// type to read into, no hooks to fire, no cache to write. A change
594 /// that should reach `current()` on its own goes through
595 /// [`RemoteSink::apply`](crate::RemoteSink::apply) instead — take a
596 /// sink from the generated `remote_sink()` and call the store's own
597 /// `watch` with it. This is the right call when something else decides
598 /// when to reload.
599 ///
600 /// # Errors
601 ///
602 /// If no source is installed, if the installed one is async, or if the
603 /// store's watch gives up. A *fetch* failing is not an error: a watch
604 /// outlives an outage by design.
605 pub fn watch(&self, watching: &Watching, interval: Duration) -> Result<(), Error> {
606 let (source, fence) = {
607 let state = self.state();
608
609 match state.source.as_ref() {
610 Some(Kind::Blocking(source)) => (Arc::clone(source), Fence::of(&state)),
611
612 #[cfg(feature = "async")]
613 Some(Kind::Asynchronous(source)) => {
614 return Err(Error::new(
615 ErrorKind::Remote,
616 format!(
617 "`{}` is an async source; watch it with `watch_async`",
618 source.describe()
619 ),
620 ))
621 }
622 None => {
623 return Err(Error::new(
624 ErrorKind::Remote,
625 "no remote source is configured",
626 ))
627 }
628 }
629 };
630
631 let mut deliver = |document| self.deliver(fence.generation, document);
632
633 if source.watch_capability() != WatchCapability::Native {
634 return source.watch(watching, interval, &mut deliver);
635 }
636
637 // A store that pushes still gets read on the interval, because the
638 // failure mode of a stream is silence: a connection that dropped
639 // without an error, a subscription the broker forgot, a blocking
640 // query answering an index that will never move again. All three
641 // look exactly like a store where nothing has changed, and the only
642 // way to tell them apart is to go and ask.
643 //
644 // Scoped threads rather than an executor: the resync is this call's
645 // for as long as this call lasts, and nothing outlives it.
646 std::thread::scope(|scope| {
647 let source = Arc::clone(&source);
648 let watcher = scope.spawn(move || source.watch(watching, interval, &mut deliver));
649 let mut pace = Pace::new(interval);
650
651 while watching.keep_going() && !watcher.is_finished() {
652 // Sliced rather than slept whole, so the resync notices the
653 // store's own watch returning instead of waiting out an
654 // interval to find out.
655 let mut left = pace.next_wait();
656
657 while left > Duration::ZERO && watching.keep_going() && !watcher.is_finished() {
658 let slice = left.min(Duration::from_millis(250));
659
660 std::thread::sleep(slice);
661 left -= slice;
662 }
663
664 if watching.keep_going() && !watcher.is_finished() {
665 match self.refresh() {
666 Ok(()) => pace.succeeded(),
667 Err(_) => pace.failed(),
668 }
669 }
670 }
671
672 watcher
673 .join()
674 .unwrap_or_else(|_| Err(Error::new(ErrorKind::Remote, "the watch panicked")))
675 })
676 }
677
678 /// [`watch`](Self::watch), for a store that is read asynchronously.
679 ///
680 /// Cancellation is dropping the future.
681 ///
682 /// # Errors
683 ///
684 /// As [`watch`](Self::watch), with the sides swapped.
685 #[cfg(feature = "async")]
686 #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
687 pub async fn watch_async(&self, watching: &Watching, interval: Duration) -> Result<(), Error> {
688 let (source, fence) = {
689 let state = self.state();
690
691 match state.source.as_ref() {
692 Some(Kind::Asynchronous(source)) => (Arc::clone(source), Fence::of(&state)),
693 Some(Kind::Blocking(source)) => {
694 return Err(Error::new(
695 ErrorKind::Remote,
696 format!(
697 "`{}` is a blocking source; watch it with `watch` on a thread",
698 source.describe()
699 ),
700 ))
701 }
702 None => {
703 return Err(Error::new(
704 ErrorKind::Remote,
705 "no remote source is configured",
706 ))
707 }
708 }
709 };
710
711 let mut deliver = |document| self.deliver(fence.generation, document);
712
713 source.watch(watching, interval, &mut deliver).await
714 }
715
716 /// The document last fetched, if any.
717 #[must_use]
718 pub fn document(&self) -> Option<Fetched> {
719 self.state().fetched.clone()
720 }
721
722 /// Whether a source is installed.
723 #[must_use]
724 pub fn is_configured(&self) -> bool {
725 self.state().source.is_some()
726 }
727
728 /// How the fetches from this source have gone.
729 ///
730 /// One lock and a clone, no I/O and no network: an exporter may call it
731 /// per scrape, which is the same contract
732 /// [`ConfigCell::status`](crate::ConfigCell::status) makes.
733 #[must_use]
734 pub fn status(&self) -> RemoteStatus {
735 self.state().status.clone()
736 }
737
738 /// Records a fetch that returned a document.
739 ///
740 /// Fenced on the source `generation` the fetch started under, and under
741 /// the one lock that reads it: [`set`](Self::set) empties the status
742 /// along with the document, so an old fetch landing afterwards would
743 /// otherwise report the *replacement* as fetched and healthy — a store
744 /// nothing has yet spoken to.
745 fn record_fetch(&self, elapsed: Option<Duration>, generation: u64) {
746 let mut state = self.state();
747
748 if state.generation != generation {
749 return;
750 }
751
752 state.status.fetches = state.status.fetches.saturating_add(1);
753 state.status.last_fetch = Some(Instant::now());
754 state.status.last_fetch_duration = elapsed;
755 state.status.consecutive_failures = 0;
756 }
757
758 /// Records a fetch that returned nothing.
759 ///
760 /// The document is untouched: a store that stopped answering leaves the
761 /// last one it did answer with in place, and the counter is what says
762 /// so. Only the failure's category and key path are kept — the same
763 /// [`FailureStatus`] a refused reload records, for the same reason.
764 ///
765 /// Fenced like [`record_fetch`](Self::record_fetch), and for the mirror
766 /// reason: an old fetch's failure must not report a store that has just
767 /// been installed as down.
768 fn record_fetch_failure(&self, error: &Error, generation: u64) {
769 let mut state = self.state();
770
771 if state.generation != generation {
772 return;
773 }
774
775 // Saturating rather than wrapping, as `ConfigCell` does: a counter
776 // that rolls over to zero reads as "healthy" at the worst moment.
777 state.status.consecutive_failures = state.status.consecutive_failures.saturating_add(1);
778 state.status.last_failure = Some(FailureStatus::of(error));
779 }
780
781 /// How the installed source names itself.
782 #[must_use]
783 pub fn describe(&self) -> Option<String> {
784 // The lock is held only for the clone: `describe()` on the source runs
785 // unlocked, so a source whose description does real work cannot stall
786 // readers.
787 let source = self.state().source.clone()?;
788
789 Some(match source {
790 Kind::Blocking(source) => source.describe(),
791 #[cfg(feature = "async")]
792 Kind::Asynchronous(source) => source.describe(),
793 })
794 }
795
796 /// Drops the document, so the next load sees no remote layer.
797 ///
798 /// A fetch that was already in flight is discarded when it lands, the
799 /// same way [`set`](Self::set) discards one: clearing is a state change
800 /// like any other, and a document a caller explicitly dropped must not
801 /// come back from a round trip that started before they dropped it.
802 ///
803 /// The *source* is left alone, and so is every [`RemoteSink`](crate::RemoteSink) taken from
804 /// it: a watch loop delivering from the same store keeps delivering, and
805 /// its next push installs normally. Dropping the document is not
806 /// replacing the store, and only replacing the store ends a watch.
807 pub fn clear(&self) {
808 let mut state = self.state();
809
810 state.fetched = None;
811 state.cleared = state.cleared.wrapping_add(1);
812 }
813
814 /// One document from a watch, stored and counted.
815 ///
816 /// **Fenced on the generation alone, unlike a fetch.** The two fences
817 /// answer different questions. A fetch was in flight *across* whatever
818 /// happened, so a `clear()` while it flew has to discard it — putting
819 /// the document back would undo the clear. A watch delivery happened
820 /// *after*: it is the store saying what it holds now, and
821 /// [`clear`](Self::clear) promises exactly that — "a watch loop
822 /// delivering from the same store keeps delivering, and its next push
823 /// installs normally".
824 ///
825 /// Sharing the fetch fence broke that promise silently: one `clear()`
826 /// bumped `cleared`, every later document was dropped, and
827 /// `record_fetch` — which fences on the generation — went on reporting
828 /// the store as healthy. A watch that has installed nothing for an hour
829 /// while its status says `reachable` is worse than one that stopped.
830 ///
831 /// # Errors
832 ///
833 /// If the source was replaced. The watch belongs to a store nobody is
834 /// asking about any more, so it ends rather than delivering into a slot
835 /// that moved on — the same answer [`RemoteSink::apply`] gives.
836 fn deliver(&self, generation: u64, document: Fetched) -> Result<(), Error> {
837 self.install_if(generation, document)
838 }
839
840 /// Stores a fetch result, unless the slot moved while it was in flight —
841 /// the source was replaced, and the result belongs to a store nobody
842 /// asked about any more, or the document was cleared and putting this
843 /// one back would undo that — or unless a newer document landed first.
844 fn commit(&self, fetched: Fetched, fence: Fence) -> Result<(), Error> {
845 let mut state = self.state();
846
847 if fetched.text.len() > state.limit {
848 return Err(too_large(fetched.text.len(), state.limit));
849 }
850
851 if Fence::of(&state) == fence && superseded(&fetched, state.fetched.as_ref()) {
852 state.fetched = Some(fetched);
853 }
854
855 Ok(())
856 }
857
858 fn state(&self) -> crate::sync::MutexGuard<'_, State> {
859 self.state
860 .lock()
861 .unwrap_or_else(std::sync::PoisonError::into_inner)
862 }
863}
864
865/// The largest document this crate will install, unless a caller says
866/// otherwise.
867///
868/// Eight mebibytes is far past any configuration anybody writes and far
869/// below what a container is given, which is the gap this number lives in.
870/// It is a guard against the accident — a bucket key pointed at a database
871/// dump, a prefix read that matched more than it meant to — not against a
872/// document somebody intended.
873///
874/// The ceiling is enforced here, at the door into the slot, so it holds for
875/// every store including one written outside this organisation. A store
876/// that can bound its read *before* allocating should also do that; this is
877/// the backstop, not the only line.
878pub const MAX_DOCUMENT_BYTES: usize = 8 * 1024 * 1024;
879
880/// The refusal, in the one shape every store gets.
881///
882/// Names the two numbers and the store, and nothing else: the document that
883/// tripped the limit is exactly the document least worth quoting.
884fn too_large(bytes: usize, limit: usize) -> Error {
885 Error::new(
886 ErrorKind::Remote,
887 format!(
888 "the store answered with {bytes} bytes, past the {limit}-byte \
889 ceiling this configuration allows; raise it with \
890 `set_max_document_bytes` if the document is meant to be this large"
891 ),
892 )
893}
894
895/// Whether `fresh` is worth installing over `installed`.
896///
897/// The fence above answers "is this from the source we are still using?".
898/// This answers the other question, which it never could: **is this the
899/// newest thing we have seen?**
900///
901/// It matters because two fetches of the same source can be in flight at
902/// once — a resync beside the store's own watch — and the one that finishes
903/// last is not necessarily the one that started last. Without this, a slow
904/// read of an old version overwrites a fast read of a new one, and the
905/// configuration moves backwards with nothing anywhere reporting it.
906///
907/// A document with **no** revision always installs. That is not a gap: a
908/// store that names no version has told us nothing to order by, and
909/// refusing on a guess would drop real updates. Ordering is a property a
910/// store opts into by answering [`Revision`](crate::Revision).
911fn superseded(fresh: &Fetched, installed: Option<&Fetched>) -> bool {
912 match (&fresh.revision, installed) {
913 (Some(fresh), Some(installed)) => fresh.supersedes(installed.revision.as_ref()),
914 _ => true,
915 }
916}
917
918impl std::fmt::Debug for Remote {
919 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
920 f.debug_struct("Remote")
921 .field("source", &self.describe())
922 .field("fetched", &self.document().is_some())
923 .finish()
924 }
925}
926
927fn none_installed() -> Error {
928 Error::new(
929 ErrorKind::Remote,
930 "no remote source is installed; call `set_remote` first",
931 )
932}
933
934#[cfg(test)]
935mod tests {
936 use super::*;
937 use crate::source::Format;
938 use crate::sync::atomic::Ordering;
939
940 struct Fake(&'static str);
941
942 impl RemoteSource for Fake {
943 fn fetch(&self) -> Result<Fetched, Error> {
944 Ok(Fetched::new(self.0, Format::Json))
945 }
946
947 fn describe(&self) -> String {
948 "a fake store".to_owned()
949 }
950 }
951
952 struct Broken;
953
954 impl RemoteSource for Broken {
955 fn fetch(&self) -> Result<Fetched, Error> {
956 Err(Error::remote("the store is unreachable"))
957 }
958
959 fn describe(&self) -> String {
960 "a broken store".to_owned()
961 }
962 }
963
964 #[test]
965 fn nothing_is_fetched_until_it_is_asked_for() {
966 let remote = Remote::new();
967 remote.set(Fake(r#"{"db": {"host": "a"}}"#));
968
969 assert!(remote.is_configured());
970 assert!(
971 remote.document().is_none(),
972 "installing a source must not reach the network"
973 );
974
975 remote.refresh().unwrap();
976 assert!(remote.document().is_some());
977 }
978
979 /// Succeeds once, then fails — a store that answered and went away.
980 struct Flaky(std::sync::atomic::AtomicBool);
981
982 impl RemoteSource for Flaky {
983 fn fetch(&self) -> Result<Fetched, Error> {
984 if self.0.swap(true, Ordering::SeqCst) {
985 return Err(Error::remote("the store went away"));
986 }
987
988 Fake(r#"{"db": {"host": "a"}}"#).fetch()
989 }
990
991 fn describe(&self) -> String {
992 "a store that answers once".to_owned()
993 }
994 }
995
996 #[test]
997 fn a_failed_fetch_leaves_the_previous_document_alone() {
998 let remote = Remote::new();
999 remote.set(Flaky(std::sync::atomic::AtomicBool::new(false)));
1000 remote.refresh().unwrap();
1001
1002 let before = remote.document();
1003 assert!(before.is_some(), "the first fetch succeeds");
1004
1005 // The second fetch *fails*, and the failure must surface — while the
1006 // document from the fetch that worked stays where it was.
1007 let error = remote.refresh().unwrap_err();
1008
1009 assert!(error.to_string().contains("went away"), "{error}");
1010 assert_eq!(remote.document(), before);
1011 }
1012
1013 /// Blocks inside `fetch` on a pair of barriers, so a test can hold a
1014 /// fetch mid-flight while it does something else to the `Remote`.
1015 struct Parked {
1016 started: std::sync::Arc<std::sync::Barrier>,
1017 release: std::sync::Arc<std::sync::Barrier>,
1018 }
1019
1020 impl RemoteSource for Parked {
1021 fn fetch(&self) -> Result<Fetched, Error> {
1022 self.started.wait();
1023 self.release.wait();
1024
1025 Fake(r#"{"db": {"host": "stale"}}"#).fetch()
1026 }
1027
1028 fn describe(&self) -> String {
1029 "a parked store".to_owned()
1030 }
1031 }
1032
1033 /// The same, for the failing half of the fence: parked mid-fetch, and
1034 /// what it finally returns is an error.
1035 struct ParkedThenBroken {
1036 started: std::sync::Arc<std::sync::Barrier>,
1037 release: std::sync::Arc<std::sync::Barrier>,
1038 }
1039
1040 impl RemoteSource for ParkedThenBroken {
1041 fn fetch(&self) -> Result<Fetched, Error> {
1042 self.started.wait();
1043 self.release.wait();
1044
1045 Broken.fetch()
1046 }
1047
1048 fn describe(&self) -> String {
1049 "a parked store that then breaks".to_owned()
1050 }
1051 }
1052
1053 /// The race the generation fence exists for: a fetch from the *old*
1054 /// source lands after `set` installed a new one. Its result must be
1055 /// discarded — new source, old store's document is the state this
1056 /// module's docs promise cannot happen.
1057 #[test]
1058 fn a_fetch_from_a_replaced_source_is_discarded() {
1059 let started = std::sync::Arc::new(std::sync::Barrier::new(2));
1060 let release = std::sync::Arc::new(std::sync::Barrier::new(2));
1061
1062 let remote = std::sync::Arc::new(Remote::new());
1063 remote.set(Parked {
1064 started: std::sync::Arc::clone(&started),
1065 release: std::sync::Arc::clone(&release),
1066 });
1067
1068 let refresher = {
1069 let remote = std::sync::Arc::clone(&remote);
1070 std::thread::spawn(move || remote.refresh())
1071 };
1072
1073 // The fetch is provably in flight...
1074 started.wait();
1075
1076 // ...when the source is replaced.
1077 remote.set(Fake(r#"{"db": {"host": "fresh"}}"#));
1078
1079 release.wait();
1080 refresher
1081 .join()
1082 .expect("the refresher must not panic")
1083 .expect("the fetch itself succeeded");
1084
1085 assert_eq!(
1086 remote.document(),
1087 None,
1088 "the old source's document landed after the replacement and must \
1089 not be paired with the new source"
1090 );
1091
1092 // And the new source works normally.
1093 remote.refresh().unwrap();
1094 assert!(remote.document().unwrap().text.contains("fresh"));
1095 }
1096
1097 /// The same fence, from the other side: `clear()` is a state change too,
1098 /// so a fetch that was in flight when a caller cleared the slot must not
1099 /// put the document back. The barriers force the interleaving — the
1100 /// fetch is provably parked when `clear` runs — so this is a proof
1101 /// rather than a race the scheduler usually loses.
1102 #[test]
1103 fn a_fetch_in_flight_when_the_slot_is_cleared_is_discarded() {
1104 let started = std::sync::Arc::new(std::sync::Barrier::new(2));
1105 let release = std::sync::Arc::new(std::sync::Barrier::new(2));
1106
1107 let remote = std::sync::Arc::new(Remote::new());
1108 remote.set(Parked {
1109 started: std::sync::Arc::clone(&started),
1110 release: std::sync::Arc::clone(&release),
1111 });
1112
1113 let refresher = {
1114 let remote = std::sync::Arc::clone(&remote);
1115 std::thread::spawn(move || remote.refresh())
1116 };
1117
1118 started.wait();
1119
1120 remote.clear();
1121
1122 release.wait();
1123 refresher
1124 .join()
1125 .expect("the refresher must not panic")
1126 .expect("the fetch itself succeeded");
1127
1128 assert_eq!(
1129 remote.document(),
1130 None,
1131 "a document the caller cleared must not come back from a fetch \
1132 that started before they cleared it"
1133 );
1134 }
1135
1136 /// Clearing the document must not end a watch. The source is untouched
1137 /// by `clear()`, so a loop that took its sink before the call is still
1138 /// serving the store it was created for, and its next delivery installs
1139 /// like any other. The first shape of this fence counted both events on
1140 /// one number and made every live sink permanently stale.
1141 #[test]
1142 fn clearing_the_document_leaves_a_watchs_sink_alive() {
1143 let remote = Remote::new();
1144 remote.set(Fake(r#"{"db": {"host": "a"}}"#));
1145
1146 // What `remote_sink()` captures, once, where a loop starts.
1147 let generation = remote.generation();
1148
1149 remote.clear();
1150
1151 remote
1152 .install_if(generation, Fetched::new("{}", crate::Format::Json))
1153 .expect("clearing the document does not replace the source");
1154 assert!(remote.document().is_some());
1155 }
1156
1157 /// The status fence, from the side `set` opens: an old fetch that
1158 /// succeeds after its source was replaced must not report the
1159 /// replacement — which nothing has yet spoken to — as fetched and
1160 /// healthy. `set` empties the status precisely so that it says nothing
1161 /// about a store that is no longer installed.
1162 #[test]
1163 fn a_late_fetch_does_not_report_the_replacement_as_healthy() {
1164 let started = std::sync::Arc::new(std::sync::Barrier::new(2));
1165 let release = std::sync::Arc::new(std::sync::Barrier::new(2));
1166
1167 let remote = std::sync::Arc::new(Remote::new());
1168 remote.set(Parked {
1169 started: std::sync::Arc::clone(&started),
1170 release: std::sync::Arc::clone(&release),
1171 });
1172
1173 let refresher = {
1174 let remote = std::sync::Arc::clone(&remote);
1175 std::thread::spawn(move || remote.refresh())
1176 };
1177
1178 started.wait();
1179 remote.set(Fake(r#"{"db": {"host": "fresh"}}"#));
1180 release.wait();
1181
1182 let _ = refresher.join().expect("the refresher must not panic");
1183
1184 let status = remote.status();
1185 assert_eq!(
1186 status.fetches, 0,
1187 "the replacement has been fetched from nobody"
1188 );
1189 assert_eq!(status.last_fetch, None);
1190 assert_eq!(status.reachable(), None);
1191 }
1192
1193 /// The same fence for a failure. A store that was replaced while its
1194 /// fetch was erroring must not leave the new one looking down.
1195 #[test]
1196 fn a_late_failure_does_not_report_the_replacement_as_down() {
1197 let started = std::sync::Arc::new(std::sync::Barrier::new(2));
1198 let release = std::sync::Arc::new(std::sync::Barrier::new(2));
1199
1200 let remote = std::sync::Arc::new(Remote::new());
1201 remote.set(ParkedThenBroken {
1202 started: std::sync::Arc::clone(&started),
1203 release: std::sync::Arc::clone(&release),
1204 });
1205
1206 let refresher = {
1207 let remote = std::sync::Arc::clone(&remote);
1208 std::thread::spawn(move || remote.refresh())
1209 };
1210
1211 started.wait();
1212 remote.set(Fake(r#"{"db": {"host": "fresh"}}"#));
1213 release.wait();
1214
1215 let _ = refresher.join().expect("the refresher must not panic");
1216
1217 let status = remote.status();
1218 assert_eq!(status.consecutive_failures, 0);
1219 assert_eq!(
1220 status.reachable(),
1221 None,
1222 "nothing has yet asked the replacement anything"
1223 );
1224 }
1225
1226 /// Readers must not wait for a slow store: `document()` and `describe()`
1227 /// are on the `load()` path, and `load()` promises to touch no network.
1228 #[test]
1229 fn readers_are_not_blocked_by_a_fetch_in_flight() {
1230 let started = std::sync::Arc::new(std::sync::Barrier::new(2));
1231 let release = std::sync::Arc::new(std::sync::Barrier::new(2));
1232
1233 let remote = std::sync::Arc::new(Remote::new());
1234 remote.set(Parked {
1235 started: std::sync::Arc::clone(&started),
1236 release: std::sync::Arc::clone(&release),
1237 });
1238
1239 let refresher = {
1240 let remote = std::sync::Arc::clone(&remote);
1241 std::thread::spawn(move || remote.refresh())
1242 };
1243
1244 started.wait();
1245
1246 // With the fetch parked, a reader thread must finish promptly. The
1247 // old two-lock design held the source lock across the fetch, so
1248 // `describe()` — and with it every `load()` — waited out the store's
1249 // full timeout.
1250 let (sender, receiver) = std::sync::mpsc::channel();
1251 {
1252 let remote = std::sync::Arc::clone(&remote);
1253 std::thread::spawn(move || {
1254 let described = remote.describe();
1255 let document = remote.document();
1256 let _ = sender.send((described, document));
1257 });
1258 }
1259
1260 let (described, document) = receiver
1261 .recv_timeout(Duration::from_secs(2))
1262 .expect("readers must not wait for the network");
1263
1264 assert_eq!(described.as_deref(), Some("a parked store"));
1265 assert_eq!(document, None);
1266
1267 release.wait();
1268 let _ = refresher.join();
1269 }
1270
1271 /// `set` clears the document atomically with the source swap: no
1272 /// interleaving may observe the new source paired with any document.
1273 #[test]
1274 fn replacing_the_source_and_dropping_the_document_is_one_step() {
1275 let remote = Remote::new();
1276 remote.set(Fake(r#"{"db": {"host": "a"}}"#));
1277 remote.refresh().unwrap();
1278 let generation = remote.generation();
1279 remote
1280 .install_if(generation, Fetched::new("{}", crate::Format::Json))
1281 .expect("the source has not moved");
1282
1283 remote.set(Fake(r#"{"db": {"host": "b"}}"#));
1284
1285 assert_eq!(remote.document(), None);
1286
1287 // And the push-side fence itself: the pre-swap generation is now
1288 // stale, so a late delivery bounces instead of landing.
1289 remote
1290 .install_if(generation, Fetched::new("{}", crate::Format::Json))
1291 .expect_err("a replaced source's generation must be refused");
1292 assert_eq!(remote.document(), None);
1293 }
1294
1295 #[test]
1296 fn a_broken_store_reports_rather_than_pretending() {
1297 let remote = Remote::new();
1298 remote.set(Broken);
1299
1300 let error = remote.refresh().unwrap_err();
1301
1302 assert_eq!(error.kind(), ErrorKind::Remote);
1303 assert!(error.to_string().contains("unreachable"), "{error}");
1304 }
1305
1306 #[test]
1307 fn refreshing_with_no_source_says_so() {
1308 let error = Remote::new().refresh().unwrap_err();
1309
1310 assert!(error.to_string().contains("set_remote"), "{error}");
1311 }
1312
1313 #[test]
1314 fn replacing_the_source_drops_the_old_document() {
1315 let remote = Remote::new();
1316 remote.set(Fake(r#"{"db": {"host": "a"}}"#));
1317 remote.refresh().unwrap();
1318
1319 remote.set(Fake(r#"{"db": {"host": "b"}}"#));
1320
1321 assert!(
1322 remote.document().is_none(),
1323 "a new source answering with the old store's values would be a puzzle"
1324 );
1325 }
1326}