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}