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}