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, RemoteSource, 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}
148
149#[derive(Default)]
150struct State {
151    source: Option<Kind>,
152    fetched: Option<Fetched>,
153    /// Bumped on every source change. A fetch snapshots it before the network
154    /// round trip and commits only if it has not moved — a result from a
155    /// source that is no longer installed is discarded, never stored.
156    ///
157    /// It is *source identity*, and that is the whole of it: a
158    /// [`RemoteSink`](crate::RemoteSink) holds one for the life of a watch loop, so anything
159    /// that moves this number ends that loop.
160    generation: u64,
161    /// Bumped by [`clear`](crate::Remote::clear), and by nothing else.
162    ///
163    /// A counter of its own rather than a bump of `generation`, because the
164    /// two questions differ: clearing drops the *document* and leaves the
165    /// source installed. Folding it into `generation` made every live
166    /// [`RemoteSink`](crate::RemoteSink) permanently stale — a watch loop whose store had not
167    /// changed and whose stream was still delivering would have every later
168    /// push refused for belonging to a source that had been "replaced". The
169    /// in-flight fetch a `clear` must still discard is fenced on this.
170    cleared: u64,
171    /// How the fetches have gone. Under the same lock as everything else
172    /// here, so a scrape cannot read a count that belongs to one source
173    /// beside a document that belongs to another.
174    status: RemoteStatus,
175}
176
177/// The state a fetch started under, in the two numbers that can invalidate
178/// its result: the source it was fetching from, and the document epoch it
179/// was fetching into.
180///
181/// Captured before the round trip and compared after it. Both halves are
182/// needed and neither is enough: a replaced source must discard the result,
183/// and so must a `clear` — but only the first ends a watch, which is why
184/// they are counted apart.
185#[derive(Clone, Copy, PartialEq, Eq)]
186struct Fence {
187    generation: u64,
188    cleared: u64,
189}
190
191impl Fence {
192    fn of(state: &State) -> Self {
193        Self {
194            generation: state.generation,
195            cleared: state.cleared,
196        }
197    }
198}
199
200/// `Arc` rather than `Box`: an async fetch borrows the source across an await
201/// point, and cloning the handle out of the lock first is what keeps a `std`
202/// mutex from being held across one.
203#[derive(Clone)]
204enum Kind {
205    Blocking(Arc<dyn RemoteSource>),
206    #[cfg(feature = "async")]
207    Asynchronous(Arc<dyn AsyncRemoteSource>),
208}
209
210impl Remote {
211    /// An empty slot: no source, no document.
212    #[must_use]
213    #[cfg(not(loom))]
214    pub const fn new() -> Self {
215        Self {
216            state: Mutex::new(State {
217                source: None,
218                fetched: None,
219                generation: 0,
220                cleared: 0,
221                status: RemoteStatus::empty(),
222            }),
223        }
224    }
225
226    /// The same, minus `const`: loom's constructors are not.
227    #[must_use]
228    #[cfg(loom)]
229    pub fn new() -> Self {
230        Self {
231            state: Mutex::new(State {
232                source: None,
233                fetched: None,
234                generation: 0,
235                cleared: 0,
236                status: RemoteStatus::empty(),
237            }),
238        }
239    }
240
241    /// Installs a blocking source, replacing any previous one.
242    ///
243    /// The document already fetched, if any, is dropped with it — a new source
244    /// answering with an old store's values would be a puzzle nobody needs.
245    /// A fetch from the previous source that is still in flight is discarded
246    /// when it lands, for the same reason.
247    ///
248    /// The recorded [`status`](Self::status) is dropped with the document:
249    /// `remote_up` for the *previous* store says nothing about this one, and
250    /// a stale `1` describing a store nobody is talking to any more is worse
251    /// than no sample at all.
252    pub fn set(&self, source: impl RemoteSource) {
253        let mut state = self.state();
254        state.source = Some(Kind::Blocking(Arc::new(source)));
255        state.fetched = None;
256        state.status = RemoteStatus::empty();
257        state.generation = state.generation.wrapping_add(1);
258    }
259
260    /// Installs an async source, replacing any previous one.
261    #[cfg(feature = "async")]
262    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
263    pub fn set_async(&self, source: impl AsyncRemoteSource) {
264        let mut state = self.state();
265        state.source = Some(Kind::Asynchronous(Arc::new(source)));
266        state.fetched = None;
267        state.status = RemoteStatus::empty();
268        state.generation = state.generation.wrapping_add(1);
269    }
270
271    /// Fetches, and keeps what came back.
272    ///
273    /// The network round trip happens with no lock held: a slow store cannot
274    /// make `load()` — which reads this state for provenance — wait for it.
275    /// If the source is replaced while the fetch is in flight, the result is
276    /// discarded and `Ok` is returned: the fetch *did* succeed, and the new
277    /// source's own refresh is the one that matters now.
278    ///
279    /// # Errors
280    ///
281    /// If no source is installed, if the installed one is async — use
282    /// [`refresh_async`](Self::refresh_async) — or if the fetch fails.
283    pub fn refresh(&self) -> Result<(), Error> {
284        let (source, fence) = {
285            let state = self.state();
286
287            match state.source.as_ref() {
288                Some(Kind::Blocking(source)) => (Arc::clone(source), Fence::of(&state)),
289
290                #[cfg(feature = "async")]
291                Some(Kind::Asynchronous(source)) => {
292                    return Err(Error::new(
293                        ErrorKind::Remote,
294                        format!(
295                            "`{}` is an async source; refresh it with `refresh_remote_async`",
296                            source.describe()
297                        ),
298                    ))
299                }
300
301                None => return Err(none_installed()),
302            }
303        };
304
305        // The span covers the round trip rather than following it, which is
306        // the only arrangement that gives a trace a duration to draw. It
307        // carries no name for the store: the one string a source has is its
308        // description, and a store URL routinely embeds `user:password@host`.
309        #[cfg(feature = "tracing")]
310        let span = crate::telemetry::fetching();
311
312        let started = Instant::now();
313
314        match source.fetch() {
315            Ok(fetched) => {
316                let elapsed = started.elapsed();
317
318                self.commit(fetched, fence);
319                self.record_fetch(Some(elapsed), fence.generation);
320
321                #[cfg(feature = "tracing")]
322                crate::telemetry::fetched(&span, elapsed);
323
324                Ok(())
325            }
326            Err(error) => {
327                self.record_fetch_failure(&error, fence.generation);
328
329                #[cfg(feature = "tracing")]
330                crate::telemetry::fetch_failed(&span, &error);
331
332                Err(error)
333            }
334        }
335    }
336
337    /// Fetches from an async source, and keeps what came back.
338    ///
339    /// A *blocking* source is not refused — swapping one implementation for
340    /// the other must not be a breaking change for the caller — but it is not
341    /// run on the executor either: it goes through
342    /// [`off_thread`](crate::off_thread), so an async caller's worker thread
343    /// never sits inside a blocking network call.
344    ///
345    /// The same replaced-mid-fetch rule as [`refresh`](Self::refresh)
346    /// applies, and matters more here: the unlocked window spans an await.
347    ///
348    /// # Errors
349    ///
350    /// If no source is installed, or the fetch fails.
351    #[cfg(feature = "async")]
352    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
353    pub async fn refresh_async(&self) -> Result<(), Error> {
354        // Cloned out of the lock before anything is awaited: holding a `std`
355        // mutex across an await point is how an executor deadlocks itself.
356        let (source, fence) = {
357            let state = self.state();
358
359            match state.source.as_ref() {
360                Some(source) => (source.clone(), Fence::of(&state)),
361                None => return Err(none_installed()),
362            }
363        };
364
365        // Not entered: this span is held across an await, and an
366        // `EnteredSpan` is `!Send`. `Span::in_scope` cannot wrap an await
367        // either, so what a subscriber gets here is the span's own timing
368        // and its fields rather than an ambient context — which is what a
369        // fetch has to report anyway, since nothing else runs inside it.
370        #[cfg(feature = "tracing")]
371        let span = crate::telemetry::fetching_async();
372
373        let started = Instant::now();
374
375        let outcome = match source {
376            Kind::Blocking(source) => crate::asynchronous::off_thread(move || source.fetch()).await,
377            Kind::Asynchronous(source) => source.fetch().await,
378        };
379
380        match outcome {
381            Ok(fetched) => {
382                let elapsed = started.elapsed();
383
384                self.commit(fetched, fence);
385                self.record_fetch(Some(elapsed), fence.generation);
386
387                #[cfg(feature = "tracing")]
388                crate::telemetry::fetched(&span, elapsed);
389
390                Ok(())
391            }
392            Err(error) => {
393                self.record_fetch_failure(&error, fence.generation);
394
395                #[cfg(feature = "tracing")]
396                crate::telemetry::fetch_failed(&span, &error);
397
398                Err(error)
399            }
400        }
401    }
402
403    /// The generation a sink created now would carry; see [`RemoteSink`](crate::RemoteSink).
404    pub(crate) fn generation(&self) -> u64 {
405        self.state().generation
406    }
407
408    /// Installs `document` if the source it came from is still the one
409    /// installed — the push-side twin of the fetch fence.
410    ///
411    /// # Errors
412    ///
413    /// When the source has been replaced since `generation` was captured:
414    /// the document belongs to a store nobody asked about any more, and
415    /// installing it would hand a stale watcher the last word.
416    pub(crate) fn install_if(&self, generation: u64, document: Fetched) -> Result<(), Error> {
417        let mut state = self.state();
418
419        if state.generation != generation {
420            return Err(Error::new(
421                crate::ErrorKind::Backend,
422                "the remote source this sink was created for has been \
423                 replaced; stop the old watch loop and take a fresh sink \
424                 from `remote_sink()`",
425            ));
426        }
427
428        state.fetched = Some(document);
429
430        // A push is a fetch somebody else performed: the store answered, and
431        // that is the whole question `RemoteStatus` reports on. Whether the
432        // document then *installs* is `ConfigStatus`'s business, and
433        // `RemoteSink::apply` records it there through the reload it runs.
434        state.status.fetches = state.status.fetches.saturating_add(1);
435        state.status.last_fetch = Some(Instant::now());
436        state.status.last_fetch_duration = None;
437        state.status.consecutive_failures = 0;
438
439        Ok(())
440    }
441
442    /// Not public API: the loom suite's door to the fence internals.
443    #[cfg(loom)]
444    #[doc(hidden)]
445    #[must_use]
446    pub fn generation_for_loom(&self) -> u64 {
447        self.generation()
448    }
449
450    /// Not public API: the loom suite's door to the fence internals.
451    ///
452    /// # Errors
453    ///
454    /// As `install_if`.
455    #[cfg(loom)]
456    #[doc(hidden)]
457    pub fn install_if_for_loom(&self, generation: u64, document: Fetched) -> Result<(), Error> {
458        self.install_if(generation, document)
459    }
460
461    /// How the installed store learns that its document changed.
462    ///
463    /// `None` when nothing is installed. What an agent reads to decide
464    /// whether to run a watch at all, and what a report prints so an
465    /// operator can see why a change took as long as it did.
466    #[must_use]
467    pub fn watch_capability(&self) -> Option<WatchCapability> {
468        match self.state().source.as_ref()? {
469            Kind::Blocking(source) => Some(source.watch_capability()),
470
471            #[cfg(feature = "async")]
472            Kind::Asynchronous(source) => Some(source.watch_capability()),
473        }
474    }
475
476    /// Watches the installed store, keeping every document it delivers.
477    ///
478    /// The store's own mechanism where it has one — a blocking query, a
479    /// stream, a subscription — and a jittered, backing-off poll where it
480    /// does not. `interval` is the poll period, and a *resync* period for a
481    /// store that pushes: a stream that has silently stalled looks exactly
482    /// like a store where nothing has changed.
483    ///
484    /// Blocks until the watch is stopped, so it belongs on a thread of its
485    /// own. Each document is stored the way a
486    /// [`refresh`](Self::refresh) stores one, generation fence included —
487    /// a document from a source that has since been replaced is discarded
488    /// rather than installed.
489    ///
490    /// **It keeps the document; it does not reload the configuration.** A
491    /// `Remote` is a store handle and has no configuration to reload: no
492    /// type to read into, no hooks to fire, no cache to write. A change
493    /// that should reach `current()` on its own goes through
494    /// [`RemoteSink::apply`](crate::RemoteSink::apply) instead — take a
495    /// sink from the generated `remote_sink()` and call the store's own
496    /// `watch` with it. This is the right call when something else decides
497    /// when to reload.
498    ///
499    /// # Errors
500    ///
501    /// If no source is installed, if the installed one is async, or if the
502    /// store's watch gives up. A *fetch* failing is not an error: a watch
503    /// outlives an outage by design.
504    pub fn watch(&self, watching: &Watching, interval: Duration) -> Result<(), Error> {
505        let (source, fence) = {
506            let state = self.state();
507
508            match state.source.as_ref() {
509                Some(Kind::Blocking(source)) => (Arc::clone(source), Fence::of(&state)),
510
511                #[cfg(feature = "async")]
512                Some(Kind::Asynchronous(source)) => {
513                    return Err(Error::new(
514                        ErrorKind::Remote,
515                        format!(
516                            "`{}` is an async source; watch it with `watch_async`",
517                            source.describe()
518                        ),
519                    ))
520                }
521                None => {
522                    return Err(Error::new(
523                        ErrorKind::Remote,
524                        "no remote source is configured",
525                    ))
526                }
527            }
528        };
529
530        let mut deliver = |document| self.deliver(fence.generation, document);
531
532        if source.watch_capability() != WatchCapability::Native {
533            return source.watch(watching, interval, &mut deliver);
534        }
535
536        // A store that pushes still gets read on the interval, because the
537        // failure mode of a stream is silence: a connection that dropped
538        // without an error, a subscription the broker forgot, a blocking
539        // query answering an index that will never move again. All three
540        // look exactly like a store where nothing has changed, and the only
541        // way to tell them apart is to go and ask.
542        //
543        // Scoped threads rather than an executor: the resync is this call's
544        // for as long as this call lasts, and nothing outlives it.
545        std::thread::scope(|scope| {
546            let source = Arc::clone(&source);
547            let watcher = scope.spawn(move || source.watch(watching, interval, &mut deliver));
548            let mut pace = Pace::new(interval);
549
550            while watching.keep_going() && !watcher.is_finished() {
551                // Sliced rather than slept whole, so the resync notices the
552                // store's own watch returning instead of waiting out an
553                // interval to find out.
554                let mut left = pace.next_wait();
555
556                while left > Duration::ZERO && watching.keep_going() && !watcher.is_finished() {
557                    let slice = left.min(Duration::from_millis(250));
558
559                    std::thread::sleep(slice);
560                    left -= slice;
561                }
562
563                if watching.keep_going() && !watcher.is_finished() {
564                    match self.refresh() {
565                        Ok(()) => pace.succeeded(),
566                        Err(_) => pace.failed(),
567                    }
568                }
569            }
570
571            watcher
572                .join()
573                .unwrap_or_else(|_| Err(Error::new(ErrorKind::Remote, "the watch panicked")))
574        })
575    }
576
577    /// [`watch`](Self::watch), for a store that is read asynchronously.
578    ///
579    /// Cancellation is dropping the future.
580    ///
581    /// # Errors
582    ///
583    /// As [`watch`](Self::watch), with the sides swapped.
584    #[cfg(feature = "async")]
585    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
586    pub async fn watch_async(&self, watching: &Watching, interval: Duration) -> Result<(), Error> {
587        let (source, fence) = {
588            let state = self.state();
589
590            match state.source.as_ref() {
591                Some(Kind::Asynchronous(source)) => (Arc::clone(source), Fence::of(&state)),
592                Some(Kind::Blocking(source)) => {
593                    return Err(Error::new(
594                        ErrorKind::Remote,
595                        format!(
596                            "`{}` is a blocking source; watch it with `watch` on a thread",
597                            source.describe()
598                        ),
599                    ))
600                }
601                None => {
602                    return Err(Error::new(
603                        ErrorKind::Remote,
604                        "no remote source is configured",
605                    ))
606                }
607            }
608        };
609
610        let mut deliver = |document| self.deliver(fence.generation, document);
611
612        source.watch(watching, interval, &mut deliver).await
613    }
614
615    /// The document last fetched, if any.
616    #[must_use]
617    pub fn document(&self) -> Option<Fetched> {
618        self.state().fetched.clone()
619    }
620
621    /// Whether a source is installed.
622    #[must_use]
623    pub fn is_configured(&self) -> bool {
624        self.state().source.is_some()
625    }
626
627    /// How the fetches from this source have gone.
628    ///
629    /// One lock and a clone, no I/O and no network: an exporter may call it
630    /// per scrape, which is the same contract
631    /// [`ConfigCell::status`](crate::ConfigCell::status) makes.
632    #[must_use]
633    pub fn status(&self) -> RemoteStatus {
634        self.state().status.clone()
635    }
636
637    /// Records a fetch that returned a document.
638    ///
639    /// Fenced on the source `generation` the fetch started under, and under
640    /// the one lock that reads it: [`set`](Self::set) empties the status
641    /// along with the document, so an old fetch landing afterwards would
642    /// otherwise report the *replacement* as fetched and healthy — a store
643    /// nothing has yet spoken to.
644    fn record_fetch(&self, elapsed: Option<Duration>, generation: u64) {
645        let mut state = self.state();
646
647        if state.generation != generation {
648            return;
649        }
650
651        state.status.fetches = state.status.fetches.saturating_add(1);
652        state.status.last_fetch = Some(Instant::now());
653        state.status.last_fetch_duration = elapsed;
654        state.status.consecutive_failures = 0;
655    }
656
657    /// Records a fetch that returned nothing.
658    ///
659    /// The document is untouched: a store that stopped answering leaves the
660    /// last one it did answer with in place, and the counter is what says
661    /// so. Only the failure's category and key path are kept — the same
662    /// [`FailureStatus`] a refused reload records, for the same reason.
663    ///
664    /// Fenced like [`record_fetch`](Self::record_fetch), and for the mirror
665    /// reason: an old fetch's failure must not report a store that has just
666    /// been installed as down.
667    fn record_fetch_failure(&self, error: &Error, generation: u64) {
668        let mut state = self.state();
669
670        if state.generation != generation {
671            return;
672        }
673
674        // Saturating rather than wrapping, as `ConfigCell` does: a counter
675        // that rolls over to zero reads as "healthy" at the worst moment.
676        state.status.consecutive_failures = state.status.consecutive_failures.saturating_add(1);
677        state.status.last_failure = Some(FailureStatus::of(error));
678    }
679
680    /// How the installed source names itself.
681    #[must_use]
682    pub fn describe(&self) -> Option<String> {
683        // The lock is held only for the clone: `describe()` on the source runs
684        // unlocked, so a source whose description does real work cannot stall
685        // readers.
686        let source = self.state().source.clone()?;
687
688        Some(match source {
689            Kind::Blocking(source) => source.describe(),
690            #[cfg(feature = "async")]
691            Kind::Asynchronous(source) => source.describe(),
692        })
693    }
694
695    /// Drops the document, so the next load sees no remote layer.
696    ///
697    /// A fetch that was already in flight is discarded when it lands, the
698    /// same way [`set`](Self::set) discards one: clearing is a state change
699    /// like any other, and a document a caller explicitly dropped must not
700    /// come back from a round trip that started before they dropped it.
701    ///
702    /// The *source* is left alone, and so is every [`RemoteSink`](crate::RemoteSink) taken from
703    /// it: a watch loop delivering from the same store keeps delivering, and
704    /// its next push installs normally. Dropping the document is not
705    /// replacing the store, and only replacing the store ends a watch.
706    pub fn clear(&self) {
707        let mut state = self.state();
708
709        state.fetched = None;
710        state.cleared = state.cleared.wrapping_add(1);
711    }
712
713    /// One document from a watch, stored and counted.
714    ///
715    /// **Fenced on the generation alone, unlike a fetch.** The two fences
716    /// answer different questions. A fetch was in flight *across* whatever
717    /// happened, so a `clear()` while it flew has to discard it — putting
718    /// the document back would undo the clear. A watch delivery happened
719    /// *after*: it is the store saying what it holds now, and
720    /// [`clear`](Self::clear) promises exactly that — "a watch loop
721    /// delivering from the same store keeps delivering, and its next push
722    /// installs normally".
723    ///
724    /// Sharing the fetch fence broke that promise silently: one `clear()`
725    /// bumped `cleared`, every later document was dropped, and
726    /// `record_fetch` — which fences on the generation — went on reporting
727    /// the store as healthy. A watch that has installed nothing for an hour
728    /// while its status says `reachable` is worse than one that stopped.
729    ///
730    /// # Errors
731    ///
732    /// If the source was replaced. The watch belongs to a store nobody is
733    /// asking about any more, so it ends rather than delivering into a slot
734    /// that moved on — the same answer [`RemoteSink::apply`] gives.
735    fn deliver(&self, generation: u64, document: Fetched) -> Result<(), Error> {
736        self.install_if(generation, document)
737    }
738
739    /// Stores a fetch result, unless the slot moved while it was in flight —
740    /// the source was replaced, and the result belongs to a store nobody
741    /// asked about any more, or the document was cleared and putting this
742    /// one back would undo that.
743    fn commit(&self, fetched: Fetched, fence: Fence) {
744        let mut state = self.state();
745
746        if Fence::of(&state) == fence {
747            state.fetched = Some(fetched);
748        }
749    }
750
751    fn state(&self) -> crate::sync::MutexGuard<'_, State> {
752        self.state
753            .lock()
754            .unwrap_or_else(std::sync::PoisonError::into_inner)
755    }
756}
757
758impl std::fmt::Debug for Remote {
759    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
760        f.debug_struct("Remote")
761            .field("source", &self.describe())
762            .field("fetched", &self.document().is_some())
763            .finish()
764    }
765}
766
767fn none_installed() -> Error {
768    Error::new(
769        ErrorKind::Remote,
770        "no remote source is installed; call `set_remote` first",
771    )
772}
773
774#[cfg(test)]
775mod tests {
776    use super::*;
777    use crate::source::Format;
778    use crate::sync::atomic::Ordering;
779
780    struct Fake(&'static str);
781
782    impl RemoteSource for Fake {
783        fn fetch(&self) -> Result<Fetched, Error> {
784            Ok(Fetched::new(self.0, Format::Json))
785        }
786
787        fn describe(&self) -> String {
788            "a fake store".to_owned()
789        }
790    }
791
792    struct Broken;
793
794    impl RemoteSource for Broken {
795        fn fetch(&self) -> Result<Fetched, Error> {
796            Err(Error::remote("the store is unreachable"))
797        }
798
799        fn describe(&self) -> String {
800            "a broken store".to_owned()
801        }
802    }
803
804    #[test]
805    fn nothing_is_fetched_until_it_is_asked_for() {
806        let remote = Remote::new();
807        remote.set(Fake(r#"{"db": {"host": "a"}}"#));
808
809        assert!(remote.is_configured());
810        assert!(
811            remote.document().is_none(),
812            "installing a source must not reach the network"
813        );
814
815        remote.refresh().unwrap();
816        assert!(remote.document().is_some());
817    }
818
819    /// Succeeds once, then fails — a store that answered and went away.
820    struct Flaky(std::sync::atomic::AtomicBool);
821
822    impl RemoteSource for Flaky {
823        fn fetch(&self) -> Result<Fetched, Error> {
824            if self.0.swap(true, Ordering::SeqCst) {
825                return Err(Error::remote("the store went away"));
826            }
827
828            Fake(r#"{"db": {"host": "a"}}"#).fetch()
829        }
830
831        fn describe(&self) -> String {
832            "a store that answers once".to_owned()
833        }
834    }
835
836    #[test]
837    fn a_failed_fetch_leaves_the_previous_document_alone() {
838        let remote = Remote::new();
839        remote.set(Flaky(std::sync::atomic::AtomicBool::new(false)));
840        remote.refresh().unwrap();
841
842        let before = remote.document();
843        assert!(before.is_some(), "the first fetch succeeds");
844
845        // The second fetch *fails*, and the failure must surface — while the
846        // document from the fetch that worked stays where it was.
847        let error = remote.refresh().unwrap_err();
848
849        assert!(error.to_string().contains("went away"), "{error}");
850        assert_eq!(remote.document(), before);
851    }
852
853    /// Blocks inside `fetch` on a pair of barriers, so a test can hold a
854    /// fetch mid-flight while it does something else to the `Remote`.
855    struct Parked {
856        started: std::sync::Arc<std::sync::Barrier>,
857        release: std::sync::Arc<std::sync::Barrier>,
858    }
859
860    impl RemoteSource for Parked {
861        fn fetch(&self) -> Result<Fetched, Error> {
862            self.started.wait();
863            self.release.wait();
864
865            Fake(r#"{"db": {"host": "stale"}}"#).fetch()
866        }
867
868        fn describe(&self) -> String {
869            "a parked store".to_owned()
870        }
871    }
872
873    /// The same, for the failing half of the fence: parked mid-fetch, and
874    /// what it finally returns is an error.
875    struct ParkedThenBroken {
876        started: std::sync::Arc<std::sync::Barrier>,
877        release: std::sync::Arc<std::sync::Barrier>,
878    }
879
880    impl RemoteSource for ParkedThenBroken {
881        fn fetch(&self) -> Result<Fetched, Error> {
882            self.started.wait();
883            self.release.wait();
884
885            Broken.fetch()
886        }
887
888        fn describe(&self) -> String {
889            "a parked store that then breaks".to_owned()
890        }
891    }
892
893    /// The race the generation fence exists for: a fetch from the *old*
894    /// source lands after `set` installed a new one. Its result must be
895    /// discarded — new source, old store's document is the state this
896    /// module's docs promise cannot happen.
897    #[test]
898    fn a_fetch_from_a_replaced_source_is_discarded() {
899        let started = std::sync::Arc::new(std::sync::Barrier::new(2));
900        let release = std::sync::Arc::new(std::sync::Barrier::new(2));
901
902        let remote = std::sync::Arc::new(Remote::new());
903        remote.set(Parked {
904            started: std::sync::Arc::clone(&started),
905            release: std::sync::Arc::clone(&release),
906        });
907
908        let refresher = {
909            let remote = std::sync::Arc::clone(&remote);
910            std::thread::spawn(move || remote.refresh())
911        };
912
913        // The fetch is provably in flight...
914        started.wait();
915
916        // ...when the source is replaced.
917        remote.set(Fake(r#"{"db": {"host": "fresh"}}"#));
918
919        release.wait();
920        refresher
921            .join()
922            .expect("the refresher must not panic")
923            .expect("the fetch itself succeeded");
924
925        assert_eq!(
926            remote.document(),
927            None,
928            "the old source's document landed after the replacement and must \
929             not be paired with the new source"
930        );
931
932        // And the new source works normally.
933        remote.refresh().unwrap();
934        assert!(remote.document().unwrap().text.contains("fresh"));
935    }
936
937    /// The same fence, from the other side: `clear()` is a state change too,
938    /// so a fetch that was in flight when a caller cleared the slot must not
939    /// put the document back. The barriers force the interleaving — the
940    /// fetch is provably parked when `clear` runs — so this is a proof
941    /// rather than a race the scheduler usually loses.
942    #[test]
943    fn a_fetch_in_flight_when_the_slot_is_cleared_is_discarded() {
944        let started = std::sync::Arc::new(std::sync::Barrier::new(2));
945        let release = std::sync::Arc::new(std::sync::Barrier::new(2));
946
947        let remote = std::sync::Arc::new(Remote::new());
948        remote.set(Parked {
949            started: std::sync::Arc::clone(&started),
950            release: std::sync::Arc::clone(&release),
951        });
952
953        let refresher = {
954            let remote = std::sync::Arc::clone(&remote);
955            std::thread::spawn(move || remote.refresh())
956        };
957
958        started.wait();
959
960        remote.clear();
961
962        release.wait();
963        refresher
964            .join()
965            .expect("the refresher must not panic")
966            .expect("the fetch itself succeeded");
967
968        assert_eq!(
969            remote.document(),
970            None,
971            "a document the caller cleared must not come back from a fetch \
972             that started before they cleared it"
973        );
974    }
975
976    /// Clearing the document must not end a watch. The source is untouched
977    /// by `clear()`, so a loop that took its sink before the call is still
978    /// serving the store it was created for, and its next delivery installs
979    /// like any other. The first shape of this fence counted both events on
980    /// one number and made every live sink permanently stale.
981    #[test]
982    fn clearing_the_document_leaves_a_watchs_sink_alive() {
983        let remote = Remote::new();
984        remote.set(Fake(r#"{"db": {"host": "a"}}"#));
985
986        // What `remote_sink()` captures, once, where a loop starts.
987        let generation = remote.generation();
988
989        remote.clear();
990
991        remote
992            .install_if(generation, Fetched::new("{}", crate::Format::Json))
993            .expect("clearing the document does not replace the source");
994        assert!(remote.document().is_some());
995    }
996
997    /// The status fence, from the side `set` opens: an old fetch that
998    /// succeeds after its source was replaced must not report the
999    /// replacement — which nothing has yet spoken to — as fetched and
1000    /// healthy. `set` empties the status precisely so that it says nothing
1001    /// about a store that is no longer installed.
1002    #[test]
1003    fn a_late_fetch_does_not_report_the_replacement_as_healthy() {
1004        let started = std::sync::Arc::new(std::sync::Barrier::new(2));
1005        let release = std::sync::Arc::new(std::sync::Barrier::new(2));
1006
1007        let remote = std::sync::Arc::new(Remote::new());
1008        remote.set(Parked {
1009            started: std::sync::Arc::clone(&started),
1010            release: std::sync::Arc::clone(&release),
1011        });
1012
1013        let refresher = {
1014            let remote = std::sync::Arc::clone(&remote);
1015            std::thread::spawn(move || remote.refresh())
1016        };
1017
1018        started.wait();
1019        remote.set(Fake(r#"{"db": {"host": "fresh"}}"#));
1020        release.wait();
1021
1022        let _ = refresher.join().expect("the refresher must not panic");
1023
1024        let status = remote.status();
1025        assert_eq!(
1026            status.fetches, 0,
1027            "the replacement has been fetched from nobody"
1028        );
1029        assert_eq!(status.last_fetch, None);
1030        assert_eq!(status.reachable(), None);
1031    }
1032
1033    /// The same fence for a failure. A store that was replaced while its
1034    /// fetch was erroring must not leave the new one looking down.
1035    #[test]
1036    fn a_late_failure_does_not_report_the_replacement_as_down() {
1037        let started = std::sync::Arc::new(std::sync::Barrier::new(2));
1038        let release = std::sync::Arc::new(std::sync::Barrier::new(2));
1039
1040        let remote = std::sync::Arc::new(Remote::new());
1041        remote.set(ParkedThenBroken {
1042            started: std::sync::Arc::clone(&started),
1043            release: std::sync::Arc::clone(&release),
1044        });
1045
1046        let refresher = {
1047            let remote = std::sync::Arc::clone(&remote);
1048            std::thread::spawn(move || remote.refresh())
1049        };
1050
1051        started.wait();
1052        remote.set(Fake(r#"{"db": {"host": "fresh"}}"#));
1053        release.wait();
1054
1055        let _ = refresher.join().expect("the refresher must not panic");
1056
1057        let status = remote.status();
1058        assert_eq!(status.consecutive_failures, 0);
1059        assert_eq!(
1060            status.reachable(),
1061            None,
1062            "nothing has yet asked the replacement anything"
1063        );
1064    }
1065
1066    /// Readers must not wait for a slow store: `document()` and `describe()`
1067    /// are on the `load()` path, and `load()` promises to touch no network.
1068    #[test]
1069    fn readers_are_not_blocked_by_a_fetch_in_flight() {
1070        let started = std::sync::Arc::new(std::sync::Barrier::new(2));
1071        let release = std::sync::Arc::new(std::sync::Barrier::new(2));
1072
1073        let remote = std::sync::Arc::new(Remote::new());
1074        remote.set(Parked {
1075            started: std::sync::Arc::clone(&started),
1076            release: std::sync::Arc::clone(&release),
1077        });
1078
1079        let refresher = {
1080            let remote = std::sync::Arc::clone(&remote);
1081            std::thread::spawn(move || remote.refresh())
1082        };
1083
1084        started.wait();
1085
1086        // With the fetch parked, a reader thread must finish promptly. The
1087        // old two-lock design held the source lock across the fetch, so
1088        // `describe()` — and with it every `load()` — waited out the store's
1089        // full timeout.
1090        let (sender, receiver) = std::sync::mpsc::channel();
1091        {
1092            let remote = std::sync::Arc::clone(&remote);
1093            std::thread::spawn(move || {
1094                let described = remote.describe();
1095                let document = remote.document();
1096                let _ = sender.send((described, document));
1097            });
1098        }
1099
1100        let (described, document) = receiver
1101            .recv_timeout(Duration::from_secs(2))
1102            .expect("readers must not wait for the network");
1103
1104        assert_eq!(described.as_deref(), Some("a parked store"));
1105        assert_eq!(document, None);
1106
1107        release.wait();
1108        let _ = refresher.join();
1109    }
1110
1111    /// `set` clears the document atomically with the source swap: no
1112    /// interleaving may observe the new source paired with any document.
1113    #[test]
1114    fn replacing_the_source_and_dropping_the_document_is_one_step() {
1115        let remote = Remote::new();
1116        remote.set(Fake(r#"{"db": {"host": "a"}}"#));
1117        remote.refresh().unwrap();
1118        let generation = remote.generation();
1119        remote
1120            .install_if(generation, Fetched::new("{}", crate::Format::Json))
1121            .expect("the source has not moved");
1122
1123        remote.set(Fake(r#"{"db": {"host": "b"}}"#));
1124
1125        assert_eq!(remote.document(), None);
1126
1127        // And the push-side fence itself: the pre-swap generation is now
1128        // stale, so a late delivery bounces instead of landing.
1129        remote
1130            .install_if(generation, Fetched::new("{}", crate::Format::Json))
1131            .expect_err("a replaced source's generation must be refused");
1132        assert_eq!(remote.document(), None);
1133    }
1134
1135    #[test]
1136    fn a_broken_store_reports_rather_than_pretending() {
1137        let remote = Remote::new();
1138        remote.set(Broken);
1139
1140        let error = remote.refresh().unwrap_err();
1141
1142        assert_eq!(error.kind(), ErrorKind::Remote);
1143        assert!(error.to_string().contains("unreachable"), "{error}");
1144    }
1145
1146    #[test]
1147    fn refreshing_with_no_source_says_so() {
1148        let error = Remote::new().refresh().unwrap_err();
1149
1150        assert!(error.to_string().contains("set_remote"), "{error}");
1151    }
1152
1153    #[test]
1154    fn replacing_the_source_drops_the_old_document() {
1155        let remote = Remote::new();
1156        remote.set(Fake(r#"{"db": {"host": "a"}}"#));
1157        remote.refresh().unwrap();
1158
1159        remote.set(Fake(r#"{"db": {"host": "b"}}"#));
1160
1161        assert!(
1162            remote.document().is_none(),
1163            "a new source answering with the old store's values would be a puzzle"
1164        );
1165    }
1166}