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}