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}