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