1mod batch_schedule;
2mod fs_api;
3#[cfg(test)]
4mod mock_fs_api;
5
6use std::{
7 any::Any,
8 borrow::Cow,
9 cell::LazyCell,
10 collections::BTreeSet,
11 env, fmt,
12 path::{Path, PathBuf},
13 sync::{
14 Arc, LazyLock,
15 mpsc::{Receiver, RecvTimeoutError, channel},
16 },
17 time::Duration,
18};
19
20use anyhow::{Context, Result};
21use bincode::{
22 Decode, Encode,
23 de::Decoder,
24 enc::Encoder,
25 error::{DecodeError, EncodeError},
26};
27use bitflags::bitflags;
28use indexmap::map::{RawEntryApiV1, raw_entry_v1::RawEntryMut};
29use notify::{
30 Config, EventKind, PollWatcher, RecommendedWatcher, Watcher,
31 event::{MetadataKind, ModifyKind, RenameMode},
32};
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
49static 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 pub recursive_mode: Option<DiskWatcherRecursiveMode>,
77 pub poll_interval: Option<Duration>,
84 pub report_invalidation_reason: bool,
90
91 pub batch_delay: Duration,
98 pub extended_batch_delay_matcher: Option<ResolvedVc<Box<dyn DiskWatcherPathMatcher>>>,
101 pub extended_batch_delay_duration: Duration,
104
105 pub settling_event_initial_delay: Duration,
109 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#[turbo_tasks::value_trait]
132pub trait DiskWatcherPathMatcher {
133 fn match_path(&self, path: &Path) -> bool;
136}
137
138#[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 fn resolve_recursive_mode(&self) -> DiskWatcherRecursiveMode {
166 let platform_has_efficient_recursive_watcher =
173 cfg!(any(target_os = "macos", target_os = "windows"));
174
175 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 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
198impl 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 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
252enum RecursiveState {
255 Stopped,
258 Watching {
259 _notify_watcher: NotifyWatcher,
261 },
262}
263
264enum NonRecursiveState {
267 Stopped,
270 Watching(NonRecursiveWatchingState),
271}
272
273struct NonRecursiveWatchingState {
275 notify_watcher: NotifyWatcher,
276 watched: BTreeSet<PathBuf>,
284}
285
286enum 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 #[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 let _ = start_watching_dir(&mut watching_state.notify_watcher, dir_path, root_path);
322 }
323 }
324
325 #[instrument(skip_all, level = "trace")]
330 pub async fn restore_all_if_watched(
331 state: &RwLock<NonRecursiveState>,
332 dir_paths: FxIndexSet<Box<Path>>,
333 root_path: &Path,
334 mut on_restored: impl FnMut(Box<Path>),
335 ) {
336 let needs_restore = |watched: &BTreeSet<PathBuf>, dir_path: &Path| {
337 dir_path != root_path && watched.contains(dir_path)
340 };
341
342 {
345 let guard = state.read().await;
346 let NonRecursiveState::Watching(watching_state) = &*guard else {
347 return;
348 };
349 if !dir_paths
350 .iter()
351 .any(|dir_path| needs_restore(&watching_state.watched, dir_path))
352 {
353 return;
354 }
355 }
356
357 let mut guard = state.write().await;
359 let NonRecursiveState::Watching(watching_state) = &mut *guard else {
360 return;
361 };
362 for dir_path in dir_paths {
363 if needs_restore(&watching_state.watched, &dir_path) {
364 let _ = restore_watched_dir(watching_state, &dir_path, root_path);
367 on_restored(dir_path);
368 }
369 }
370 }
371
372 fn restore_watched_dir(
374 watching_state: &mut NonRecursiveWatchingState,
375 dir_path: &Path,
376 root_path: &Path,
377 ) -> Result<()> {
378 start_watching_dir(&mut watching_state.notify_watcher, dir_path, root_path)?;
380
381 for child_path in watching_state.watched.iter_path_children(dir_path) {
383 start_watching_dir(&mut watching_state.notify_watcher, child_path, root_path)?;
385 }
386 Ok(())
387 }
388
389 #[instrument(skip_all, level = "trace")]
394 pub async fn ensure_watched(
395 state: &RwLock<NonRecursiveState>,
396 dir_path: &Path,
397 root_path: &Path,
398 ) -> Result<()> {
399 if dir_path == root_path {
402 return Ok(());
403 }
404
405 {
408 let guard = state.read().await;
409 let NonRecursiveState::Watching(watching_state) = &*guard else {
410 return Ok(());
411 };
412 if watching_state.watched.contains(dir_path) {
413 return Ok(());
414 }
415 }
416
417 let mut guard = state.write().await;
419 let NonRecursiveState::Watching(watching_state) = &mut *guard else {
420 return Ok(());
421 };
422 if watching_state.watched.insert(dir_path.to_path_buf()) {
423 start_watching_dir_and_parents(watching_state, dir_path, root_path)?;
424 }
425 Ok(())
426 }
427
428 fn start_watching_dir(
435 notify_watcher: &mut NotifyWatcher,
436 dir_path: &Path,
437 root_path: &Path,
438 ) -> Result<()> {
439 debug_assert_ne!(dir_path, root_path);
440
441 match notify_watcher.watch(dir_path, notify::RecursiveMode::NonRecursive) {
442 Ok(())
443 | Err(notify::Error {
444 kind: notify::ErrorKind::PathNotFound,
448 ..
449 }) => Ok(()),
450 Err(err) => {
451 return Err(err).context(format!("Unable to watch {}", dir_path.display(),));
453 }
454 }
455 }
456
457 fn start_watching_dir_and_parents(
464 state: &mut NonRecursiveWatchingState,
465 dir_path: &Path,
466 root_path: &Path,
467 ) -> Result<()> {
468 let mut found_watched_ancestor = false;
469
470 let dir_and_ancestor_paths: Vec<_> = [dir_path]
472 .into_iter()
473 .chain(
474 dir_path
475 .ancestors()
476 .skip(1)
479 .take_while(|p| {
480 found_watched_ancestor = *p == root_path || state.watched.contains(*p);
481 !found_watched_ancestor
482 }),
483 )
484 .collect();
485
486 if !found_watched_ancestor {
487 anyhow::bail!(
489 "failed to find the fs root of {root_path:?} while watching {dir_path:?}"
490 );
491 }
492
493 for path in dir_and_ancestor_paths.into_iter().rev() {
497 start_watching_dir(&mut state.notify_watcher, path, root_path)?;
500 state.watched.insert(path.to_owned());
501 }
502
503 Ok(())
504 }
505}
506
507impl DiskWatcher {
508 pub fn new(config: DiskWatcherConfig) -> Self {
509 assert!(
510 config.extended_batch_delay_duration >= config.batch_delay,
511 "extended_batch_delay_duration must be at least batch_delay"
512 );
513 Self {
514 state: State::new_stopped(config.resolve_recursive_mode()),
515 config,
516 }
517 }
518
519 pub async fn start_watching<FsApi: DiskFileSystemWatcherApi>(fs: Arc<FsApi>) -> Result<()> {
520 let watcher: &Self = fs.watcher();
521
522 let extended_batch_delay_matcher = match watcher.config.extended_batch_delay_matcher {
524 Some(matcher) => Some(matcher.into_trait_ref().await?),
525 None => None,
526 };
527
528 let state_guard = watcher.state.write().await;
529
530 if let StateWriteGuard::Recursive(guard) = &state_guard
532 && matches!(**guard, RecursiveState::Watching { .. })
533 {
534 return Ok(());
535 } else if let StateWriteGuard::NonRecursive(guard) = &state_guard
536 && matches!(**guard, NonRecursiveState::Watching(..))
537 {
538 return Ok(());
539 }
540
541 let (tx, rx) = channel();
543 let config = Config::default();
546 let config = config.with_follow_symlinks(false);
549
550 let mut notify_watcher = if let Some(poll_interval) = watcher.config.poll_interval {
551 let config = config.with_poll_interval(poll_interval);
552 NotifyWatcher::Polling(PollWatcher::new(tx, config)?)
553 } else {
554 NotifyWatcher::Recommended(RecommendedWatcher::new(tx, config)?)
555 };
556
557 let root_path = fs.root_path();
560 notify_watcher.watch(
561 root_path,
562 notify::RecursiveMode::from(watcher.state.recursive_mode()),
563 )?;
564
565 if watcher.config.report_invalidation_reason {
570 let name = fs.name().clone();
571 fs.invalidate_all_with_reason(|path| WatchStart {
572 name: name.clone(),
573 path: RcStr::from(path.to_string_lossy()),
575 });
576 } else {
577 fs.invalidate_all();
578 }
579
580 spawn_thread({
581 let fs = fs.clone();
582 move || Self::watch_thread(fs, rx, extended_batch_delay_matcher)
583 });
584
585 match state_guard {
588 StateWriteGuard::Recursive(mut recursive) => {
589 *recursive = RecursiveState::Watching {
590 _notify_watcher: notify_watcher,
591 }
592 }
593 StateWriteGuard::NonRecursive(mut non_recursive) => {
594 *non_recursive = NonRecursiveState::Watching(NonRecursiveWatchingState {
595 notify_watcher,
596 watched: BTreeSet::new(),
597 })
598 }
599 };
600
601 Ok(())
602 }
603
604 pub async fn stop_watching(&self) {
605 match &self.state {
606 State::Recursive(state) => *state.write().await = RecursiveState::Stopped,
607 State::NonRecursive(state) => *state.write().await = NonRecursiveState::Stopped,
608 }
609 }
612
613 fn watch_thread<FsApi: DiskFileSystemWatcherApi>(
618 fs: Arc<FsApi>,
619 rx: Receiver<notify::Result<notify::Event>>,
620 extended_batch_delay_matcher: Option<TraitRef<Box<dyn DiskWatcherPathMatcher>>>,
621 ) {
622 let watcher: &Self = fs.watcher();
623 let config = &watcher.config;
624 let report_invalidation_reason = config.report_invalidation_reason;
625 let mut batch = BatchedInvalidations::new(
626 watcher.state.recursive_mode(),
627 config.poll_interval.is_some(),
628 );
629 let mut schedule = BatchSchedule::new(config);
630
631 'outer: loop {
632 loop {
633 match schedule.recv_event(&rx, &*fs, &batch) {
634 Ok(Ok(event)) => {
635 if event.need_rescan() {
645 let _lock = fs.invalidation_lock().blocking_write();
646
647 while rx.try_recv().is_ok() {}
650
651 if let State::NonRecursive(non_recursive) = &watcher.state {
652 fs.tokio_handle().block_on(
659 non_recursive_helpers::restore_all_watched_ignore_errors(
660 non_recursive,
661 fs.root_path(),
662 ),
663 );
664 }
665
666 if report_invalidation_reason {
667 fs.invalidate_all_with_reason(|path| InvalidateRescan {
668 path: RcStr::from(path.to_string_lossy()),
670 });
671 } else {
672 fs.invalidate_all();
673 }
674
675 batch.clear();
678 schedule.reset();
679 break;
680 }
681
682 let mut delay = config.batch_delay;
687 if let Some(matcher) = &extended_batch_delay_matcher
688 && event.paths.iter().any(|path| matcher.match_path(path))
689 {
690 delay = delay.max(config.extended_batch_delay_duration);
691 }
692
693 if batch.add_event(event, fs.invalidator_map(), fs.dir_invalidator_map()) {
694 schedule.extend(delay);
695 } else if !batch.has_pending_invalidations() && batch.has_new_paths() {
696 break;
700 }
701 }
702 Ok(Err(notify::Error { kind, paths })) => {
704 if !is_not_found_error(&kind) {
708 eprintln!("watch error ({paths:?}): {kind:?} ");
709 }
710
711 batch.add_error(paths, fs.root_path());
712 schedule.extend(config.batch_delay);
713 }
714 Err(RecvTimeoutError::Timeout) => {
715 break;
717 }
718 Err(RecvTimeoutError::Disconnected) => {
719 break 'outer;
722 }
723 }
724 }
725
726 if let State::NonRecursive(non_recursive) = &watcher.state
729 && batch.has_new_paths()
730 {
731 let new_paths = batch.take_new_paths();
732 fs.tokio_handle()
738 .block_on(non_recursive_helpers::restore_all_if_watched(
739 non_recursive,
740 new_paths,
741 fs.root_path(),
742 |path| batch.mark_restored_path(path),
743 ));
744 }
745 if !batch.has_pending_invalidations() {
746 batch.clear();
747 continue;
748 }
749
750 let Some(turbo_tasks) = fs.turbo_tasks() else {
751 break 'outer;
753 };
754 let _guard = fs.tokio_handle().enter();
755
756 let _lock = fs.invalidation_lock().blocking_write();
757 let mut invalidators = Vec::new();
759 let mut changed_paths: Vec<PathBuf> = Vec::new();
761 batch.execute(
762 fs.invalidator_map(),
763 fs.dir_invalidator_map(),
764 |invalidation_reason_path, invalidator| {
765 invalidators.push(invalidator);
766 if changed_paths.last().map(|p| p.as_path()) != Some(invalidation_reason_path) {
767 changed_paths.push(invalidation_reason_path.to_path_buf());
768 }
769 },
770 );
771 invalidate(
772 &*fs,
773 &*turbo_tasks,
774 report_invalidation_reason,
775 &changed_paths,
776 invalidators,
777 );
778 }
779 }
780
781 pub async fn ensure_watched_file(&self, path: &Path, root_path: &Path) -> Result<()> {
782 if let State::NonRecursive(non_recursive) = &self.state
786 && let Some(dir_path) = path.parent()
787 {
788 non_recursive_helpers::ensure_watched(non_recursive, dir_path, root_path).await?;
789 }
790 Ok(())
791 }
792
793 pub async fn ensure_watched_dir(&self, dir_path: &Path, root_path: &Path) -> Result<()> {
794 if let State::NonRecursive(non_recursive) = &self.state {
795 non_recursive_helpers::ensure_watched(non_recursive, dir_path, root_path).await?;
796 }
797 Ok(())
798 }
799}
800
801bitflags! {
802 struct InvalidationFlags: u8 {
805 const PATH = 1 << 0;
807 const PATH_DIR = 1 << 1;
809 const PATH_AND_CHILDREN = 1 << 2;
811 const PATH_AND_CHILDREN_DIR = 1 << 3;
813 }
814}
815
816struct BatchedInvalidations {
826 paths: FxIndexMap<Box<Path>, InvalidationFlags>,
827 last_updated_index: Option<usize>,
829 new_paths: Option<FxIndexSet<Box<Path>>>,
837 polling: bool,
840}
841
842impl BatchedInvalidations {
843 fn new(recursive_mode: DiskWatcherRecursiveMode, polling: bool) -> Self {
844 Self {
845 paths: FxIndexMap::default(),
846 last_updated_index: None,
847 new_paths: match recursive_mode {
848 DiskWatcherRecursiveMode::NonRecursive => Some(FxIndexSet::default()),
849 DiskWatcherRecursiveMode::Recursive => None,
850 },
851 polling,
852 }
853 }
854
855 fn is_content_change(&self, kind: MetadataKind) -> bool {
862 match kind {
863 MetadataKind::WriteTime => self.polling,
867 MetadataKind::Any => cfg!(target_os = "macos"),
878 _ => false,
879 }
880 }
881
882 fn clear(&mut self) {
883 self.paths.clear();
884 self.last_updated_index = None;
885 if let Some(new_paths) = &mut self.new_paths {
886 new_paths.clear();
887 }
888 }
889
890 fn has_new_paths(&self) -> bool {
891 self.new_paths.as_ref().is_some_and(|p| !p.is_empty())
892 }
893
894 fn take_new_paths(&mut self) -> FxIndexSet<Box<Path>> {
897 self.new_paths
898 .as_mut()
899 .map(std::mem::take)
900 .unwrap_or_default()
901 }
902
903 fn mark_restored_path(&mut self, path: Box<Path>) {
906 self.mark(
907 Cow::Owned(path.into_path_buf()),
908 InvalidationFlags::PATH_AND_CHILDREN | InvalidationFlags::PATH_AND_CHILDREN_DIR,
909 );
910 }
911
912 fn has_pending_invalidations(&self) -> bool {
914 !self.paths.is_empty()
915 }
916
917 fn mark_new_path(&mut self, path: &Path) {
920 if let Some(new_paths) = &mut self.new_paths
921 && !new_paths.contains(path)
922 {
923 new_paths.insert(Box::from(path));
924 }
925 }
926
927 fn mark(&mut self, path: Cow<'_, Path>, flags: InvalidationFlags) -> usize {
929 match self.paths.raw_entry_mut_v1().from_key(path.as_ref()) {
930 RawEntryMut::Occupied(mut entry) => {
931 *entry.get_mut() |= flags;
932 entry.index()
933 }
934 RawEntryMut::Vacant(entry) => {
935 let index = entry.index();
936 entry.insert(path.into_owned().into_boxed_path(), flags);
937 index
938 }
939 }
940 }
941
942 fn last_updated_path(&self) -> Option<&Path> {
943 self.last_updated_index
944 .and_then(|index| self.paths.get_index(index))
945 .map(|(path, _)| &**path)
946 }
947
948 fn mark_parent_dir(&mut self, path: &Path) {
949 if let Some(parent) = path.parent() {
950 self.mark(Cow::Borrowed(parent), InvalidationFlags::PATH_DIR);
951 }
952 }
953
954 #[must_use]
960 fn add_event(
961 &mut self,
962 event: notify::Event,
963 invalidator_map: &InvalidatorMap,
964 dir_invalidator_map: &InvalidatorMap,
965 ) -> bool {
966 let invalidator_map_guard = LazyCell::new(|| invalidator_map.lock().unwrap());
968 let dir_invalidator_map_guard = LazyCell::new(|| dir_invalidator_map.lock().unwrap());
969
970 let is_relevant = |batch: &Self, path: &Path, parent: Option<&Path>, recursive: bool| {
974 if batch.paths.contains_key(path) {
976 return true;
977 }
978 invalidator_map_guard.contains_key(path)
981 || dir_invalidator_map_guard.contains_key(path)
982 || parent.is_some_and(|parent| dir_invalidator_map_guard.contains_key(parent))
983 || (recursive
984 && (invalidator_map_guard.contains_path_or_children(path)
985 || dir_invalidator_map_guard.contains_path_or_children(path)))
986 };
987
988 let paths = event.paths;
989 let mut last_updated_index = None;
990 match event.kind {
991 EventKind::Modify(ModifyKind::Data(_)) => {
992 for path in paths {
993 if is_relevant(self, &path, None, false) {
994 last_updated_index =
995 Some(self.mark(Cow::Owned(path), InvalidationFlags::PATH));
996 }
997 }
998 }
999 EventKind::Modify(ModifyKind::Metadata(kind)) if self.is_content_change(kind) => {
1001 for path in paths {
1002 if is_relevant(self, &path, None, false) {
1003 last_updated_index =
1004 Some(self.mark(Cow::Owned(path), InvalidationFlags::PATH));
1005 }
1006 }
1007 }
1008 EventKind::Create(_) => {
1009 for path in paths {
1010 self.mark_new_path(&path);
1011 if !is_relevant(self, &path, path.parent(), true) {
1012 continue;
1013 }
1014 self.mark_parent_dir(&path);
1015 last_updated_index = Some(self.mark(
1016 Cow::Owned(path),
1017 InvalidationFlags::PATH_AND_CHILDREN
1018 | InvalidationFlags::PATH_AND_CHILDREN_DIR,
1019 ));
1020 }
1021 }
1022 EventKind::Remove(_) => {
1023 for path in paths {
1024 if !is_relevant(self, &path, path.parent(), true) {
1025 continue;
1026 }
1027 self.mark_parent_dir(&path);
1028 last_updated_index = Some(self.mark(
1029 Cow::Owned(path),
1030 InvalidationFlags::PATH_AND_CHILDREN
1031 | InvalidationFlags::PATH_AND_CHILDREN_DIR,
1032 ));
1033 }
1034 }
1035 EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
1037 let [source, destination] = <[PathBuf; 2]>::try_from(paths)
1038 .expect("RenameMode::Both event must contain exactly two paths");
1039
1040 if is_relevant(self, &source, source.parent(), true) {
1041 self.mark_parent_dir(&source);
1042 last_updated_index =
1043 Some(self.mark(Cow::Owned(source), InvalidationFlags::PATH_AND_CHILDREN));
1044 }
1045
1046 self.mark_new_path(&destination);
1047 if is_relevant(self, &destination, destination.parent(), true) {
1048 self.mark_parent_dir(&destination);
1049 last_updated_index = Some(self.mark(
1050 Cow::Owned(destination),
1051 InvalidationFlags::PATH_AND_CHILDREN,
1052 ));
1053 }
1054 }
1055 EventKind::Any | EventKind::Modify(ModifyKind::Any | ModifyKind::Name(..)) => {
1059 for path in paths {
1060 if !is_relevant(self, &path, path.parent(), true) {
1061 continue;
1062 }
1063 self.mark_parent_dir(&path);
1064 last_updated_index = Some(self.mark(
1065 Cow::Owned(path),
1066 InvalidationFlags::PATH_AND_CHILDREN
1067 | InvalidationFlags::PATH_AND_CHILDREN_DIR,
1068 ));
1069 }
1070 }
1071 EventKind::Modify(ModifyKind::Metadata(..) | ModifyKind::Other)
1072 | EventKind::Access(_)
1073 | EventKind::Other => {}
1074 }
1075 if let Some(index) = last_updated_index {
1076 self.last_updated_index = Some(index);
1077 true
1078 } else {
1079 false
1080 }
1081 }
1082
1083 fn add_error(&mut self, paths: Vec<PathBuf>, root_path: &Path) {
1085 let flags = InvalidationFlags::PATH_AND_CHILDREN | InvalidationFlags::PATH_AND_CHILDREN_DIR;
1086 if paths.is_empty() {
1087 self.last_updated_index = Some(self.mark(Cow::Borrowed(root_path), flags));
1088 } else {
1089 for path in paths {
1090 self.last_updated_index = Some(self.mark(Cow::Owned(path), flags));
1091 }
1092 }
1093 }
1094
1095 fn execute(
1105 &mut self,
1106 invalidator_map: &InvalidatorMap,
1107 dir_invalidator_map: &InvalidatorMap,
1108 mut invalidate: impl FnMut(&Path, Invalidator),
1109 ) {
1110 for (map, exact_flag, recursive_flag) in [
1111 (
1112 invalidator_map,
1113 InvalidationFlags::PATH,
1114 InvalidationFlags::PATH_AND_CHILDREN,
1115 ),
1116 (
1117 dir_invalidator_map,
1118 InvalidationFlags::PATH_DIR,
1119 InvalidationFlags::PATH_AND_CHILDREN_DIR,
1120 ),
1121 ] {
1122 let mut map = map.lock().unwrap();
1123 for (path, flags) in &self.paths {
1124 if flags.contains(recursive_flag) {
1125 for (_, invalidators) in map.extract_path_with_children(path) {
1126 for invalidator in invalidators {
1127 invalidate(path, invalidator);
1128 }
1129 }
1130 } else if flags.contains(exact_flag)
1131 && let Some(invalidators) = map.remove(&**path)
1132 {
1133 for invalidator in invalidators {
1134 invalidate(path, invalidator);
1135 }
1136 }
1137 }
1138 }
1139 self.clear();
1140 }
1141}
1142
1143const MAX_SPAN_PATHS: usize = 20;
1145
1146struct SpanPaths<'a>(&'a [PathBuf]);
1149
1150impl std::fmt::Display for SpanPaths<'_> {
1151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1152 for (index, path) in self.0.iter().take(MAX_SPAN_PATHS).enumerate() {
1153 if index > 0 {
1154 f.write_str(", ")?;
1155 }
1156 write!(f, "{}", path.display())?;
1157 }
1158 if self.0.len() > MAX_SPAN_PATHS {
1159 write!(f, ", ... ({} more paths)", self.0.len() - MAX_SPAN_PATHS)?;
1160 }
1161 Ok(())
1162 }
1163}
1164
1165#[instrument(
1166 parent = None,
1167 level = "info",
1168 name = "file change",
1169 skip_all,
1170 fields(
1171 paths = %SpanPaths(invalidation_reason_paths),
1172 invalidators = invalidators.len(),
1173 )
1174)]
1175fn invalidate(
1176 inner: &impl DiskFileSystemWatcherApi,
1177 turbo_tasks: &dyn TurboTasksApi,
1178 report_invalidation_reason: bool,
1179 invalidation_reason_paths: &[PathBuf],
1180 invalidators: Vec<Invalidator>,
1181) {
1182 if invalidators.is_empty() {
1183 return;
1184 }
1185 if report_invalidation_reason {
1186 let reasons = invalidation_reason_paths
1187 .iter()
1188 .filter_map(|path| format_absolute_fs_path(path, inner.name(), inner.root_path()))
1189 .map(|path| WatchChange { path });
1190 Invalidator::invalidate_all_with_reasons(invalidators, turbo_tasks, reasons);
1191 } else {
1192 Invalidator::invalidate_all(invalidators, turbo_tasks);
1193 }
1194}
1195
1196#[derive(Clone, PartialEq, Eq, Hash)]
1199pub struct InvalidateRescan {
1200 path: RcStr,
1201}
1202
1203impl InvalidationReason for InvalidateRescan {
1204 fn kind(&self) -> Option<StaticOrArc<dyn InvalidationReasonKind>> {
1205 Some(StaticOrArc::Static(&INVALIDATE_RESCAN_KIND))
1206 }
1207}
1208
1209impl fmt::Display for InvalidateRescan {
1210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1211 write!(f, "{} in filesystem invalidated", self.path)
1212 }
1213}
1214
1215#[derive(PartialEq, Eq, Hash)]
1217struct InvalidateRescanKind;
1218
1219static INVALIDATE_RESCAN_KIND: InvalidateRescanKind = InvalidateRescanKind;
1220
1221impl InvalidationReasonKind for InvalidateRescanKind {
1222 fn fmt(
1223 &self,
1224 reasons: &FxIndexSet<StaticOrArc<dyn InvalidationReason>>,
1225 f: &mut fmt::Formatter<'_>,
1226 ) -> fmt::Result {
1227 let first_reason: &dyn InvalidationReason = &*reasons[0];
1228 write!(
1229 f,
1230 "{} items in filesystem invalidated due to notify::Watcher rescan event ({}, ...)",
1231 reasons.len(),
1232 (first_reason as &dyn Any)
1233 .downcast_ref::<InvalidateRescan>()
1234 .unwrap()
1235 .path
1236 )
1237 }
1238}
1239
1240fn is_not_found_error(kind: ¬ify::ErrorKind) -> bool {
1243 matches!(kind, notify::ErrorKind::Io(err) if err.kind() == std::io::ErrorKind::NotFound)
1244}
1245
1246#[cfg(test)]
1247mod tests {
1248 use std::{
1249 fs,
1250 time::{Instant, SystemTime},
1251 };
1252
1253 use notify::event::{CreateKind, DataChange, RemoveKind};
1254 use rstest::rstest;
1255 use turbo_tasks::TurboTasks;
1256 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
1257
1258 use super::*;
1259 use crate::watcher::mock_fs_api::MockFileSystem;
1260
1261 #[test]
1262 fn span_paths_are_truncated() {
1263 let paths = |n: usize| {
1264 (0..n)
1265 .map(|i| PathBuf::from(format!("p{i}")))
1266 .collect::<Vec<_>>()
1267 };
1268 assert_eq!(SpanPaths(&paths(0)).to_string(), "");
1269 assert_eq!(SpanPaths(&paths(2)).to_string(), "p0, p1");
1270 let twenty = (0..20)
1271 .map(|i| format!("p{i}"))
1272 .collect::<Vec<_>>()
1273 .join(", ");
1274 assert_eq!(SpanPaths(&paths(20)).to_string(), twenty);
1275 assert_eq!(
1276 SpanPaths(&paths(21)).to_string(),
1277 format!("{twenty}, ... (1 more paths)")
1278 );
1279 assert_eq!(
1280 SpanPaths(&paths(25)).to_string(),
1281 format!("{twenty}, ... (5 more paths)")
1282 );
1283 }
1284
1285 #[cfg(not(miri))]
1286 #[tokio::test]
1287 async fn batches_only_tracked_paths() {
1288 let tt = TurboTasks::new(TurboTasksBackend::new(
1289 BackendOptions::default(),
1290 noop_backing_storage(),
1291 ));
1292 tt.run_once(async move {
1293 let invalidator_map = InvalidatorMap::new();
1294 let dir_invalidator_map = InvalidatorMap::new();
1295 let invalidator = turbo_tasks::get_invalidator().unwrap();
1296 invalidator_map.insert(Arc::new(PathBuf::from("root/file")), invalidator);
1297 dir_invalidator_map.insert(Arc::new(PathBuf::from("root/listed")), invalidator);
1298 dir_invalidator_map.insert(Arc::new(PathBuf::from("dirs/sub")), invalidator);
1299
1300 let content = EventKind::Modify(ModifyKind::Data(DataChange::Any));
1301 let create = EventKind::Create(CreateKind::Any);
1302 let remove = EventKind::Remove(RemoveKind::Any);
1303 let rename = EventKind::Modify(ModifyKind::Name(RenameMode::From));
1304 for (kind, path, relevant) in [
1305 (content, "root", false),
1306 (content, "root/file", true),
1307 (content, "root/listed", true),
1308 (create, "root/listed/new", true),
1309 (create, "root", true),
1310 (remove, "root", true),
1311 (rename, "root", true),
1312 (remove, "dirs", true),
1313 ] {
1314 let mut batch =
1315 BatchedInvalidations::new(DiskWatcherRecursiveMode::NonRecursive, false);
1316 let event = notify::Event::new(kind).add_path(PathBuf::from(path));
1317 assert_eq!(
1318 batch.add_event(event, &invalidator_map, &dir_invalidator_map),
1319 relevant,
1320 "{kind:?} at {path}",
1321 );
1322 assert_eq!(batch.last_updated_path().is_some(), relevant);
1323 }
1324 anyhow::Ok(())
1325 })
1326 .await
1327 .unwrap();
1328 }
1329
1330 async fn wait_for_rerun(fs: &Arc<MockFileSystem>, path: &Path, previous_runs: u64) {
1333 const WATCH_TIMEOUT: Duration = Duration::from_secs(5);
1334 let deadline = Instant::now() + WATCH_TIMEOUT;
1335 loop {
1336 if fs.tracked_read_strongly_consistent(path).await > previous_runs {
1337 return;
1338 }
1339 assert!(
1340 Instant::now() < deadline,
1341 "the watcher did not invalidate {path:?} within {WATCH_TIMEOUT:?}",
1342 );
1343 tokio::time::sleep(Duration::from_millis(10)).await;
1344 }
1345 }
1346
1347 fn backdate(path: &Path) {
1350 fs::File::options()
1351 .write(true)
1352 .open(path)
1353 .unwrap()
1354 .set_modified(SystemTime::now() - Duration::from_secs(10))
1355 .unwrap();
1356 }
1357
1358 #[cfg(not(miri))]
1364 #[rstest]
1365 #[case::native_recursive(None, DiskWatcherRecursiveMode::Recursive)]
1366 #[case::native_non_recursive(None, DiskWatcherRecursiveMode::NonRecursive)]
1367 #[case::polling_recursive(Some(Duration::from_millis(20)), DiskWatcherRecursiveMode::Recursive)]
1368 #[case::polling_non_recursive(
1369 Some(Duration::from_millis(20)),
1370 DiskWatcherRecursiveMode::NonRecursive
1371 )]
1372 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1373 async fn watches_file_and_directory_changes(
1374 #[case] poll_interval: Option<Duration>,
1375 #[case] recursive_mode: DiskWatcherRecursiveMode,
1376 ) {
1377 let tt = TurboTasks::new(TurboTasksBackend::new(
1378 BackendOptions::default(),
1379 noop_backing_storage(),
1380 ));
1381 tt.run_once(async move {
1382 let fs = MockFileSystem::new(DiskWatcherConfig {
1383 recursive_mode: Some(recursive_mode),
1384 poll_interval,
1385 report_invalidation_reason: true,
1386 ..Default::default()
1387 });
1388 let sub_dir = fs.root_path.join("sub");
1389 let file_path = sub_dir.join("file.txt");
1390 fs::create_dir(&sub_dir).unwrap();
1391 fs::write(&file_path, "initial").unwrap();
1392 backdate(&file_path);
1393
1394 DiskWatcher::start_watching(fs.clone()).await?;
1395
1396 assert_eq!(fs.tracked_read_strongly_consistent(&file_path).await, 1);
1398 assert_eq!(fs.tracked_read_strongly_consistent(&sub_dir).await, 1);
1399
1400 assert_eq!(fs.tracked_read_strongly_consistent(&file_path).await, 1);
1402 assert_eq!(fs.tracked_read_strongly_consistent(&sub_dir).await, 1);
1403
1404 fs::write(&file_path, "updated")?;
1406 wait_for_rerun(&fs, &file_path, 1).await;
1407
1408 let dir_runs = fs.tracked_read_strongly_consistent(&sub_dir).await;
1410 fs::write(sub_dir.join("new.txt"), "new")?;
1411 wait_for_rerun(&fs, &sub_dir, dir_runs).await;
1412
1413 fs.watcher.stop_watching().await;
1414 anyhow::Ok(())
1415 })
1416 .await
1417 .unwrap();
1418 }
1419
1420 #[cfg(not(miri))]
1423 #[rstest]
1424 #[case::native_recursive(None, DiskWatcherRecursiveMode::Recursive)]
1425 #[case::native_non_recursive(None, DiskWatcherRecursiveMode::NonRecursive)]
1426 #[case::polling_recursive(Some(Duration::from_millis(20)), DiskWatcherRecursiveMode::Recursive)]
1427 #[case::polling_non_recursive(
1428 Some(Duration::from_millis(20)),
1429 DiskWatcherRecursiveMode::NonRecursive
1430 )]
1431 #[cfg_attr(feature = "ci", ignore = "timing-sensitive, flaky in CI")]
1432 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1433 async fn irrelevant_events_do_not_delay_invalidation(
1434 #[case] poll_interval: Option<Duration>,
1435 #[case] recursive_mode: DiskWatcherRecursiveMode,
1436 ) {
1437 const BATCH_DELAY: Duration = Duration::from_millis(300);
1438 const CHURN: Duration = Duration::from_secs(3);
1439 const CHURN_INTERVAL: Duration = Duration::from_millis(50);
1440
1441 let tt = TurboTasks::new(TurboTasksBackend::new(
1442 BackendOptions::default(),
1443 noop_backing_storage(),
1444 ));
1445 tt.run_once(async move {
1446 let fs = MockFileSystem::new(DiskWatcherConfig {
1447 recursive_mode: Some(recursive_mode),
1448 poll_interval,
1449 batch_delay: BATCH_DELAY,
1450 extended_batch_delay_duration: BATCH_DELAY,
1451 ..Default::default()
1452 });
1453 let sub_dir = fs.root_path.join("sub");
1454 let file_path = sub_dir.join("file.txt");
1455 let other_path = sub_dir.join("other.txt");
1456 fs::create_dir(&sub_dir).unwrap();
1457 fs::write(&file_path, "initial").unwrap();
1458 fs::write(&other_path, "0").unwrap();
1459 backdate(&file_path);
1460 backdate(&other_path);
1461
1462 DiskWatcher::start_watching(fs.clone()).await?;
1463 assert_eq!(fs.tracked_read_strongly_consistent(&file_path).await, 1);
1464
1465 let churn_start = Instant::now();
1466 let churn = std::thread::spawn({
1467 let other_path = other_path.clone();
1468 move || {
1469 let mut i = 0u64;
1470 while churn_start.elapsed() < CHURN {
1471 i += 1;
1472 fs::write(&other_path, i.to_string()).unwrap();
1473 std::thread::sleep(CHURN_INTERVAL);
1474 }
1475 }
1476 });
1477
1478 std::thread::sleep(CHURN_INTERVAL * 2);
1479 fs::write(&file_path, "updated")?;
1480 wait_for_rerun(&fs, &file_path, 1).await;
1481 let elapsed = churn_start.elapsed();
1482 assert!(
1483 elapsed < CHURN - Duration::from_millis(500),
1484 "invalidation was delayed by unrelated events: took {elapsed:?}"
1485 );
1486
1487 churn.join().unwrap();
1488 fs.watcher.stop_watching().await;
1489 anyhow::Ok(())
1490 })
1491 .await
1492 .unwrap();
1493 }
1494
1495 #[cfg(not(miri))]
1498 #[rstest]
1499 #[case::native_recursive(None, DiskWatcherRecursiveMode::Recursive)]
1500 #[case::native_non_recursive(None, DiskWatcherRecursiveMode::NonRecursive)]
1501 #[case::polling_recursive(Some(Duration::from_millis(20)), DiskWatcherRecursiveMode::Recursive)]
1502 #[case::polling_non_recursive(
1503 Some(Duration::from_millis(20)),
1504 DiskWatcherRecursiveMode::NonRecursive
1505 )]
1506 #[cfg_attr(feature = "ci", ignore = "timing-sensitive, flaky in CI")]
1507 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1508 async fn repeated_writes_to_tracked_file_are_debounced(
1509 #[case] poll_interval: Option<Duration>,
1510 #[case] recursive_mode: DiskWatcherRecursiveMode,
1511 ) {
1512 const BATCH_DELAY: Duration = Duration::from_millis(300);
1513 const CHURN: Duration = Duration::from_millis(1500);
1514 const CHURN_INTERVAL: Duration = Duration::from_millis(50);
1515
1516 let tt = TurboTasks::new(TurboTasksBackend::new(
1517 BackendOptions::default(),
1518 noop_backing_storage(),
1519 ));
1520 tt.run_once(async move {
1521 let fs = MockFileSystem::new(DiskWatcherConfig {
1522 recursive_mode: Some(recursive_mode),
1523 poll_interval,
1524 batch_delay: BATCH_DELAY,
1525 extended_batch_delay_duration: BATCH_DELAY,
1526 ..Default::default()
1527 });
1528 let sub_dir = fs.root_path.join("sub");
1529 let file_path = sub_dir.join("file.txt");
1530 fs::create_dir(&sub_dir).unwrap();
1531 fs::write(&file_path, "initial").unwrap();
1532 backdate(&file_path);
1533
1534 DiskWatcher::start_watching(fs.clone()).await?;
1535 assert_eq!(fs.tracked_read_strongly_consistent(&file_path).await, 1);
1536
1537 let churn_start = Instant::now();
1538 let churn = std::thread::spawn({
1539 let file_path = file_path.clone();
1540 move || {
1541 let mut i = 0u64;
1542 while churn_start.elapsed() < CHURN {
1543 i += 1;
1544 fs::write(&file_path, i.to_string()).unwrap();
1545 std::thread::sleep(CHURN_INTERVAL);
1546 }
1547 }
1548 });
1549
1550 tokio::time::sleep(Duration::from_secs(1)).await;
1552 assert!(churn_start.elapsed() < CHURN);
1553 assert_eq!(
1554 fs.tracked_read_strongly_consistent(&file_path).await,
1555 1,
1556 "the batch was flushed while the tracked file was still being written"
1557 );
1558
1559 churn.join().unwrap();
1560 wait_for_rerun(&fs, &file_path, 1).await;
1561 assert_eq!(fs.tracked_read_strongly_consistent(&file_path).await, 2);
1562
1563 fs.watcher.stop_watching().await;
1564 anyhow::Ok(())
1565 })
1566 .await
1567 .unwrap();
1568 }
1569
1570 #[cfg(not(miri))]
1583 #[rstest]
1584 #[case::non_recursive_create(DiskWatcherRecursiveMode::NonRecursive, false)]
1585 #[case::non_recursive_rename(DiskWatcherRecursiveMode::NonRecursive, true)]
1586 #[case::recursive_create(DiskWatcherRecursiveMode::Recursive, false)]
1587 #[case::recursive_rename(DiskWatcherRecursiveMode::Recursive, true)]
1588 #[cfg_attr(feature = "ci", ignore = "timing-sensitive, flaky in CI")]
1589 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1590 async fn recreated_directory_is_watched_again(
1591 #[case] recursive_mode: DiskWatcherRecursiveMode,
1592 #[case] rename_into_place: bool,
1593 ) {
1594 const BATCH_DELAY: Duration = Duration::from_millis(50);
1595 const SETTLE: Duration = Duration::from_millis(500);
1598
1599 let tt = TurboTasks::new(TurboTasksBackend::new(
1600 BackendOptions::default(),
1601 noop_backing_storage(),
1602 ));
1603 tt.run_once(async move {
1604 let fs = MockFileSystem::new(DiskWatcherConfig {
1605 recursive_mode: Some(recursive_mode),
1606 batch_delay: BATCH_DELAY,
1607 extended_batch_delay_duration: BATCH_DELAY,
1608 ..Default::default()
1609 });
1610 let dir_a = fs.root_path.join("a");
1611 let dir_b = dir_a.join("b");
1612 let file_path = dir_b.join("c.txt");
1613 fs::create_dir_all(&dir_b)?;
1614 fs::write(&file_path, "initial")?;
1615 backdate(&file_path);
1616
1617 DiskWatcher::start_watching(fs.clone()).await?;
1618 assert_eq!(fs.tracked_read_strongly_consistent(&file_path).await, 1);
1620
1621 let gate = fs.read_gate.write().await;
1624 fs::remove_dir_all(&dir_a)?;
1625 tokio::time::sleep(SETTLE).await;
1626
1627 if rename_into_place {
1628 let staging = fs.root_path.join("staging");
1629 fs::create_dir_all(staging.join("b"))?;
1630 fs::write(staging.join("b").join("c.txt"), "recreated")?;
1631 fs::rename(&staging, &dir_a)?;
1632 } else {
1633 fs::create_dir_all(&dir_b)?;
1634 fs::write(&file_path, "recreated")?;
1635 }
1636 tokio::time::sleep(SETTLE).await;
1637
1638 drop(gate);
1640 assert_eq!(fs.tracked_read_strongly_consistent(&file_path).await, 2);
1641
1642 fs::write(&file_path, "modified")?;
1643 wait_for_rerun(&fs, &file_path, 2).await;
1644
1645 fs.watcher.stop_watching().await;
1646 anyhow::Ok(())
1647 })
1648 .await
1649 .unwrap();
1650 }
1651
1652 #[cfg(not(miri))]
1659 #[rstest]
1660 #[case::non_recursive(DiskWatcherRecursiveMode::NonRecursive)]
1661 #[case::recursive(DiskWatcherRecursiveMode::Recursive)]
1662 #[cfg_attr(feature = "ci", ignore = "timing-sensitive, flaky in CI")]
1663 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1664 async fn read_registered_before_watch_is_restored_is_invalidated(
1665 #[case] recursive_mode: DiskWatcherRecursiveMode,
1666 ) {
1667 const BATCH_DELAY: Duration = Duration::from_millis(300);
1668 const SETTLE: Duration = Duration::from_millis(800);
1669 const CHURN_INTERVAL: Duration = Duration::from_millis(50);
1670 const MAX_CHURN: Duration = Duration::from_secs(10);
1672
1673 let tt = TurboTasks::new(TurboTasksBackend::new(
1674 BackendOptions::default(),
1675 noop_backing_storage(),
1676 ));
1677 tt.run_once(async move {
1678 let fs = MockFileSystem::new(DiskWatcherConfig {
1679 recursive_mode: Some(recursive_mode),
1680 batch_delay: BATCH_DELAY,
1681 extended_batch_delay_duration: BATCH_DELAY,
1682 ..Default::default()
1683 });
1684 let dir_a = fs.root_path.join("a");
1685 let dir_b = dir_a.join("b");
1686 let file_path = dir_b.join("c.txt");
1687 let other_dir = fs.root_path.join("other");
1688 let other_path = other_dir.join("x.txt");
1689 fs::create_dir_all(&dir_b)?;
1690 fs::create_dir_all(&other_dir)?;
1691 fs::write(&file_path, "initial")?;
1692 fs::write(&other_path, "0")?;
1693 backdate(&file_path);
1694 backdate(&other_path);
1695
1696 DiskWatcher::start_watching(fs.clone()).await?;
1697 assert_eq!(fs.tracked_read_strongly_consistent(&file_path).await, 1);
1698 assert_eq!(fs.tracked_read_strongly_consistent(&other_path).await, 1);
1699
1700 let gate = fs.read_gate.write().await;
1703 fs::remove_dir_all(&dir_a)?;
1704 tokio::time::sleep(SETTLE).await;
1705
1706 let churn_done = Arc::new(std::sync::atomic::AtomicBool::new(false));
1709 let churn = std::thread::spawn({
1710 let other_path = other_path.clone();
1711 let churn_done = churn_done.clone();
1712 move || {
1713 let churn_start = Instant::now();
1714 let mut i = 0u64;
1715 while !churn_done.load(std::sync::atomic::Ordering::Relaxed)
1716 && churn_start.elapsed() < MAX_CHURN
1717 {
1718 i += 1;
1719 fs::write(&other_path, i.to_string()).unwrap();
1720 std::thread::sleep(CHURN_INTERVAL);
1721 }
1722 }
1723 });
1724 std::thread::sleep(CHURN_INTERVAL * 2);
1725
1726 fs::create_dir_all(&dir_b)?;
1727 fs::write(&file_path, "recreated")?;
1728 std::thread::sleep(CHURN_INTERVAL * 2);
1729
1730 drop(gate);
1732 assert_eq!(fs.tracked_read_strongly_consistent(&file_path).await, 2);
1733
1734 fs::write(&file_path, "modified")?;
1736
1737 churn_done.store(true, std::sync::atomic::Ordering::Relaxed);
1738 churn.join().unwrap();
1739 wait_for_rerun(&fs, &file_path, 2).await;
1740
1741 fs.watcher.stop_watching().await;
1742 anyhow::Ok(())
1743 })
1744 .await
1745 .unwrap();
1746 }
1747}