Skip to main content

dynamic_config/watch/
mod.rs

1//! The filesystem watcher behind hot reload.
2//!
3//! One concern per file: this module holds what a watcher is pointed at
4//! ([`Watched`]) and how it detects changes ([`WatchMode`]); `handle.rs`
5//! starts and stops one — the one-watcher-per-type registry, the spawn,
6//! the [`WatchHandle`] that owns the backend; `debounce.rs` is the
7//! background loop that waits out an editor's flurry before reloading;
8//! `relevance.rs` decides which events are about our files at all,
9//! Kubernetes `..data` swaps included.
10
11mod debounce;
12mod handle;
13mod options;
14mod relevance;
15#[cfg(test)]
16mod tests;
17
18pub use debounce::set_atomic_save_grace;
19pub use handle::{spawn, spawn_with, WatchHandle, WatchKey};
20pub use options::WatchOptions;
21
22use std::path::PathBuf;
23use std::time::Duration;
24
25use crate::discovery;
26use crate::source::LoadSpec;
27
28/// What a watcher looks at, owned.
29///
30/// The watch used to borrow a `LoadSpec<'static>`, which chained every
31/// watcher to statics only the attribute can produce. Owning the three
32/// facts the watch actually uses — the explicit file paths, the discovery
33/// name, the searched directories — frees the builder (or anything else)
34/// to start one from runtime data.
35#[derive(Debug, Clone)]
36pub struct Watched {
37    files: Vec<PathBuf>,
38    search_name: Option<String>,
39    search_directories: Vec<PathBuf>,
40}
41
42impl Watched {
43    /// Captures what a watcher needs from `spec`, with any lifetime.
44    ///
45    /// The searched directories are resolved here, once — the same moment
46    /// the directory watches are registered, so the two cannot disagree.
47    #[must_use]
48    pub fn from_spec(spec: &LoadSpec<'_>) -> Self {
49        Self {
50            files: spec
51                .sources
52                .iter()
53                .filter_map(|source| source.path())
54                .map(PathBuf::from)
55                .collect(),
56            search_name: spec.search.as_ref().map(|search| search.name.to_owned()),
57            search_directories: spec
58                .search
59                .as_ref()
60                .map(|search| discovery::search_directories(search))
61                .unwrap_or_default(),
62        }
63    }
64}
65
66/// How to detect changes.
67///
68/// The native backend is right almost everywhere and wrong in one important
69/// place: inotify and its equivalents do not fire on many network and overlay
70/// filesystems — NFS, some Docker bind mounts, some CI runners. The failure is
71/// silent, because the watch registers successfully and simply never delivers
72/// anything, so there is nothing to detect and fall back from. It has to be
73/// chosen deliberately.
74#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
75#[non_exhaustive]
76pub enum WatchMode {
77    /// The platform's notification backend. Efficient, and the default.
78    #[default]
79    Native,
80    /// Re-read the files on an interval. Works anywhere, at the cost of the
81    /// interval's worth of latency and a periodic wake-up.
82    ///
83    /// Each tick compares **contents**, not only timestamps. A filesystem
84    /// timestamp is compared here in whole seconds, so an edit landing in the
85    /// same second as the previous scan would otherwise be invisible — and
86    /// stay invisible, because the next scan compares against the value it
87    /// just recorded. Configuration files are small and few, and a watcher
88    /// that misses edits is the failure polling was chosen to escape.
89    Poll {
90        /// How often to look.
91        interval: Duration,
92    },
93}