Skip to main content

turbo_tasks_fs/watcher/
mod.rs

1mod batch_schedule;
2mod fs_api;
3#[cfg(test)]
4mod mock_fs_api;
5
6use std::{
7    any::Any,
8    borrow::Cow,
9    collections::BTreeSet,
10    env, fmt,
11    path::{Path, PathBuf},
12    sync::{
13        Arc, LazyLock,
14        mpsc::{Receiver, RecvTimeoutError, channel},
15    },
16    time::Duration,
17};
18
19use anyhow::{Context, Result};
20use bincode::{
21    Decode, Encode,
22    de::Decoder,
23    enc::Encoder,
24    error::{DecodeError, EncodeError},
25};
26use bitflags::bitflags;
27use indexmap::map::{RawEntryApiV1, raw_entry_v1::RawEntryMut};
28use notify::{
29    Config, EventKind, PollWatcher, RecommendedWatcher, Watcher,
30    event::{MetadataKind, ModifyKind, RenameMode},
31};
32use rustc_hash::FxHashSet;
33use tokio::sync::{RwLock, RwLockWriteGuard};
34use tracing::instrument;
35use turbo_rcstr::RcStr;
36use turbo_tasks::{
37    FxIndexMap, FxIndexSet, InvalidationReason, InvalidationReasonKind, Invalidator, ResolvedVc,
38    TraitRef, TurboTasksApi, spawn_thread, util::StaticOrArc,
39};
40
41use crate::{
42    format_absolute_fs_path,
43    invalidation::{WatchChange, WatchStart},
44    invalidator_map::InvalidatorMap,
45    path_map::OrderedPathMapExt,
46    watcher::{batch_schedule::BatchSchedule, fs_api::DiskFileSystemWatcherApi},
47};
48
49/// Overrides [`DiskWatcherConfig::recursive_mode`]. Users shouldn't need to set this, this is
50/// intended only for debugging purposes.
51static FORCED_WATCH_RECURSIVE_MODE: LazyLock<Option<DiskWatcherRecursiveMode>> = LazyLock::new(
52    || match env::var("TURBO_TASKS_FORCE_WATCH_MODE").as_deref() {
53        Ok("recursive") => Some(DiskWatcherRecursiveMode::Recursive),
54        Ok("nonrecursive") => Some(DiskWatcherRecursiveMode::NonRecursive),
55        Ok(_) => {
56            eprintln!(
57                "unsupported `TURBO_TASKS_FORCE_WATCH_MODE`, must be `recursive` or `nonrecursive`"
58            );
59            None
60        }
61        _ => None,
62    },
63);
64
65#[turbo_tasks::task_input]
66#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Encode, Decode)]
67pub struct DiskWatcherConfig {
68    /// Whether to let the [`notify::Watcher`] recurse into subdirectories itself, or to track and
69    /// watch each directory we care about ourselves.
70    ///
71    /// [`None`] picks a default based on the platform and [`Self::poll_interval`], which is
72    /// normally what you want.
73    ///
74    /// The `TURBO_TASKS_FORCE_WATCH_MODE` environment variable will override this configuration
75    /// value (intended for debugging purposes).
76    pub recursive_mode: Option<DiskWatcherRecursiveMode>,
77    /// Poll the filesystem at this interval instead of using the platform's native file watching,
78    /// for cases where native watching doesn't work (e.g. some Docker setups). This is slow and
79    /// inefficient, it should only be used as a last resort.
80    ///
81    /// [`None`] disables polling, using the platform's native watcher ([`RecommendedWatcher`])
82    /// instead of [`PollWatcher`].
83    pub poll_interval: Option<Duration>,
84    /// Attach an [`InvalidationReason`] to every invalidation the watcher causes ([`WatchStart`],
85    /// [`WatchChange`], or [`InvalidateRescan`]), so that it can be reported to the user.
86    ///
87    /// This costs an extra allocation per invalidated path, so it's only worth enabling when
88    /// something actually consumes the reasons.
89    pub report_invalidation_reason: bool,
90
91    /// How long to keep a batch of filesystem events open, waiting for more events, before
92    /// flushing invalidations. Batching coalesces bursts (e.g. a `git checkout`) into a single
93    /// invalidation pass and avoids reading half-written files.
94    ///
95    /// If set too low (<10ms), this is known to cause partial file reads on Linux where `inotify`
96    /// has very low latency.
97    pub batch_delay: Duration,
98    /// When [`DiskWatcherPathMatcher::match_path`] returns `true`, we will extend the batch by
99    /// [`Self::extended_batch_delay_duration`].
100    pub extended_batch_delay_matcher: Option<ResolvedVc<Box<dyn DiskWatcherPathMatcher>>>,
101    /// The idle period required to close a batch once [`Self::extended_batch_delay_matcher`] has
102    /// matched. Unused when there is no matcher.
103    pub extended_batch_delay_duration: Duration,
104
105    /// If a single batch stays open at least this long, emit a `FilesystemSettlingEvent`
106    /// compilation event so the user knows why work has stalled. Repeated events within the same
107    /// batch back off exponentially, up to [`Self::settling_event_max_delay`].
108    pub settling_event_initial_delay: Duration,
109    /// Upper bound for the exponentially increasing interval between repeated
110    /// `FilesystemSettlingEvent`s within a single batch.
111    pub settling_event_max_delay: Duration,
112}
113
114impl Default for DiskWatcherConfig {
115    fn default() -> Self {
116        Self {
117            recursive_mode: None,
118            poll_interval: None,
119            report_invalidation_reason: false,
120            batch_delay: Duration::from_millis(10),
121            extended_batch_delay_matcher: None,
122            extended_batch_delay_duration: Duration::from_millis(200),
123            settling_event_initial_delay: Duration::from_millis(500),
124            settling_event_max_delay: Duration::from_secs(60),
125        }
126    }
127}
128
129/// Matches absolute paths reported by the filesystem watcher. See
130/// [`DiskWatcherConfig::extended_batch_delay_matcher`].
131#[turbo_tasks::value_trait]
132pub trait DiskWatcherPathMatcher {
133    /// Called on the watcher thread once per path of every incoming event, so this should be
134    /// cheap and must not block.
135    fn match_path(&self, path: &Path) -> bool;
136}
137
138/// Equivalent to [`notify::RecursiveMode`], but implements traits needed by [`turbo_tasks`].
139///
140/// When using [`Self::Recursive`], [`notify::Watcher`] will recursively track all contents
141/// of the filesystem root. This should only be used on platforms with efficient recursive watcher
142/// implementations (i.e. macOS and Windows).
143///
144/// When using [`Self::NonRecursive`], we only track previously read files and their parent
145/// directories.
146#[turbo_tasks::task_input]
147#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Encode, Decode)]
148pub enum DiskWatcherRecursiveMode {
149    Recursive,
150    NonRecursive,
151}
152
153impl From<DiskWatcherRecursiveMode> for notify::RecursiveMode {
154    fn from(value: DiskWatcherRecursiveMode) -> Self {
155        match value {
156            DiskWatcherRecursiveMode::Recursive => notify::RecursiveMode::Recursive,
157            DiskWatcherRecursiveMode::NonRecursive => notify::RecursiveMode::NonRecursive,
158        }
159    }
160}
161
162impl DiskWatcherConfig {
163    /// Resolves [`Self::recursive_mode`], falling back to a default based on
164    /// [`Self::poll_interval`] and the platform.
165    fn resolve_recursive_mode(&self) -> DiskWatcherRecursiveMode {
166        // macOS and Windows have efficient recursive watchers, so it's best to track the entire
167        // directory and filter events to the files we care about. inotify on Linux is
168        // non-recursive, so notify-rs's implementation is inefficient; better for us to track it
169        // ourselves and only watch the directories we know we care about.
170        //
171        // See: <https://github.com/vercel/turborepo/pull/4100>
172        let platform_has_efficient_recursive_watcher =
173            cfg!(any(target_os = "macos", target_os = "windows"));
174
175        // the env var is a debugging escape hatch, so it wins over everything else
176        if let Some(forced) = *FORCED_WATCH_RECURSIVE_MODE {
177            forced
178        } else if let Some(recursive_mode) = self.recursive_mode {
179            recursive_mode
180        } else if self.poll_interval.is_some() {
181            // `PollWatcher` implements recursive watching by walking the entire subtree on every
182            // poll, so watching the fs root recursively would stat every file in the project each
183            // interval. Watching non-recursively keeps each poll to the directories we've read.
184            DiskWatcherRecursiveMode::NonRecursive
185        } else if platform_has_efficient_recursive_watcher {
186            DiskWatcherRecursiveMode::Recursive
187        } else {
188            DiskWatcherRecursiveMode::NonRecursive
189        }
190    }
191}
192
193pub(crate) struct DiskWatcher {
194    state: State,
195    config: DiskWatcherConfig,
196}
197
198/// Only [`Self::config`] is serialized: a decoded [`DiskWatcher`] is always stopped.
199impl Encode for DiskWatcher {
200    fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
201        self.config.encode(encoder)
202    }
203}
204
205impl<Ctx> Decode<Ctx> for DiskWatcher {
206    fn decode<D: Decoder<Context = Ctx>>(decoder: &mut D) -> Result<Self, DecodeError> {
207        Ok(Self::new(DiskWatcherConfig::decode(decoder)?))
208    }
209}
210bincode::impl_borrow_decode!(DiskWatcher);
211
212enum State {
213    // Note: Information about if we're a recursive or non-recursive watcher must live outside the
214    // `RwLock` to allow us to quickly bail out before calling functions in
215    // `non_recursive_helpers`.
216    Recursive(RwLock<RecursiveState>),
217    NonRecursive(RwLock<NonRecursiveState>),
218}
219
220enum StateWriteGuard<'a> {
221    Recursive(RwLockWriteGuard<'a, RecursiveState>),
222    NonRecursive(RwLockWriteGuard<'a, NonRecursiveState>),
223}
224
225impl State {
226    fn new_stopped(recursive_mode: DiskWatcherRecursiveMode) -> Self {
227        match recursive_mode {
228            DiskWatcherRecursiveMode::Recursive => {
229                Self::Recursive(RwLock::new(RecursiveState::Stopped))
230            }
231            DiskWatcherRecursiveMode::NonRecursive => {
232                Self::NonRecursive(RwLock::new(NonRecursiveState::Stopped))
233            }
234        }
235    }
236
237    async fn write(&self) -> StateWriteGuard<'_> {
238        match self {
239            Self::Recursive(state) => StateWriteGuard::Recursive(state.write().await),
240            Self::NonRecursive(state) => StateWriteGuard::NonRecursive(state.write().await),
241        }
242    }
243
244    fn recursive_mode(&self) -> DiskWatcherRecursiveMode {
245        match self {
246            Self::Recursive(_) => DiskWatcherRecursiveMode::Recursive,
247            Self::NonRecursive(_) => DiskWatcherRecursiveMode::NonRecursive,
248        }
249    }
250}
251
252/// Used when [`DiskWatcherConfig::recursive_mode`] returns [`RecursiveMode::Recursive`] (default on
253/// macOS and Windows when not polling).
254enum RecursiveState {
255    /// Used when [`DiskWatcher::start_watching`] hasn't been called yet or after
256    /// [`DiskWatcher::stop_watching`] is called.
257    Stopped,
258    Watching {
259        /// Hold onto the watcher: When this is dropped, it will cause the channel to disconnect
260        _notify_watcher: NotifyWatcher,
261    },
262}
263
264/// Used when [`DiskWatcherConfig::recursive_mode`] returns [`RecursiveMode::NonRecursive`] (default
265/// on Linux, and everywhere when polling).
266enum NonRecursiveState {
267    /// Used when [`DiskWatcher::start_watching`] hasn't been called yet or after
268    /// [`DiskWatcher::stop_watching`] is called.
269    Stopped,
270    Watching(NonRecursiveWatchingState),
271}
272
273// split out from the `NonRecursiveState` enum because we want to pass this value around
274struct NonRecursiveWatchingState {
275    notify_watcher: NotifyWatcher,
276    /// Keeps track of which directories are currently or were previously watched by
277    /// [`Self::notify_watcher`].
278    ///
279    /// Invariants:
280    /// - Never contains `root_path`. A watcher for `root_path` is implicitly set up during
281    ///   [`DiskWatcher::start_watching`].
282    /// - Contains all parent directories up to `root_path` for every entry.
283    watched: BTreeSet<PathBuf>,
284}
285
286/// A thin wrapper around [`RecommendedWatcher`] and [`PollWatcher`].
287enum NotifyWatcher {
288    Recommended(RecommendedWatcher),
289    Polling(PollWatcher),
290}
291
292impl NotifyWatcher {
293    fn watch(&mut self, path: &Path, recursive_mode: notify::RecursiveMode) -> notify::Result<()> {
294        match self {
295            Self::Recommended(watcher) => watcher.watch(path, recursive_mode),
296            Self::Polling(watcher) => watcher.watch(path, recursive_mode),
297        }
298    }
299}
300
301mod non_recursive_helpers {
302    use super::*;
303    use crate::path_map::OrderedPathSetExt;
304
305    /// Called after a rescan in case a previously watched-but-deleted directory was recreated.
306    #[instrument(skip_all, level = "trace")]
307    pub async fn restore_all_watched_ignore_errors(
308        state: &RwLock<NonRecursiveState>,
309        root_path: &Path,
310    ) {
311        let mut guard = state.write().await;
312        let NonRecursiveState::Watching(watching_state) = &mut *guard else {
313            return;
314        };
315        for dir_path in watching_state.watched.iter() {
316            // TODO: Report diagnostics if this error happens
317            //
318            // Don't watch the parents, because those are already included in `self.watched` (so
319            // it'd be redundant), but also because this could deadlock, since we'd try to modify
320            // `self.watched` while iterating over it (write lock overlapping with a read lock).
321            let _ = start_watching_dir(&mut watching_state.notify_watcher, dir_path, root_path);
322        }
323    }
324
325    /// Called when a new directory is found in a parent directory we're watching. Restores the
326    /// watcher if we were previously watching it.
327    #[instrument(skip_all, level = "trace")]
328    pub async fn restore_if_watched(
329        state: &RwLock<NonRecursiveState>,
330        dir_path: &Path,
331        root_path: &Path,
332    ) -> Result<()> {
333        // fast path: The root directory is always implicitly watched during
334        // `DiskWatcher::start_watching`, we assume it is never deleted and never needs to be
335        // restored.
336        if dir_path == root_path {
337            return Ok(());
338        }
339
340        // fast path: the directory isn't in `watched`, only take a read lock and bail out early
341        {
342            let guard = state.read().await;
343            let NonRecursiveState::Watching(watching_state) = &*guard else {
344                return Ok(());
345            };
346            if !watching_state.watched.contains(dir_path) {
347                return Ok(());
348            }
349        }
350
351        // slow path: re-watch the path
352        let mut guard = state.write().await;
353        let NonRecursiveState::Watching(watching_state) = &mut *guard else {
354            return Ok(());
355        };
356
357        // watch the new directory
358        start_watching_dir(&mut watching_state.notify_watcher, dir_path, root_path)?;
359
360        // Also try to restore any watchers for children of this directory
361        for child_path in watching_state.watched.iter_path_children(dir_path) {
362            // Don't watch the parents -- see the comment on `restore_all_watched`
363            start_watching_dir(&mut watching_state.notify_watcher, child_path, root_path)?;
364        }
365        Ok(())
366    }
367
368    /// Called when a file in `dir_path` or `dir_path` itself is read or written. Adds a new watcher
369    /// if we're not already watching the directory.
370    ///
371    /// This should be called *before* reading a file to avoid a race condition.
372    #[instrument(skip_all, level = "trace")]
373    pub async fn ensure_watched(
374        state: &RwLock<NonRecursiveState>,
375        dir_path: &Path,
376        root_path: &Path,
377    ) -> Result<()> {
378        // fast path: The root directory is always implicitly watched during
379        // `DiskWatcher::start_watching`.
380        if dir_path == root_path {
381            return Ok(());
382        }
383
384        // fast path: the directory is already in `watched`, only take a read lock and bail out
385        // early
386        {
387            let guard = state.read().await;
388            let NonRecursiveState::Watching(watching_state) = &*guard else {
389                return Ok(());
390            };
391            if watching_state.watched.contains(dir_path) {
392                return Ok(());
393            }
394        }
395
396        // slow path: watch the path
397        let mut guard = state.write().await;
398        let NonRecursiveState::Watching(watching_state) = &mut *guard else {
399            return Ok(());
400        };
401        if watching_state.watched.insert(dir_path.to_path_buf()) {
402            start_watching_dir_and_parents(watching_state, dir_path, root_path)?;
403        }
404        Ok(())
405    }
406
407    /// Private helper, assumes that `dir_path` has already been added to
408    /// [`NonRecursiveWatchingState::watched`].
409    ///
410    /// This does not watch any of the parent directories. For that, use
411    /// [`start_watching_dir_and_parents`]. Use this method when iterating over previously-watched
412    /// values in `self.watching`.
413    fn start_watching_dir(
414        notify_watcher: &mut NotifyWatcher,
415        dir_path: &Path,
416        root_path: &Path,
417    ) -> Result<()> {
418        debug_assert_ne!(dir_path, root_path);
419
420        match notify_watcher.watch(dir_path, notify::RecursiveMode::NonRecursive) {
421            Ok(())
422            | Err(notify::Error {
423                // The path was probably deleted before we could process the event, but the parent
424                // should still be watched. The codepaths that care about this either call
425                // `start_watching_dir_and_parents` or handle the parents themselves.
426                kind: notify::ErrorKind::PathNotFound,
427                ..
428            }) => Ok(()),
429            Err(err) => {
430                // ast-grep-ignore: no-context-format
431                return Err(err).context(format!("Unable to watch {}", dir_path.display(),));
432            }
433        }
434    }
435
436    /// Private helper, assumes that `dir_path` has already been added to
437    /// [`NonRecursiveWatchingState::watched`].
438    ///
439    /// Watches the given `dir_path` and every parent up to `root_path`. Parents must be recursively
440    /// watched in case any of them change:
441    /// https://docs.rs/notify/latest/notify/#parent-folder-deletion
442    fn start_watching_dir_and_parents(
443        state: &mut NonRecursiveWatchingState,
444        dir_path: &Path,
445        root_path: &Path,
446    ) -> Result<()> {
447        let mut found_watched_ancestor = false;
448
449        // NOTE: `Path::ancestors` yields ancestors from longest to shortest path.
450        let dir_and_ancestor_paths: Vec<_> = [dir_path]
451            .into_iter()
452            .chain(
453                dir_path
454                    .ancestors()
455                    // skip: `ancestors` includes `dir_path` itself, as well as the ancestors, but
456                    // we only want to apply the `take_while` check to parents
457                    .skip(1)
458                    .take_while(|p| {
459                        found_watched_ancestor = *p == root_path || state.watched.contains(*p);
460                        !found_watched_ancestor
461                    }),
462            )
463            .collect();
464
465        if !found_watched_ancestor {
466            // this should never happen, as we should eventually hit the `root_path`
467            anyhow::bail!(
468                "failed to find the fs root of {root_path:?} while watching {dir_path:?}"
469            );
470        }
471
472        // Reverse the iterator: We want to start closest to the root and work towards `dir_path`
473        // (opposite of `Path::ancestors`), to avoid a potential race condition if directories are
474        // removed and re-added before we've watched their parent.
475        for path in dir_and_ancestor_paths.into_iter().rev() {
476            // this will silently ignore if the path is not found, expecting that we've watched the
477            // parent directory
478            start_watching_dir(&mut state.notify_watcher, path, root_path)?;
479            state.watched.insert(path.to_owned());
480        }
481
482        Ok(())
483    }
484}
485
486impl DiskWatcher {
487    pub fn new(config: DiskWatcherConfig) -> Self {
488        assert!(
489            config.extended_batch_delay_duration >= config.batch_delay,
490            "extended_batch_delay_duration must be at least batch_delay"
491        );
492        Self {
493            state: State::new_stopped(config.resolve_recursive_mode()),
494            config,
495        }
496    }
497
498    pub async fn start_watching<FsApi: DiskFileSystemWatcherApi>(fs: Arc<FsApi>) -> Result<()> {
499        let watcher: &Self = fs.watcher();
500
501        // read in the turbo-task context and before acquiring the lock
502        let extended_batch_delay_matcher = match watcher.config.extended_batch_delay_matcher {
503            Some(matcher) => Some(matcher.into_trait_ref().await?),
504            None => None,
505        };
506
507        let state_guard = watcher.state.write().await;
508
509        // bail out if we're already watching
510        if let StateWriteGuard::Recursive(guard) = &state_guard
511            && matches!(**guard, RecursiveState::Watching { .. })
512        {
513            return Ok(());
514        } else if let StateWriteGuard::NonRecursive(guard) = &state_guard
515            && matches!(**guard, NonRecursiveState::Watching(..))
516        {
517            return Ok(());
518        }
519
520        // Create a channel to receive the events.
521        let (tx, rx) = channel();
522        // Create a watcher object, delivering debounced events.
523        // The notification back-end is selected based on the platform.
524        let config = Config::default();
525        // we should track and invalidate each part of a symlink chain ourselves in
526        // turbo-tasks-fs
527        let config = config.with_follow_symlinks(false);
528
529        let mut notify_watcher = if let Some(poll_interval) = watcher.config.poll_interval {
530            let config = config.with_poll_interval(poll_interval);
531            NotifyWatcher::Polling(PollWatcher::new(tx, config)?)
532        } else {
533            NotifyWatcher::Recommended(RecommendedWatcher::new(tx, config)?)
534        };
535
536        // TOCTOU: we must watch `root_path` before calling any invalidators and setting up the
537        // watchers in their associated functions
538        let root_path = fs.root_path();
539        notify_watcher.watch(
540            root_path,
541            notify::RecursiveMode::from(watcher.state.recursive_mode()),
542        )?;
543
544        // We need to invalidate all reads or writes that happened before watching. As a
545        // side-effect, this will call `ensure_watched` again, setting up any watchers needed.
546        //
547        // Best is to start_watching before starting to read
548        if watcher.config.report_invalidation_reason {
549            let name = fs.name().clone();
550            fs.invalidate_all_with_reason(|path| WatchStart {
551                name: name.clone(),
552                // this path is just used for display purposes
553                path: RcStr::from(path.to_string_lossy()),
554            });
555        } else {
556            fs.invalidate_all();
557        }
558
559        spawn_thread({
560            let fs = fs.clone();
561            move || Self::watch_thread(fs, rx, extended_batch_delay_matcher)
562        });
563
564        // Updating `self.state` is done last. If we panic while setting up the watcher, it'll
565        // stay in the `Stopped` state.
566        match state_guard {
567            StateWriteGuard::Recursive(mut recursive) => {
568                *recursive = RecursiveState::Watching {
569                    _notify_watcher: notify_watcher,
570                }
571            }
572            StateWriteGuard::NonRecursive(mut non_recursive) => {
573                *non_recursive = NonRecursiveState::Watching(NonRecursiveWatchingState {
574                    notify_watcher,
575                    watched: BTreeSet::new(),
576                })
577            }
578        };
579
580        Ok(())
581    }
582
583    pub async fn stop_watching(&self) {
584        match &self.state {
585            State::Recursive(state) => *state.write().await = RecursiveState::Stopped,
586            State::NonRecursive(state) => *state.write().await = NonRecursiveState::Stopped,
587        }
588        // thread will detect the stop because the channel is disconnected when `NotifyWatcher` is
589        // dropped
590    }
591
592    /// Internal thread that processes the events from the watcher
593    /// and invalidates the cache.
594    ///
595    /// Should only be called once from `start_watching`.
596    fn watch_thread<FsApi: DiskFileSystemWatcherApi>(
597        fs: Arc<FsApi>,
598        rx: Receiver<notify::Result<notify::Event>>,
599        extended_batch_delay_matcher: Option<TraitRef<Box<dyn DiskWatcherPathMatcher>>>,
600    ) {
601        let watcher: &Self = fs.watcher();
602        let config = &watcher.config;
603        let report_invalidation_reason = config.report_invalidation_reason;
604        let mut batch = BatchedInvalidations::new(
605            watcher.state.recursive_mode(),
606            config.poll_interval.is_some(),
607        );
608        let mut schedule = BatchSchedule::new(config);
609
610        'outer: loop {
611            loop {
612                match schedule.recv_event(&rx, &*fs, &batch) {
613                    Ok(Ok(event)) => {
614                        // TODO: We might benefit from some user-facing diagnostics if it rescans
615                        // occur frequently (i.e. more than X times in Y minutes)
616                        //
617                        // You can test rescans on Linux by reducing the inotify queue to something
618                        // really small:
619                        //
620                        // ```
621                        // echo 3 | sudo tee /proc/sys/fs/inotify/max_queued_events
622                        // ```
623                        if event.need_rescan() {
624                            let _lock = fs.invalidation_lock().blocking_write();
625
626                            // flush the whole mpsc queue, we're about to rescan, we don't need to
627                            // process any other update events that have already happened
628                            while rx.try_recv().is_ok() {}
629
630                            if let State::NonRecursive(non_recursive) = &watcher.state {
631                                // we can't narrow this down to a smaller set of paths: Rescan
632                                // events (at least when tested on
633                                // Linux) come with no `paths`, and we use
634                                // only one global `notify::Watcher` instance.
635                                //
636                                // TODO: Report diagnostics if an error happens
637                                fs.tokio_handle().block_on(
638                                    non_recursive_helpers::restore_all_watched_ignore_errors(
639                                        non_recursive,
640                                        fs.root_path(),
641                                    ),
642                                );
643                            }
644
645                            if report_invalidation_reason {
646                                fs.invalidate_all_with_reason(|path| InvalidateRescan {
647                                    // this path is just used for display purposes
648                                    path: RcStr::from(path.to_string_lossy()),
649                                });
650                            } else {
651                                fs.invalidate_all();
652                            }
653
654                            // no need to process the rest of the batch as we just
655                            // invalidated everything
656                            batch.clear();
657                            schedule.reset();
658                            break;
659                        }
660
661                        // Any event that contributes to the batch keeps it open for another
662                        // `batch_delay`. A path matching `extended_batch_delay_matcher` (e.g. a
663                        // package-manager install target) keeps it open for
664                        // `extended_batch_delay_duration` instead.
665                        let mut delay = config.batch_delay;
666                        if let Some(matcher) = &extended_batch_delay_matcher
667                            && event.paths.iter().any(|path| matcher.match_path(path))
668                        {
669                            delay = delay.max(config.extended_batch_delay_duration);
670                        }
671
672                        if batch.add_event(event) {
673                            schedule.extend(delay);
674                        }
675                    }
676                    // Error raised by notify watcher itself
677                    Ok(Err(notify::Error { kind, paths })) => {
678                        println!("watch error ({paths:?}): {kind:?} ");
679
680                        batch.add_error(paths, fs.root_path());
681                        schedule.extend(config.batch_delay);
682                    }
683                    Err(RecvTimeoutError::Timeout) => {
684                        // the batch is complete: break out to invalidate the collected paths.
685                        break;
686                    }
687                    Err(RecvTimeoutError::Disconnected) => {
688                        // Sender has been disconnected, which means DiskFileSystem has been dropped
689                        // exit thread
690                        break 'outer;
691                    }
692                }
693            }
694
695            // We need to start watching first before invalidating the changed paths...
696            // This is only needed on platforms we don't do recursive watching on.
697            if let State::NonRecursive(non_recursive) = &watcher.state {
698                for path in batch.new_paths() {
699                    // TODO: Report diagnostics if this error happens
700                    let _ = fs
701                        .tokio_handle()
702                        .block_on(non_recursive_helpers::restore_if_watched(
703                            non_recursive,
704                            path,
705                            fs.root_path(),
706                        ));
707                }
708            }
709
710            let Some(turbo_tasks) = fs.turbo_tasks() else {
711                // TurboTasks was dropped, stop watching
712                break 'outer;
713            };
714            let _guard = fs.tokio_handle().enter();
715
716            let _lock = fs.invalidation_lock().blocking_write();
717            batch.execute(
718                fs.invalidator_map(),
719                fs.dir_invalidator_map(),
720                |invalidation_reason_path, invalidator| {
721                    invalidate(
722                        &*fs,
723                        &*turbo_tasks,
724                        report_invalidation_reason,
725                        invalidation_reason_path,
726                        invalidator,
727                    )
728                },
729            );
730        }
731    }
732
733    pub async fn ensure_watched_file(&self, path: &Path, root_path: &Path) -> Result<()> {
734        // Watch the parent directory instead of the specified file, since directories also track
735        // their immediate children (even in non-recursive mode), and we need to watch all the
736        // parents anyways.
737        if let State::NonRecursive(non_recursive) = &self.state
738            && let Some(dir_path) = path.parent()
739        {
740            non_recursive_helpers::ensure_watched(non_recursive, dir_path, root_path).await?;
741        }
742        Ok(())
743    }
744
745    pub async fn ensure_watched_dir(&self, dir_path: &Path, root_path: &Path) -> Result<()> {
746        if let State::NonRecursive(non_recursive) = &self.state {
747            non_recursive_helpers::ensure_watched(non_recursive, dir_path, root_path).await?;
748        }
749        Ok(())
750    }
751}
752
753bitflags! {
754    /// Describes how a single path in a [`BatchedInvalidations`] should be invalidated. A path may
755    /// carry any combination of these (accumulated across the events in a batch).
756    struct InvalidationFlags: u8 {
757        /// Invalidate exactly this path in the file-content invalidator map.
758        const PATH = 1 << 0;
759        /// Invalidate exactly this path in the directory-listing invalidator map.
760        const PATH_DIR = 1 << 1;
761        /// Invalidate this path and all of its children in the file-content invalidator map.
762        const PATH_AND_CHILDREN = 1 << 2;
763        /// Invalidate this path and all of its children in the directory-listing invalidator map.
764        const PATH_AND_CHILDREN_DIR = 1 << 3;
765    }
766}
767
768/// A set of deferred invalidations. Because one or more files may be updated many times in quick
769/// succession, we don't want to perform invalidations until we think the filesystem has settled.
770///
771/// This avoids reading partially-written files which might generate transient errors, and reduces
772/// CPU and memory usage by producing less wasted work.
773///
774/// Paths are stored once in a flag-keyed map, with a set of [`InvalidationFlags`] describing what
775/// needs to happen for each, rather than in several separate sets. This avoids cloning each
776/// `PathBuf` into multiple collections.
777struct BatchedInvalidations {
778    paths: FxIndexMap<Box<Path>, InvalidationFlags>,
779    /// The most recently updated entry in [`Self::paths`].
780    last_updated_index: Option<usize>,
781    /// See [`Self::new_paths`]. Stored as [`None`] in recursive mode.
782    new_paths: Option<FxHashSet<usize>>,
783    /// Whether events are coming from [`PollWatcher`] instead of [`RecommendedWatcher`], which
784    /// changes how a file content change is reported. See [`Self::is_content_change`].
785    polling: bool,
786}
787
788impl BatchedInvalidations {
789    fn new(recursive_mode: DiskWatcherRecursiveMode, polling: bool) -> Self {
790        Self {
791            paths: FxIndexMap::default(),
792            last_updated_index: None,
793            new_paths: match recursive_mode {
794                DiskWatcherRecursiveMode::NonRecursive => Some(FxHashSet::default()),
795                DiskWatcherRecursiveMode::Recursive => None,
796            },
797            polling,
798        }
799    }
800
801    /// Whether a [`ModifyKind::Metadata`] event means the file's *contents* changed.
802    ///
803    /// Some backends don't report content changes as [`ModifyKind::Data`] at all, so we have to
804    /// treat one specific metadata change per backend as a content change. Accepting these
805    /// unconditionally would mean invalidating on every `chmod`/`touch` on the backends that do
806    /// report `Data` properly.
807    fn is_content_change(&self, kind: MetadataKind) -> bool {
808        match kind {
809            // `PollWatcher` detects changes by comparing mtimes, so a content change surfaces as a
810            // write-time change. It only emits `Data` when `Config::with_compare_contents` is
811            // enabled, which we don't do because hashing every watched file is too expensive.
812            MetadataKind::WriteTime => self.polling,
813            // fsevents does not always emit `kFSEventStreamEventFlagItemModified` for a content
814            // change; sometimes it only emits `kFSEventStreamEventFlagItemInodeMetaMod`, which
815            // notify maps to `MetadataKind::Any`. This causes redundant invalidations, but it's the
816            // only way to reliably detect content changes there. Fix for PACK-2437.
817            //
818            // libuv does the same thing to trigger `UV_CHANGES`:
819            // https://github.com/libuv/libuv/commit/73cf3600d75a5884b890a1a94048b8f3f9c66876
820            //
821            // inotify and ReadDirectoryChangesW both report content changes on their own, so
822            // `MetadataKind::Any` there only ever means an actual attribute change.
823            MetadataKind::Any => cfg!(target_os = "macos"),
824            _ => false,
825        }
826    }
827
828    fn clear(&mut self) {
829        self.paths.clear();
830        self.last_updated_index = None;
831        if let Some(new_paths) = &mut self.new_paths {
832            new_paths.clear();
833        }
834    }
835
836    /// Records `index` as newly-created so its watch can be (re-)established. No-op in recursive
837    /// watching mode.
838    fn mark_new_path(&mut self, index: usize) {
839        if let Some(new_paths) = &mut self.new_paths {
840            new_paths.insert(index);
841        }
842    }
843
844    /// Sets the `flags` for `path`. Returns the index that was modified.
845    fn mark(&mut self, path: Cow<'_, Path>, flags: InvalidationFlags) -> usize {
846        match self.paths.raw_entry_mut_v1().from_key(path.as_ref()) {
847            RawEntryMut::Occupied(mut entry) => {
848                *entry.get_mut() |= flags;
849                entry.index()
850            }
851            RawEntryMut::Vacant(entry) => {
852                let index = entry.index();
853                entry.insert(path.into_owned().into_boxed_path(), flags);
854                index
855            }
856        }
857    }
858
859    fn last_updated_path(&self) -> Option<&Path> {
860        self.last_updated_index
861            .and_then(|index| self.paths.get_index(index))
862            .map(|(path, _)| &**path)
863    }
864
865    fn mark_parent_dir(&mut self, path: &Path) {
866        if let Some(parent) = path.parent() {
867            self.mark(Cow::Borrowed(parent), InvalidationFlags::PATH_DIR);
868        }
869    }
870
871    /// Iterates over the newly-created paths in this batch. In non-recursive watching mode, these
872    /// must have their watches (re-)established before [`Self::execute`] is called (see the note
873    /// there). Always empty in recursive mode.
874    fn new_paths(&self) -> impl Iterator<Item = &Path> {
875        self.new_paths
876            .iter()
877            .flatten()
878            .map(|&index| self.paths.get_index(index).unwrap().0.as_ref())
879    }
880
881    /// Updates the batch to contain updated paths from the given event. Does not perform any
882    /// invalidations.
883    ///
884    /// Returns whether the event contained relevant events.
885    #[must_use]
886    fn add_event(&mut self, event: notify::Event) -> bool {
887        let paths: Vec<PathBuf> = event.paths;
888        let mut last_updated_index = None;
889        match event.kind {
890            EventKind::Modify(ModifyKind::Data(_)) => {
891                for path in paths {
892                    last_updated_index = Some(self.mark(Cow::Owned(path), InvalidationFlags::PATH));
893                }
894            }
895            // Some backends (fsevents, polling) can report metadata events for file content changes
896            EventKind::Modify(ModifyKind::Metadata(kind)) if self.is_content_change(kind) => {
897                for path in paths {
898                    last_updated_index = Some(self.mark(Cow::Owned(path), InvalidationFlags::PATH));
899                }
900            }
901            EventKind::Create(_) => {
902                for path in paths {
903                    self.mark_parent_dir(&path);
904                    let index = self.mark(
905                        Cow::Owned(path),
906                        InvalidationFlags::PATH_AND_CHILDREN
907                            | InvalidationFlags::PATH_AND_CHILDREN_DIR,
908                    );
909                    self.mark_new_path(index);
910                    last_updated_index = Some(index);
911                }
912            }
913            EventKind::Remove(_) => {
914                for path in paths {
915                    self.mark_parent_dir(&path);
916                    last_updated_index = Some(self.mark(
917                        Cow::Owned(path),
918                        InvalidationFlags::PATH_AND_CHILDREN
919                            | InvalidationFlags::PATH_AND_CHILDREN_DIR,
920                    ));
921                }
922            }
923            // A single event emitted with both the `From` and `To` paths.
924            EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
925                let [source, destination] = <[PathBuf; 2]>::try_from(paths)
926                    .expect("RenameMode::Both event must contain exactly two paths");
927
928                self.mark_parent_dir(&source);
929                self.mark(Cow::Owned(source), InvalidationFlags::PATH_AND_CHILDREN);
930
931                self.mark_parent_dir(&destination);
932                let index = self.mark(
933                    Cow::Owned(destination),
934                    InvalidationFlags::PATH_AND_CHILDREN,
935                );
936                self.mark_new_path(index);
937                last_updated_index = Some(index);
938            }
939            // We expect `RenameMode::Both` to cover most of the cases we need to invalidate,
940            // but we also check other RenameModes to cover cases where notify couldn't match the
941            // two rename events.
942            EventKind::Any | EventKind::Modify(ModifyKind::Any | ModifyKind::Name(..)) => {
943                for path in paths {
944                    self.mark_parent_dir(&path);
945                    last_updated_index = Some(self.mark(
946                        Cow::Owned(path),
947                        InvalidationFlags::PATH_AND_CHILDREN
948                            | InvalidationFlags::PATH_AND_CHILDREN_DIR,
949                    ));
950                }
951            }
952            EventKind::Modify(ModifyKind::Metadata(..) | ModifyKind::Other)
953            | EventKind::Access(_)
954            | EventKind::Other => {}
955        }
956        if let Some(index) = last_updated_index {
957            self.last_updated_index = Some(index);
958            true
959        } else {
960            false
961        }
962    }
963
964    /// Updates the batch to invalidate paths associated with a watcher error.
965    fn add_error(&mut self, paths: Vec<PathBuf>, root_path: &Path) {
966        let flags = InvalidationFlags::PATH_AND_CHILDREN | InvalidationFlags::PATH_AND_CHILDREN_DIR;
967        if paths.is_empty() {
968            self.last_updated_index = Some(self.mark(Cow::Borrowed(root_path), flags));
969        } else {
970            for path in paths {
971                self.last_updated_index = Some(self.mark(Cow::Owned(path), flags));
972            }
973        }
974    }
975
976    /// Performs all batched invalidations, calling `invalidate` once for each `(path, invalidator)`
977    /// pair that needs to be invalidated, then clears the batch.
978    ///
979    /// In non-recursive watching mode, [`Self::new_paths`] must be processed (to (re-)establish
980    /// watches) *before* calling this.
981    ///
982    /// For each path, a recursive invalidation subsumes an exact one, as
983    /// [`extract_path_with_children`][OrderedPathMapExt::extract_path_with_children] removes the
984    /// path itself in addition to its children.
985    fn execute(
986        &mut self,
987        invalidator_map: &InvalidatorMap,
988        dir_invalidator_map: &InvalidatorMap,
989        invalidate: impl Fn(&Path, Invalidator),
990    ) {
991        for (map, exact_flag, recursive_flag) in [
992            (
993                invalidator_map,
994                InvalidationFlags::PATH,
995                InvalidationFlags::PATH_AND_CHILDREN,
996            ),
997            (
998                dir_invalidator_map,
999                InvalidationFlags::PATH_DIR,
1000                InvalidationFlags::PATH_AND_CHILDREN_DIR,
1001            ),
1002        ] {
1003            let mut map = map.lock().unwrap();
1004            for (path, flags) in &self.paths {
1005                if flags.contains(recursive_flag) {
1006                    for (_, invalidators) in map.extract_path_with_children(path) {
1007                        for invalidator in invalidators {
1008                            invalidate(path, invalidator);
1009                        }
1010                    }
1011                } else if flags.contains(exact_flag)
1012                    && let Some(invalidators) = map.remove(&**path)
1013                {
1014                    for invalidator in invalidators {
1015                        invalidate(path, invalidator);
1016                    }
1017                }
1018            }
1019        }
1020        self.clear();
1021    }
1022}
1023
1024#[instrument(
1025    parent = None,
1026    level = "info",
1027    name = "file change",
1028    skip_all,
1029    fields(name = %invalidation_reason_path.display())
1030)]
1031fn invalidate(
1032    inner: &impl DiskFileSystemWatcherApi,
1033    turbo_tasks: &dyn TurboTasksApi,
1034    report_invalidation_reason: bool,
1035    invalidation_reason_path: &Path,
1036    invalidator: Invalidator,
1037) {
1038    if report_invalidation_reason
1039        && let Some(path) =
1040            format_absolute_fs_path(invalidation_reason_path, inner.name(), inner.root_path())
1041    {
1042        invalidator.invalidate_with_reason(turbo_tasks, WatchChange { path });
1043        return;
1044    }
1045    invalidator.invalidate(turbo_tasks);
1046}
1047
1048/// Invalidation was caused by a watcher rescan event. This will likely invalidate *every* watched
1049/// file.
1050#[derive(Clone, PartialEq, Eq, Hash)]
1051pub struct InvalidateRescan {
1052    path: RcStr,
1053}
1054
1055impl InvalidationReason for InvalidateRescan {
1056    fn kind(&self) -> Option<StaticOrArc<dyn InvalidationReasonKind>> {
1057        Some(StaticOrArc::Static(&INVALIDATE_RESCAN_KIND))
1058    }
1059}
1060
1061impl fmt::Display for InvalidateRescan {
1062    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1063        write!(f, "{} in filesystem invalidated", self.path)
1064    }
1065}
1066
1067/// [Invalidation kind][InvalidationReasonKind] for [`InvalidateRescan`].
1068#[derive(PartialEq, Eq, Hash)]
1069struct InvalidateRescanKind;
1070
1071static INVALIDATE_RESCAN_KIND: InvalidateRescanKind = InvalidateRescanKind;
1072
1073impl InvalidationReasonKind for InvalidateRescanKind {
1074    fn fmt(
1075        &self,
1076        reasons: &FxIndexSet<StaticOrArc<dyn InvalidationReason>>,
1077        f: &mut fmt::Formatter<'_>,
1078    ) -> fmt::Result {
1079        let first_reason: &dyn InvalidationReason = &*reasons[0];
1080        write!(
1081            f,
1082            "{} items in filesystem invalidated due to notify::Watcher rescan event ({}, ...)",
1083            reasons.len(),
1084            (first_reason as &dyn Any)
1085                .downcast_ref::<InvalidateRescan>()
1086                .unwrap()
1087                .path
1088        )
1089    }
1090}
1091
1092#[cfg(test)]
1093mod tests {
1094    use std::{
1095        fs,
1096        time::{Instant, SystemTime},
1097    };
1098
1099    use rstest::rstest;
1100    use turbo_tasks::TurboTasks;
1101    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
1102
1103    use super::*;
1104    use crate::watcher::mock_fs_api::MockFileSystem;
1105
1106    /// Polls [`tracked_read`] until it has executed more than `previous_runs` times, i.e. until the
1107    /// watcher has invalidated it.
1108    async fn wait_for_rerun(fs: &Arc<MockFileSystem>, path: &Path, previous_runs: u64) {
1109        const WATCH_TIMEOUT: Duration = Duration::from_secs(5);
1110        let deadline = Instant::now() + WATCH_TIMEOUT;
1111        loop {
1112            if fs.tracked_read_strongly_consistent(path).await > previous_runs {
1113                return;
1114            }
1115            assert!(
1116                Instant::now() < deadline,
1117                "the watcher did not invalidate {path:?} within {WATCH_TIMEOUT:?}",
1118            );
1119            tokio::time::sleep(Duration::from_millis(10)).await;
1120        }
1121    }
1122
1123    /// Backdates `path`'s mtime so that a [`PollWatcher`] can see the write that follows it to
1124    /// avoid mtime truncation issues.
1125    fn backdate(path: &Path) {
1126        fs::File::options()
1127            .write(true)
1128            .open(path)
1129            .unwrap()
1130            .set_modified(SystemTime::now() - Duration::from_secs(10))
1131            .unwrap();
1132    }
1133
1134    /// `recursive_mode` is set explicitly rather than left to the platform default so that both
1135    /// watching strategies are covered on every host. `TURBO_TASKS_FORCE_WATCH_MODE` still
1136    /// overrides it, collapsing these into two cases.
1137    // Miri cannot run the native cases because inotify is unsupported, while the polling cases
1138    // require Turbo Tasks' link-section registry, which is unavailable under Miri.
1139    #[cfg(not(miri))]
1140    #[rstest]
1141    #[case::native_recursive(None, DiskWatcherRecursiveMode::Recursive)]
1142    #[case::native_non_recursive(None, DiskWatcherRecursiveMode::NonRecursive)]
1143    #[case::polling_recursive(Some(Duration::from_millis(20)), DiskWatcherRecursiveMode::Recursive)]
1144    #[case::polling_non_recursive(
1145        Some(Duration::from_millis(20)),
1146        DiskWatcherRecursiveMode::NonRecursive
1147    )]
1148    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1149    async fn watches_file_and_directory_changes(
1150        #[case] poll_interval: Option<Duration>,
1151        #[case] recursive_mode: DiskWatcherRecursiveMode,
1152    ) {
1153        let tt = TurboTasks::new(TurboTasksBackend::new(
1154            BackendOptions::default(),
1155            noop_backing_storage(),
1156        ));
1157        tt.run_once(async move {
1158            let fs = MockFileSystem::new(DiskWatcherConfig {
1159                recursive_mode: Some(recursive_mode),
1160                poll_interval,
1161                report_invalidation_reason: true,
1162                ..Default::default()
1163            });
1164            let sub_dir = fs.root_path.join("sub");
1165            let file_path = sub_dir.join("file.txt");
1166            fs::create_dir(&sub_dir).unwrap();
1167            fs::write(&file_path, "initial").unwrap();
1168            backdate(&file_path);
1169
1170            DiskWatcher::start_watching(fs.clone()).await?;
1171
1172            // the initial reads register the invalidators that the watcher will later fire
1173            assert_eq!(fs.tracked_read_strongly_consistent(&file_path).await, 1);
1174            assert_eq!(fs.tracked_read_strongly_consistent(&sub_dir).await, 1);
1175
1176            // reading again without touching the filesystem must not re-run anything
1177            assert_eq!(fs.tracked_read_strongly_consistent(&file_path).await, 1);
1178            assert_eq!(fs.tracked_read_strongly_consistent(&sub_dir).await, 1);
1179
1180            // modifying a file invalidates the task that read that file
1181            fs::write(&file_path, "updated")?;
1182            wait_for_rerun(&fs, &file_path, 1).await;
1183
1184            // creating a file invalidates the task that listed the containing directory
1185            let dir_runs = fs.tracked_read_strongly_consistent(&sub_dir).await;
1186            fs::write(sub_dir.join("new.txt"), "new")?;
1187            wait_for_rerun(&fs, &sub_dir, dir_runs).await;
1188
1189            fs.watcher.stop_watching().await;
1190            anyhow::Ok(())
1191        })
1192        .await
1193        .unwrap();
1194    }
1195}