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