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