Skip to main content

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}