Skip to main content

dynamic_config/
remote.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`] — 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`](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`]'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
96use std::sync::{Arc, Weak};
97
98use crate::sync::atomic::{AtomicBool, Ordering};
99use crate::sync::Mutex;
100use std::time::{Duration, Instant};
101
102use crate::error::{Error, ErrorKind};
103use crate::reload::FailureStatus;
104use crate::source::Format;
105
106/// A document a remote store handed back.
107#[derive(Clone, PartialEq, Eq)]
108pub struct Fetched {
109    /// The document text, in `format`.
110    pub text: String,
111    /// How to parse it.
112    pub format: Format,
113}
114
115impl Fetched {
116    /// A document and the format it is written in.
117    #[must_use]
118    pub fn new(text: impl Into<String>, format: Format) -> Self {
119        Self {
120            text: text.into(),
121            format,
122        }
123    }
124}
125
126// The document is the one thing a `Debug` of this type must never print:
127// a remote store's flagship use case is serving secrets, and `Fetched` is
128// what every watch callback receives — one `tracing::debug!(?document)` away
129// from a log. The length is enough to debug with.
130impl std::fmt::Debug for Fetched {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        f.debug_struct("Fetched")
133            .field("format", &self.format)
134            .field("bytes", &self.text.len())
135            .finish()
136    }
137}
138
139/// A remote store that can be read without an async runtime.
140///
141/// The right trait for anything with a plain HTTP API — Consul and Vault both
142/// are — because implementing it needs no runtime and using it needs no
143/// runtime either. `fetch` may block; it is called from
144/// `refresh_remote()`, never from `load()`.
145pub trait RemoteSource: Send + Sync + 'static {
146    /// Reads the current document.
147    ///
148    /// # Errors
149    ///
150    /// Whatever going wrong looks like for this store. Use
151    /// [`Error::remote`](crate::Error::remote) so the failure is categorised
152    /// consistently, or [`Error::auth`](crate::Error::auth) for a credential
153    /// the store itself refused — that is the distinction a watch loop backs
154    /// off on rather than stopping.
155    fn fetch(&self) -> Result<Fetched, Error>;
156
157    /// How to name this source in an error or a report.
158    fn describe(&self) -> String;
159}
160
161/// A remote store that is read asynchronously.
162///
163/// The right trait for a client that is async to begin with — etcd speaks gRPC
164/// and NATS is a streaming protocol, so both are. Used through
165/// `refresh_remote_async().await`.
166///
167/// The lifetime-bound boxed future rather than `async fn`: this trait is
168/// object-safe on purpose, so a configuration type can hold one without being
169/// generic over it.
170#[cfg(feature = "async")]
171#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
172pub trait AsyncRemoteSource: Send + Sync + 'static {
173    /// Reads the current document.
174    ///
175    /// # Errors
176    ///
177    /// As [`RemoteSource::fetch`].
178    fn fetch(
179        &self,
180    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Fetched, Error>> + Send + '_>>;
181
182    /// How to name this source in an error or a report.
183    fn describe(&self) -> String;
184}
185
186/// What is true of a remote source right now, for an operator asking.
187///
188/// The fetch half of the picture [`ConfigStatus`](crate::ConfigStatus)
189/// starts, and deliberately the *same* picture rather than a second one:
190/// the same [`FailureStatus`] type, the same `consecutive_failures` meaning
191/// zero-is-healthy, the same recorded-where-it-happens rule, and the same
192/// rendering through [`telemetry::Exposition`](crate::telemetry::Exposition).
193/// Two vocabularies for one question is how two surfaces come to disagree
194/// after the first bug.
195///
196/// The two do not overlap, and the split is worth stating because it is the
197/// distinction an operator is actually asking about:
198///
199/// | Question | Where it is answered |
200/// |---|---|
201/// | did the **store** answer | here |
202/// | did the **document** install | [`ConfigStatus`](crate::ConfigStatus) |
203///
204/// A fetch that returned an unchanged document is a success here and is not
205/// an install there, which is exactly the case neither surface could report
206/// before this type existed.
207///
208/// # What it does not carry
209///
210/// **No document, no key, and no description of the store.** A store's
211/// description is its URL, and a store URL routinely embeds
212/// `user:password@host` — so nothing here is derived from
213/// [`describe`](Remote::describe), and the name a metric is labelled with
214/// is the caller's own, exactly as it is for a `ConfigStatus`.
215#[derive(Debug, Clone, Default, PartialEq, Eq)]
216#[non_exhaustive]
217pub struct RemoteStatus {
218    /// Documents this slot has received since the process started, whether
219    /// pulled by [`refresh`](Remote::refresh) or pushed through
220    /// [`RemoteSink::apply`].
221    pub fetches: u64,
222    /// When the last of them arrived. `None` before the first.
223    pub last_fetch: Option<Instant>,
224    /// How long the last *pulled* fetch took.
225    ///
226    /// `None` before the first pull, and `None` again after a document
227    /// arrives by push: a watch loop's own round trip is timed by the store
228    /// crate that made it, and reporting the previous pull's duration beside
229    /// a push's timestamp would be a number that is not about the fetch it
230    /// appears to describe.
231    pub last_fetch_duration: Option<Duration>,
232    /// The most recent fetch that returned nothing, if there has been one.
233    /// Kept after a later success: it is history, and
234    /// [`consecutive_failures`](Self::consecutive_failures) is the health.
235    pub last_failure: Option<FailureStatus>,
236    /// Fetches that returned nothing since one returned a document.
237    /// **Zero means healthy.**
238    pub consecutive_failures: u32,
239}
240
241impl RemoteStatus {
242    /// Whether the store answered the last time it was asked.
243    ///
244    /// Three states rather than two, and the third is the point: `None`
245    /// before anything has been asked of the store at all. A source that has
246    /// been installed and never fetched is not *down* — reporting it as down
247    /// is how a scrape at startup pages somebody — so the metric is absent
248    /// rather than zero, exactly as `last_success_seconds` is.
249    #[must_use]
250    pub fn reachable(&self) -> Option<bool> {
251        if self.fetches == 0 && self.consecutive_failures == 0 {
252            return None;
253        }
254
255        Some(self.consecutive_failures == 0)
256    }
257
258    /// A status with nothing recorded yet.
259    ///
260    /// `const`, because [`Remote::new`] is: a `Remote` lives in a `static`.
261    /// `Default` cannot be, which is the only reason this exists.
262    const fn empty() -> Self {
263        Self {
264            fetches: 0,
265            last_fetch: None,
266            last_fetch_duration: None,
267            last_failure: None,
268            consecutive_failures: 0,
269        }
270    }
271
272    /// How long ago the last document arrived from the store.
273    ///
274    /// `None` before the first. Monotonic, for the reason
275    /// [`ConfigStatus::stale_for`](crate::ConfigStatus::stale_for) is: a wall
276    /// clock going backwards under NTP would make a fresh fetch look stale.
277    #[must_use]
278    pub fn stale_for(&self) -> Option<Duration> {
279        self.last_fetch.map(|at| at.elapsed())
280    }
281}
282
283/// The remote source for one configuration type, and its last document.
284///
285/// `Remote::new()` is `const`, so this lives in a `static` — which is how
286/// `#[dynamic_config]` emits it.
287///
288/// # What it records about itself
289///
290/// Every fetch this type performs and every delivery it accepts is counted
291/// into a [`RemoteStatus`], on the same terms `ConfigCell` records a
292/// [`ConfigStatus`](crate::ConfigStatus): recorded where it happens, read by
293/// an atomic-cheap [`status`](Self::status), and never on the read path —
294/// `load()` reads [`document`](Self::document), which this does not touch.
295/// The cost is one `Instant::now()` per fetch, beside a network round trip.
296#[derive(Default)]
297pub struct Remote {
298    /// One lock for the whole state, deliberately. Two separate locks — one
299    /// for the source, one for the document — allowed an interleaving where
300    /// a slow fetch from the *old* source committed its result after `set`
301    /// had installed a new one: new source, old store's document. The
302    /// generation counter is the fence that makes that impossible.
303    state: Mutex<State>,
304}
305
306#[derive(Default)]
307struct State {
308    source: Option<Kind>,
309    fetched: Option<Fetched>,
310    /// Bumped on every source change. A fetch snapshots it before the network
311    /// round trip and commits only if it has not moved — a result from a
312    /// source that is no longer installed is discarded, never stored.
313    ///
314    /// It is *source identity*, and that is the whole of it: a
315    /// [`RemoteSink`] holds one for the life of a watch loop, so anything
316    /// that moves this number ends that loop.
317    generation: u64,
318    /// Bumped by [`clear`](Remote::clear), and by nothing else.
319    ///
320    /// A counter of its own rather than a bump of `generation`, because the
321    /// two questions differ: clearing drops the *document* and leaves the
322    /// source installed. Folding it into `generation` made every live
323    /// [`RemoteSink`] permanently stale — a watch loop whose store had not
324    /// changed and whose stream was still delivering would have every later
325    /// push refused for belonging to a source that had been "replaced". The
326    /// in-flight fetch a `clear` must still discard is fenced on this.
327    cleared: u64,
328    /// How the fetches have gone. Under the same lock as everything else
329    /// here, so a scrape cannot read a count that belongs to one source
330    /// beside a document that belongs to another.
331    status: RemoteStatus,
332}
333
334/// The state a fetch started under, in the two numbers that can invalidate
335/// its result: the source it was fetching from, and the document epoch it
336/// was fetching into.
337///
338/// Captured before the round trip and compared after it. Both halves are
339/// needed and neither is enough: a replaced source must discard the result,
340/// and so must a `clear` — but only the first ends a watch, which is why
341/// they are counted apart.
342#[derive(Clone, Copy, PartialEq, Eq)]
343struct Fence {
344    generation: u64,
345    cleared: u64,
346}
347
348impl Fence {
349    fn of(state: &State) -> Self {
350        Self {
351            generation: state.generation,
352            cleared: state.cleared,
353        }
354    }
355}
356
357/// `Arc` rather than `Box`: an async fetch borrows the source across an await
358/// point, and cloning the handle out of the lock first is what keeps a `std`
359/// mutex from being held across one.
360#[derive(Clone)]
361enum Kind {
362    Blocking(Arc<dyn RemoteSource>),
363    #[cfg(feature = "async")]
364    Asynchronous(Arc<dyn AsyncRemoteSource>),
365}
366
367impl Remote {
368    /// An empty slot: no source, no document.
369    #[must_use]
370    #[cfg(not(loom))]
371    pub const fn new() -> Self {
372        Self {
373            state: Mutex::new(State {
374                source: None,
375                fetched: None,
376                generation: 0,
377                cleared: 0,
378                status: RemoteStatus::empty(),
379            }),
380        }
381    }
382
383    /// The same, minus `const`: loom's constructors are not.
384    #[must_use]
385    #[cfg(loom)]
386    pub fn new() -> Self {
387        Self {
388            state: Mutex::new(State {
389                source: None,
390                fetched: None,
391                generation: 0,
392                cleared: 0,
393                status: RemoteStatus::empty(),
394            }),
395        }
396    }
397
398    /// Installs a blocking source, replacing any previous one.
399    ///
400    /// The document already fetched, if any, is dropped with it — a new source
401    /// answering with an old store's values would be a puzzle nobody needs.
402    /// A fetch from the previous source that is still in flight is discarded
403    /// when it lands, for the same reason.
404    ///
405    /// The recorded [`status`](Self::status) is dropped with the document:
406    /// `remote_up` for the *previous* store says nothing about this one, and
407    /// a stale `1` describing a store nobody is talking to any more is worse
408    /// than no sample at all.
409    pub fn set(&self, source: impl RemoteSource) {
410        let mut state = self.state();
411        state.source = Some(Kind::Blocking(Arc::new(source)));
412        state.fetched = None;
413        state.status = RemoteStatus::empty();
414        state.generation = state.generation.wrapping_add(1);
415    }
416
417    /// Installs an async source, replacing any previous one.
418    #[cfg(feature = "async")]
419    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
420    pub fn set_async(&self, source: impl AsyncRemoteSource) {
421        let mut state = self.state();
422        state.source = Some(Kind::Asynchronous(Arc::new(source)));
423        state.fetched = None;
424        state.status = RemoteStatus::empty();
425        state.generation = state.generation.wrapping_add(1);
426    }
427
428    /// Fetches, and keeps what came back.
429    ///
430    /// The network round trip happens with no lock held: a slow store cannot
431    /// make `load()` — which reads this state for provenance — wait for it.
432    /// If the source is replaced while the fetch is in flight, the result is
433    /// discarded and `Ok` is returned: the fetch *did* succeed, and the new
434    /// source's own refresh is the one that matters now.
435    ///
436    /// # Errors
437    ///
438    /// If no source is installed, if the installed one is async — use
439    /// [`refresh_async`](Self::refresh_async) — or if the fetch fails.
440    pub fn refresh(&self) -> Result<(), Error> {
441        let (source, fence) = {
442            let state = self.state();
443
444            match state.source.as_ref() {
445                Some(Kind::Blocking(source)) => (Arc::clone(source), Fence::of(&state)),
446
447                #[cfg(feature = "async")]
448                Some(Kind::Asynchronous(source)) => {
449                    return Err(Error::new(
450                        ErrorKind::Remote,
451                        format!(
452                            "`{}` is an async source; refresh it with `refresh_remote_async`",
453                            source.describe()
454                        ),
455                    ))
456                }
457
458                None => return Err(none_installed()),
459            }
460        };
461
462        // The span covers the round trip rather than following it, which is
463        // the only arrangement that gives a trace a duration to draw. It
464        // carries no name for the store: the one string a source has is its
465        // description, and a store URL routinely embeds `user:password@host`.
466        #[cfg(feature = "tracing")]
467        let span = crate::telemetry::fetching();
468
469        let started = Instant::now();
470
471        match source.fetch() {
472            Ok(fetched) => {
473                let elapsed = started.elapsed();
474
475                self.commit(fetched, fence);
476                self.record_fetch(Some(elapsed), fence.generation);
477
478                #[cfg(feature = "tracing")]
479                crate::telemetry::fetched(&span, elapsed);
480
481                Ok(())
482            }
483            Err(error) => {
484                self.record_fetch_failure(&error, fence.generation);
485
486                #[cfg(feature = "tracing")]
487                crate::telemetry::fetch_failed(&span, &error);
488
489                Err(error)
490            }
491        }
492    }
493
494    /// Fetches from an async source, and keeps what came back.
495    ///
496    /// A *blocking* source is not refused — swapping one implementation for
497    /// the other must not be a breaking change for the caller — but it is not
498    /// run on the executor either: it goes through
499    /// [`off_thread`](crate::off_thread), so an async caller's worker thread
500    /// never sits inside a blocking network call.
501    ///
502    /// The same replaced-mid-fetch rule as [`refresh`](Self::refresh)
503    /// applies, and matters more here: the unlocked window spans an await.
504    ///
505    /// # Errors
506    ///
507    /// If no source is installed, or the fetch fails.
508    #[cfg(feature = "async")]
509    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
510    pub async fn refresh_async(&self) -> Result<(), Error> {
511        // Cloned out of the lock before anything is awaited: holding a `std`
512        // mutex across an await point is how an executor deadlocks itself.
513        let (source, fence) = {
514            let state = self.state();
515
516            match state.source.as_ref() {
517                Some(source) => (source.clone(), Fence::of(&state)),
518                None => return Err(none_installed()),
519            }
520        };
521
522        // Not entered: this span is held across an await, and an
523        // `EnteredSpan` is `!Send`. `Span::in_scope` cannot wrap an await
524        // either, so what a subscriber gets here is the span's own timing
525        // and its fields rather than an ambient context — which is what a
526        // fetch has to report anyway, since nothing else runs inside it.
527        #[cfg(feature = "tracing")]
528        let span = crate::telemetry::fetching_async();
529
530        let started = Instant::now();
531
532        let outcome = match source {
533            Kind::Blocking(source) => crate::asynchronous::off_thread(move || source.fetch()).await,
534            Kind::Asynchronous(source) => source.fetch().await,
535        };
536
537        match outcome {
538            Ok(fetched) => {
539                let elapsed = started.elapsed();
540
541                self.commit(fetched, fence);
542                self.record_fetch(Some(elapsed), fence.generation);
543
544                #[cfg(feature = "tracing")]
545                crate::telemetry::fetched(&span, elapsed);
546
547                Ok(())
548            }
549            Err(error) => {
550                self.record_fetch_failure(&error, fence.generation);
551
552                #[cfg(feature = "tracing")]
553                crate::telemetry::fetch_failed(&span, &error);
554
555                Err(error)
556            }
557        }
558    }
559
560    /// The generation a sink created now would carry; see [`RemoteSink`].
561    pub(crate) fn generation(&self) -> u64 {
562        self.state().generation
563    }
564
565    /// Installs `document` if the source it came from is still the one
566    /// installed — the push-side twin of the fetch fence.
567    ///
568    /// # Errors
569    ///
570    /// When the source has been replaced since `generation` was captured:
571    /// the document belongs to a store nobody asked about any more, and
572    /// installing it would hand a stale watcher the last word.
573    pub(crate) fn install_if(&self, generation: u64, document: Fetched) -> Result<(), Error> {
574        let mut state = self.state();
575
576        if state.generation != generation {
577            return Err(Error::new(
578                crate::ErrorKind::Backend,
579                "the remote source this sink was created for has been \
580                 replaced; stop the old watch loop and take a fresh sink \
581                 from `remote_sink()`",
582            ));
583        }
584
585        state.fetched = Some(document);
586
587        // A push is a fetch somebody else performed: the store answered, and
588        // that is the whole question `RemoteStatus` reports on. Whether the
589        // document then *installs* is `ConfigStatus`'s business, and
590        // `RemoteSink::apply` records it there through the reload it runs.
591        state.status.fetches = state.status.fetches.saturating_add(1);
592        state.status.last_fetch = Some(Instant::now());
593        state.status.last_fetch_duration = None;
594        state.status.consecutive_failures = 0;
595
596        Ok(())
597    }
598
599    /// Not public API: the loom suite's door to the fence internals.
600    #[cfg(loom)]
601    #[doc(hidden)]
602    #[must_use]
603    pub fn generation_for_loom(&self) -> u64 {
604        self.generation()
605    }
606
607    /// Not public API: the loom suite's door to the fence internals.
608    ///
609    /// # Errors
610    ///
611    /// As `install_if`.
612    #[cfg(loom)]
613    #[doc(hidden)]
614    pub fn install_if_for_loom(&self, generation: u64, document: Fetched) -> Result<(), Error> {
615        self.install_if(generation, document)
616    }
617
618    /// The document last fetched, if any.
619    #[must_use]
620    pub fn document(&self) -> Option<Fetched> {
621        self.state().fetched.clone()
622    }
623
624    /// Whether a source is installed.
625    #[must_use]
626    pub fn is_configured(&self) -> bool {
627        self.state().source.is_some()
628    }
629
630    /// How the fetches from this source have gone.
631    ///
632    /// One lock and a clone, no I/O and no network: an exporter may call it
633    /// per scrape, which is the same contract
634    /// [`ConfigCell::status`](crate::ConfigCell::status) makes.
635    #[must_use]
636    pub fn status(&self) -> RemoteStatus {
637        self.state().status.clone()
638    }
639
640    /// Records a fetch that returned a document.
641    ///
642    /// Fenced on the source `generation` the fetch started under, and under
643    /// the one lock that reads it: [`set`](Self::set) empties the status
644    /// along with the document, so an old fetch landing afterwards would
645    /// otherwise report the *replacement* as fetched and healthy — a store
646    /// nothing has yet spoken to.
647    fn record_fetch(&self, elapsed: Option<Duration>, generation: u64) {
648        let mut state = self.state();
649
650        if state.generation != generation {
651            return;
652        }
653
654        state.status.fetches = state.status.fetches.saturating_add(1);
655        state.status.last_fetch = Some(Instant::now());
656        state.status.last_fetch_duration = elapsed;
657        state.status.consecutive_failures = 0;
658    }
659
660    /// Records a fetch that returned nothing.
661    ///
662    /// The document is untouched: a store that stopped answering leaves the
663    /// last one it did answer with in place, and the counter is what says
664    /// so. Only the failure's category and key path are kept — the same
665    /// [`FailureStatus`] a refused reload records, for the same reason.
666    ///
667    /// Fenced like [`record_fetch`](Self::record_fetch), and for the mirror
668    /// reason: an old fetch's failure must not report a store that has just
669    /// been installed as down.
670    fn record_fetch_failure(&self, error: &Error, generation: u64) {
671        let mut state = self.state();
672
673        if state.generation != generation {
674            return;
675        }
676
677        // Saturating rather than wrapping, as `ConfigCell` does: a counter
678        // that rolls over to zero reads as "healthy" at the worst moment.
679        state.status.consecutive_failures = state.status.consecutive_failures.saturating_add(1);
680        state.status.last_failure = Some(FailureStatus::of(error));
681    }
682
683    /// How the installed source names itself.
684    #[must_use]
685    pub fn describe(&self) -> Option<String> {
686        // The lock is held only for the clone: `describe()` on the source runs
687        // unlocked, so a source whose description does real work cannot stall
688        // readers.
689        let source = self.state().source.clone()?;
690
691        Some(match source {
692            Kind::Blocking(source) => source.describe(),
693            #[cfg(feature = "async")]
694            Kind::Asynchronous(source) => source.describe(),
695        })
696    }
697
698    /// Drops the document, so the next load sees no remote layer.
699    ///
700    /// A fetch that was already in flight is discarded when it lands, the
701    /// same way [`set`](Self::set) discards one: clearing is a state change
702    /// like any other, and a document a caller explicitly dropped must not
703    /// come back from a round trip that started before they dropped it.
704    ///
705    /// The *source* is left alone, and so is every [`RemoteSink`] taken from
706    /// it: a watch loop delivering from the same store keeps delivering, and
707    /// its next push installs normally. Dropping the document is not
708    /// replacing the store, and only replacing the store ends a watch.
709    pub fn clear(&self) {
710        let mut state = self.state();
711
712        state.fetched = None;
713        state.cleared = state.cleared.wrapping_add(1);
714    }
715
716    /// Stores a fetch result, unless the slot moved while it was in flight —
717    /// the source was replaced, and the result belongs to a store nobody
718    /// asked about any more, or the document was cleared and putting this
719    /// one back would undo that.
720    fn commit(&self, fetched: Fetched, fence: Fence) {
721        let mut state = self.state();
722
723        if Fence::of(&state) == fence {
724            state.fetched = Some(fetched);
725        }
726    }
727
728    fn state(&self) -> crate::sync::MutexGuard<'_, State> {
729        self.state
730            .lock()
731            .unwrap_or_else(std::sync::PoisonError::into_inner)
732    }
733}
734
735impl std::fmt::Debug for Remote {
736    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
737        f.debug_struct("Remote")
738            .field("source", &self.describe())
739            .field("fetched", &self.document().is_some())
740            .finish()
741    }
742}
743
744fn none_installed() -> Error {
745    Error::new(
746        ErrorKind::Remote,
747        "no remote source is installed; call `set_remote` first",
748    )
749}
750
751// ---------------------------------------------------------------------------
752// Stopping a blocking watch
753// ---------------------------------------------------------------------------
754
755/// A running blocking watch, from the caller's side.
756///
757/// Dropping it stops the loop — the same contract the file watcher's
758/// `WatchHandle` has, for the same reason: a watch nobody owns is a leak nobody
759/// asked for. [`detach`](Self::detach) is the way to say *this one really should
760/// run forever*.
761///
762/// Only blocking loops need this. An async watch is a future: drop it and it is
763/// cancelled, on any executor.
764///
765/// ```no_run
766/// # use dynamic_config::RemoteWatch;
767/// # struct Consul;
768/// # impl Consul {
769/// #     fn watch(&self, _: dynamic_config::Watching, _: fn(dynamic_config::Fetched) -> Result<(), dynamic_config::Error>) -> Result<(), dynamic_config::Error> { Ok(()) }
770/// # }
771/// # fn example(consul: Consul) {
772/// # fn apply(_: dynamic_config::Fetched) -> Result<(), dynamic_config::Error> { Ok(()) }
773/// let watch = RemoteWatch::new();
774/// let watching = watch.watching();
775///
776/// std::thread::spawn(move || consul.watch(watching, apply));
777///
778/// // ... and later, or by dropping `watch`:
779/// watch.stop();
780/// # }
781/// ```
782#[must_use = "dropping the handle stops the watch; bind it, or call `.detach()` \
783              to watch for the rest of the process"]
784#[derive(Debug)]
785pub struct RemoteWatch {
786    running: Arc<AtomicBool>,
787}
788
789impl RemoteWatch {
790    /// A handle for a watch that has not been handed to a loop yet.
791    pub fn new() -> Self {
792        Self {
793            running: Arc::new(AtomicBool::new(true)),
794        }
795    }
796
797    /// The loop's half of this handle.
798    ///
799    /// Hand it to the watch; keep the `RemoteWatch` yourself.
800    #[must_use]
801    pub fn watching(&self) -> Watching {
802        Watching {
803            running: Arc::downgrade(&self.running),
804        }
805    }
806
807    /// Stops the loop at its next check.
808    ///
809    /// *At its next check* is the whole caveat, and it is not small: a loop
810    /// parked in a blocking query does not return until the store answers or
811    /// the wait expires, so the store's wait time is the worst-case delay. Each
812    /// companion crate documents its own.
813    pub fn stop(&self) {
814        self.running.store(false, Ordering::Release);
815    }
816
817    /// Whether the loop has been told to stop.
818    #[must_use]
819    pub fn is_stopped(&self) -> bool {
820        !self.running.load(Ordering::Acquire)
821    }
822
823    /// Watches for the remainder of the process.
824    ///
825    /// Leaks the handle on purpose, exactly as the file watcher's
826    /// `WatchHandle::detach` does: a watch that must never stop has no owner to
827    /// hold it, and pretending otherwise is how it ends up stopped at the end of
828    /// `main`'s first statement.
829    pub fn detach(self) {
830        std::mem::forget(self);
831    }
832}
833
834impl Default for RemoteWatch {
835    fn default() -> Self {
836        Self::new()
837    }
838}
839
840impl Drop for RemoteWatch {
841    fn drop(&mut self) {
842        self.stop();
843    }
844}
845
846/// The loop's half of a [`RemoteWatch`].
847///
848/// A `Weak`, so a handle that is dropped without anyone remembering to call
849/// `stop` still ends the loop: the upgrade fails and
850/// [`keep_going`](Self::keep_going) answers `false`.
851#[derive(Debug, Clone)]
852pub struct Watching {
853    running: Weak<AtomicBool>,
854}
855
856impl Watching {
857    /// Whether the loop should go round again.
858    ///
859    /// `false` once the caller called [`RemoteWatch::stop`] or dropped the
860    /// handle. Check it before every request, not only after one: a loop that
861    /// checks only on the way out issues one more query than it was asked to.
862    #[must_use]
863    pub fn keep_going(&self) -> bool {
864        self.running
865            .upgrade()
866            .is_some_and(|running| running.load(Ordering::Acquire))
867    }
868
869    /// Sleeps for `total`, waking early if the watch is stopped.
870    ///
871    /// The polling loop every blocking store crate writes: sleep a slice,
872    /// check [`keep_going`](Self::keep_going), repeat — so a stopped watch
873    /// ends within a quarter second instead of at the end of its interval.
874    /// Here once, rather than once per store crate.
875    pub fn sleep_for(&self, total: Duration) {
876        const SLICE: Duration = Duration::from_millis(250);
877
878        let mut slept = Duration::ZERO;
879
880        while slept < total && self.keep_going() {
881            std::thread::sleep(SLICE.min(total - slept));
882            slept += SLICE;
883        }
884    }
885
886    /// A token for a watch that should never stop.
887    ///
888    /// For a loop the caller genuinely wants to outlive everything, so there is
889    /// no handle to hold. Prefer [`RemoteWatch::detach`], which says the same
890    /// thing at the point where somebody decided it.
891    #[must_use]
892    pub fn forever() -> Self {
893        // A `Weak` that can never upgrade would stop the loop immediately, so
894        // this leaks one live flag — one allocation, once, for the life of the
895        // process.
896        let running = Box::leak(Box::new(Arc::new(AtomicBool::new(true))));
897
898        Self {
899            running: Arc::downgrade(running),
900        }
901    }
902}
903
904#[cfg(test)]
905mod tests {
906    use super::*;
907
908    struct Fake(&'static str);
909
910    impl RemoteSource for Fake {
911        fn fetch(&self) -> Result<Fetched, Error> {
912            Ok(Fetched::new(self.0, Format::Json))
913        }
914
915        fn describe(&self) -> String {
916            "a fake store".to_owned()
917        }
918    }
919
920    struct Broken;
921
922    impl RemoteSource for Broken {
923        fn fetch(&self) -> Result<Fetched, Error> {
924            Err(Error::remote("the store is unreachable"))
925        }
926
927        fn describe(&self) -> String {
928            "a broken store".to_owned()
929        }
930    }
931
932    #[test]
933    fn nothing_is_fetched_until_it_is_asked_for() {
934        let remote = Remote::new();
935        remote.set(Fake(r#"{"db": {"host": "a"}}"#));
936
937        assert!(remote.is_configured());
938        assert!(
939            remote.document().is_none(),
940            "installing a source must not reach the network"
941        );
942
943        remote.refresh().unwrap();
944        assert!(remote.document().is_some());
945    }
946
947    /// Succeeds once, then fails — a store that answered and went away.
948    struct Flaky(std::sync::atomic::AtomicBool);
949
950    impl RemoteSource for Flaky {
951        fn fetch(&self) -> Result<Fetched, Error> {
952            if self.0.swap(true, Ordering::SeqCst) {
953                return Err(Error::remote("the store went away"));
954            }
955
956            Fake(r#"{"db": {"host": "a"}}"#).fetch()
957        }
958
959        fn describe(&self) -> String {
960            "a store that answers once".to_owned()
961        }
962    }
963
964    #[test]
965    fn a_failed_fetch_leaves_the_previous_document_alone() {
966        let remote = Remote::new();
967        remote.set(Flaky(std::sync::atomic::AtomicBool::new(false)));
968        remote.refresh().unwrap();
969
970        let before = remote.document();
971        assert!(before.is_some(), "the first fetch succeeds");
972
973        // The second fetch *fails*, and the failure must surface — while the
974        // document from the fetch that worked stays where it was.
975        let error = remote.refresh().unwrap_err();
976
977        assert!(error.to_string().contains("went away"), "{error}");
978        assert_eq!(remote.document(), before);
979    }
980
981    /// Blocks inside `fetch` on a pair of barriers, so a test can hold a
982    /// fetch mid-flight while it does something else to the `Remote`.
983    struct Parked {
984        started: std::sync::Arc<std::sync::Barrier>,
985        release: std::sync::Arc<std::sync::Barrier>,
986    }
987
988    impl RemoteSource for Parked {
989        fn fetch(&self) -> Result<Fetched, Error> {
990            self.started.wait();
991            self.release.wait();
992
993            Fake(r#"{"db": {"host": "stale"}}"#).fetch()
994        }
995
996        fn describe(&self) -> String {
997            "a parked store".to_owned()
998        }
999    }
1000
1001    /// The same, for the failing half of the fence: parked mid-fetch, and
1002    /// what it finally returns is an error.
1003    struct ParkedThenBroken {
1004        started: std::sync::Arc<std::sync::Barrier>,
1005        release: std::sync::Arc<std::sync::Barrier>,
1006    }
1007
1008    impl RemoteSource for ParkedThenBroken {
1009        fn fetch(&self) -> Result<Fetched, Error> {
1010            self.started.wait();
1011            self.release.wait();
1012
1013            Broken.fetch()
1014        }
1015
1016        fn describe(&self) -> String {
1017            "a parked store that then breaks".to_owned()
1018        }
1019    }
1020
1021    /// The race the generation fence exists for: a fetch from the *old*
1022    /// source lands after `set` installed a new one. Its result must be
1023    /// discarded — new source, old store's document is the state this
1024    /// module's docs promise cannot happen.
1025    #[test]
1026    fn a_fetch_from_a_replaced_source_is_discarded() {
1027        let started = std::sync::Arc::new(std::sync::Barrier::new(2));
1028        let release = std::sync::Arc::new(std::sync::Barrier::new(2));
1029
1030        let remote = std::sync::Arc::new(Remote::new());
1031        remote.set(Parked {
1032            started: std::sync::Arc::clone(&started),
1033            release: std::sync::Arc::clone(&release),
1034        });
1035
1036        let refresher = {
1037            let remote = std::sync::Arc::clone(&remote);
1038            std::thread::spawn(move || remote.refresh())
1039        };
1040
1041        // The fetch is provably in flight...
1042        started.wait();
1043
1044        // ...when the source is replaced.
1045        remote.set(Fake(r#"{"db": {"host": "fresh"}}"#));
1046
1047        release.wait();
1048        refresher
1049            .join()
1050            .expect("the refresher must not panic")
1051            .expect("the fetch itself succeeded");
1052
1053        assert_eq!(
1054            remote.document(),
1055            None,
1056            "the old source's document landed after the replacement and must \
1057             not be paired with the new source"
1058        );
1059
1060        // And the new source works normally.
1061        remote.refresh().unwrap();
1062        assert!(remote.document().unwrap().text.contains("fresh"));
1063    }
1064
1065    /// The same fence, from the other side: `clear()` is a state change too,
1066    /// so a fetch that was in flight when a caller cleared the slot must not
1067    /// put the document back. The barriers force the interleaving — the
1068    /// fetch is provably parked when `clear` runs — so this is a proof
1069    /// rather than a race the scheduler usually loses.
1070    #[test]
1071    fn a_fetch_in_flight_when_the_slot_is_cleared_is_discarded() {
1072        let started = std::sync::Arc::new(std::sync::Barrier::new(2));
1073        let release = std::sync::Arc::new(std::sync::Barrier::new(2));
1074
1075        let remote = std::sync::Arc::new(Remote::new());
1076        remote.set(Parked {
1077            started: std::sync::Arc::clone(&started),
1078            release: std::sync::Arc::clone(&release),
1079        });
1080
1081        let refresher = {
1082            let remote = std::sync::Arc::clone(&remote);
1083            std::thread::spawn(move || remote.refresh())
1084        };
1085
1086        started.wait();
1087
1088        remote.clear();
1089
1090        release.wait();
1091        refresher
1092            .join()
1093            .expect("the refresher must not panic")
1094            .expect("the fetch itself succeeded");
1095
1096        assert_eq!(
1097            remote.document(),
1098            None,
1099            "a document the caller cleared must not come back from a fetch \
1100             that started before they cleared it"
1101        );
1102    }
1103
1104    /// Clearing the document must not end a watch. The source is untouched
1105    /// by `clear()`, so a loop that took its sink before the call is still
1106    /// serving the store it was created for, and its next delivery installs
1107    /// like any other. The first shape of this fence counted both events on
1108    /// one number and made every live sink permanently stale.
1109    #[test]
1110    fn clearing_the_document_leaves_a_watchs_sink_alive() {
1111        let remote = Remote::new();
1112        remote.set(Fake(r#"{"db": {"host": "a"}}"#));
1113
1114        // What `remote_sink()` captures, once, where a loop starts.
1115        let generation = remote.generation();
1116
1117        remote.clear();
1118
1119        remote
1120            .install_if(generation, Fetched::new("{}", crate::Format::Json))
1121            .expect("clearing the document does not replace the source");
1122        assert!(remote.document().is_some());
1123    }
1124
1125    /// The status fence, from the side `set` opens: an old fetch that
1126    /// succeeds after its source was replaced must not report the
1127    /// replacement — which nothing has yet spoken to — as fetched and
1128    /// healthy. `set` empties the status precisely so that it says nothing
1129    /// about a store that is no longer installed.
1130    #[test]
1131    fn a_late_fetch_does_not_report_the_replacement_as_healthy() {
1132        let started = std::sync::Arc::new(std::sync::Barrier::new(2));
1133        let release = std::sync::Arc::new(std::sync::Barrier::new(2));
1134
1135        let remote = std::sync::Arc::new(Remote::new());
1136        remote.set(Parked {
1137            started: std::sync::Arc::clone(&started),
1138            release: std::sync::Arc::clone(&release),
1139        });
1140
1141        let refresher = {
1142            let remote = std::sync::Arc::clone(&remote);
1143            std::thread::spawn(move || remote.refresh())
1144        };
1145
1146        started.wait();
1147        remote.set(Fake(r#"{"db": {"host": "fresh"}}"#));
1148        release.wait();
1149
1150        let _ = refresher.join().expect("the refresher must not panic");
1151
1152        let status = remote.status();
1153        assert_eq!(
1154            status.fetches, 0,
1155            "the replacement has been fetched from nobody"
1156        );
1157        assert_eq!(status.last_fetch, None);
1158        assert_eq!(status.reachable(), None);
1159    }
1160
1161    /// The same fence for a failure. A store that was replaced while its
1162    /// fetch was erroring must not leave the new one looking down.
1163    #[test]
1164    fn a_late_failure_does_not_report_the_replacement_as_down() {
1165        let started = std::sync::Arc::new(std::sync::Barrier::new(2));
1166        let release = std::sync::Arc::new(std::sync::Barrier::new(2));
1167
1168        let remote = std::sync::Arc::new(Remote::new());
1169        remote.set(ParkedThenBroken {
1170            started: std::sync::Arc::clone(&started),
1171            release: std::sync::Arc::clone(&release),
1172        });
1173
1174        let refresher = {
1175            let remote = std::sync::Arc::clone(&remote);
1176            std::thread::spawn(move || remote.refresh())
1177        };
1178
1179        started.wait();
1180        remote.set(Fake(r#"{"db": {"host": "fresh"}}"#));
1181        release.wait();
1182
1183        let _ = refresher.join().expect("the refresher must not panic");
1184
1185        let status = remote.status();
1186        assert_eq!(status.consecutive_failures, 0);
1187        assert_eq!(
1188            status.reachable(),
1189            None,
1190            "nothing has yet asked the replacement anything"
1191        );
1192    }
1193
1194    /// Readers must not wait for a slow store: `document()` and `describe()`
1195    /// are on the `load()` path, and `load()` promises to touch no network.
1196    #[test]
1197    fn readers_are_not_blocked_by_a_fetch_in_flight() {
1198        let started = std::sync::Arc::new(std::sync::Barrier::new(2));
1199        let release = std::sync::Arc::new(std::sync::Barrier::new(2));
1200
1201        let remote = std::sync::Arc::new(Remote::new());
1202        remote.set(Parked {
1203            started: std::sync::Arc::clone(&started),
1204            release: std::sync::Arc::clone(&release),
1205        });
1206
1207        let refresher = {
1208            let remote = std::sync::Arc::clone(&remote);
1209            std::thread::spawn(move || remote.refresh())
1210        };
1211
1212        started.wait();
1213
1214        // With the fetch parked, a reader thread must finish promptly. The
1215        // old two-lock design held the source lock across the fetch, so
1216        // `describe()` — and with it every `load()` — waited out the store's
1217        // full timeout.
1218        let (sender, receiver) = std::sync::mpsc::channel();
1219        {
1220            let remote = std::sync::Arc::clone(&remote);
1221            std::thread::spawn(move || {
1222                let described = remote.describe();
1223                let document = remote.document();
1224                let _ = sender.send((described, document));
1225            });
1226        }
1227
1228        let (described, document) = receiver
1229            .recv_timeout(Duration::from_secs(2))
1230            .expect("readers must not wait for the network");
1231
1232        assert_eq!(described.as_deref(), Some("a parked store"));
1233        assert_eq!(document, None);
1234
1235        release.wait();
1236        let _ = refresher.join();
1237    }
1238
1239    /// `set` clears the document atomically with the source swap: no
1240    /// interleaving may observe the new source paired with any document.
1241    #[test]
1242    fn replacing_the_source_and_dropping_the_document_is_one_step() {
1243        let remote = Remote::new();
1244        remote.set(Fake(r#"{"db": {"host": "a"}}"#));
1245        remote.refresh().unwrap();
1246        let generation = remote.generation();
1247        remote
1248            .install_if(generation, Fetched::new("{}", crate::Format::Json))
1249            .expect("the source has not moved");
1250
1251        remote.set(Fake(r#"{"db": {"host": "b"}}"#));
1252
1253        assert_eq!(remote.document(), None);
1254
1255        // And the push-side fence itself: the pre-swap generation is now
1256        // stale, so a late delivery bounces instead of landing.
1257        remote
1258            .install_if(generation, Fetched::new("{}", crate::Format::Json))
1259            .expect_err("a replaced source's generation must be refused");
1260        assert_eq!(remote.document(), None);
1261    }
1262
1263    #[test]
1264    fn a_broken_store_reports_rather_than_pretending() {
1265        let remote = Remote::new();
1266        remote.set(Broken);
1267
1268        let error = remote.refresh().unwrap_err();
1269
1270        assert_eq!(error.kind(), ErrorKind::Remote);
1271        assert!(error.to_string().contains("unreachable"), "{error}");
1272    }
1273
1274    #[test]
1275    fn refreshing_with_no_source_says_so() {
1276        let error = Remote::new().refresh().unwrap_err();
1277
1278        assert!(error.to_string().contains("set_remote"), "{error}");
1279    }
1280
1281    #[test]
1282    fn replacing_the_source_drops_the_old_document() {
1283        let remote = Remote::new();
1284        remote.set(Fake(r#"{"db": {"host": "a"}}"#));
1285        remote.refresh().unwrap();
1286
1287        remote.set(Fake(r#"{"db": {"host": "b"}}"#));
1288
1289        assert!(
1290            remote.document().is_none(),
1291            "a new source answering with the old store's values would be a puzzle"
1292        );
1293    }
1294}
1295
1296/// A fenced door for a remote watch loop's pushes.
1297///
1298/// Created by the generated `remote_sink()` *after* the source is
1299/// installed, it remembers which source that was. [`apply`](Self::apply)
1300/// installs the document and reloads — unless the source has since been
1301/// replaced, in which case it refuses: a watch loop serving yesterday's
1302/// store cannot overwrite today's, by construction rather than by the old
1303/// documentation's request to please stop the loop first.
1304///
1305/// Cheap to clone; each wiring of a watch loop should take its own —
1306/// **once, where the loop starts**. A sink taken per delivery reads the
1307/// generation of that moment and fences nothing.
1308#[derive(Clone, Copy)]
1309pub struct RemoteSink {
1310    remote: &'static Remote,
1311    generation: u64,
1312    reload: fn() -> Result<(), Error>,
1313    name: &'static str,
1314}
1315
1316impl RemoteSink {
1317    /// Not public API: called by the generated `remote_sink()`.
1318    #[doc(hidden)]
1319    #[must_use]
1320    pub fn new(
1321        remote: &'static Remote,
1322        reload: fn() -> Result<(), Error>,
1323        name: &'static str,
1324    ) -> Self {
1325        Self {
1326            remote,
1327            generation: remote.generation(),
1328            reload,
1329            name,
1330        }
1331    }
1332
1333    /// How the fetches from the store behind this sink have gone.
1334    ///
1335    /// The door a `#[dynamic_config]` type has to its
1336    /// [`RemoteStatus`]: the slot itself is generated private, and a sink is
1337    /// the public handle on it — which is also where the question belongs,
1338    /// since a sink is what a watch loop holds.
1339    ///
1340    /// Taking a sink *only* to read this is fine and costs an atomic load:
1341    /// the generation a sink captures fences
1342    /// [`apply`](Self::apply) and nothing else. A loop that will deliver
1343    /// documents still takes its own, once, where it starts.
1344    ///
1345    /// ```no_run
1346    /// # struct DbConfig;
1347    /// # impl DbConfig {
1348    /// #     fn remote_sink() -> dynamic_config::RemoteSink { unimplemented!() }
1349    /// # }
1350    /// let status = DbConfig::remote_sink().status();
1351    ///
1352    /// if status.reachable() == Some(false) {
1353    ///     eprintln!("the store has stopped answering");
1354    /// }
1355    /// ```
1356    ///
1357    /// With the `telemetry` feature, `Exposition::add_remote` renders the
1358    /// same status as Prometheus text; see
1359    /// [the telemetry module](crate::telemetry). The example above stays
1360    /// feature-free on purpose, because this method is not.
1361    #[must_use]
1362    pub fn status(&self) -> RemoteStatus {
1363        self.remote.status()
1364    }
1365
1366    /// Reports an attempt to reach the store that came back with nothing.
1367    ///
1368    /// A watch loop is the half of a store this crate cannot see.
1369    /// [`apply`](Self::apply) records a delivery, so a *working* watch keeps
1370    /// [`RemoteStatus`] current — but a loop whose stream broke, whose
1371    /// blocking query is erroring or whose credential was refused delivers
1372    /// nothing, and would otherwise say nothing: `reachable` would report the
1373    /// last delivery rather than the last attempt, and a store that stopped
1374    /// answering an hour ago would look healthy until something called
1375    /// `refresh`.
1376    ///
1377    /// What it moves is deliberately narrow — the failure streak and the last
1378    /// failure, and nothing else. `fetches`, `last_fetch` and
1379    /// `last_fetch_duration` are left alone, so
1380    /// `dynamic_config_remote_last_fetch_seconds` keeps *ageing* while
1381    /// `dynamic_config_remote_up` goes to zero, which is the pair an alert
1382    /// wants. The stored document is untouched: a failed attempt is no reason
1383    /// to stop serving what the last good one produced.
1384    ///
1385    /// Fenced on the sink's generation exactly as [`apply`](Self::apply) is,
1386    /// so a loop still winding down after its source was replaced cannot
1387    /// charge its failures to the replacement. A stale report is dropped
1388    /// silently, and there is nothing to handle: a loop must never have to
1389    /// deal with a failure to report a failure.
1390    ///
1391    /// The error's kind and key path are recorded and nothing else — a
1392    /// store's address never enters a [`RemoteStatus`], for the reason its
1393    /// own documentation gives.
1394    pub fn failed(&self, error: &Error) {
1395        // The fence is inside `record_fetch_failure`, under the same lock
1396        // that reads the generation: a check here and a write there would
1397        // leave a window for a replacement to land between them.
1398        self.remote.record_fetch_failure(error, self.generation);
1399    }
1400
1401    /// Installs a document the watch pushed, and reloads.
1402    ///
1403    /// Everything a file change would do happens here too — validation,
1404    /// the reload hooks, the cache — because it is the same code path,
1405    /// reached with a document instead of a filesystem event. A failure
1406    /// leaves the previous snapshot serving.
1407    ///
1408    /// # Errors
1409    ///
1410    /// If the source has been replaced since this sink was created —
1411    /// checked before the reload *and again after it*, because a
1412    /// replacement can land while the reload runs — or if the resulting
1413    /// configuration does not load or validate.
1414    pub fn apply(&self, document: Fetched) -> Result<(), Error> {
1415        self.remote.install_if(self.generation, document)?;
1416
1417        let outcome = (self.reload)();
1418
1419        // The reload read the slot as it stood while it ran. If the source
1420        // was replaced mid-flight — after `install_if` said yes — what just
1421        // installed may derive from this sink's document even though the
1422        // fence now belongs to the replacement. Reload once more against
1423        // the slot as it stands, so the replacement's state has the last
1424        // word, then refuse like any other stale push.
1425        if self.remote.generation() != self.generation {
1426            let _ = (self.reload)();
1427
1428            let error = Error::new(
1429                crate::ErrorKind::Backend,
1430                "the remote source this sink was created for was replaced \
1431                 while its delivery reloaded; the replacement's state was \
1432                 restored — stop the old watch loop and take a fresh sink \
1433                 from `remote_sink()`",
1434            );
1435            crate::__log_remote_failure(self.name, &error);
1436
1437            return Err(error);
1438        }
1439
1440        match outcome {
1441            Ok(()) => {
1442                crate::__log_remote_reload(self.name, None);
1443
1444                Ok(())
1445            }
1446            Err(error) => {
1447                crate::__log_remote_failure(self.name, &error);
1448
1449                Err(error)
1450            }
1451        }
1452    }
1453}
1454
1455impl std::fmt::Debug for RemoteSink {
1456    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1457        f.debug_struct("RemoteSink")
1458            .field("config", &self.name)
1459            .field("generation", &self.generation)
1460            .finish_non_exhaustive()
1461    }
1462}