Skip to content

Commit d71ba0a

Browse files
committed
test: Verbose stress test output
1 parent 9f0dd7a commit d71ba0a

14 files changed

Lines changed: 770 additions & 791 deletions

.github/workflows/rust.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ jobs:
1919
strategy:
2020
matrix:
2121
os: [ubuntu-latest, macos-latest]
22+
# Guard against deadlocks in the shared-picker / watcher teardown
23+
# path: a stuck test would otherwise consume a full 6h CI slot.
24+
timeout-minutes: 10
2225
steps:
2326
- uses: actions/checkout@v5
2427

crates/fff-c/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -896,9 +896,9 @@ pub unsafe extern "C" fn fff_restart_index(
896896
};
897897

898898
let (warmup_caches, content_indexing, watch, mode) = if let Some(mut picker) = guard.take() {
899-
let warmup = picker.need_enable_mmap_cache();
900-
let enable_content_indexing = picker.need_enable_content_indexing();
901-
let watch = picker.need_watch();
899+
let warmup = picker.has_mmap_cache();
900+
let enable_content_indexing = picker.has_content_indexing();
901+
let watch = picker.has_watcher();
902902
let mode = picker.mode();
903903

904904
picker.stop_background_monitor();

crates/fff-core/src/background_watcher.rs

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ impl BackgroundWatcher {
8282
let (watch_tx, watch_rx) = mpsc::channel::<PathBuf>();
8383
let watch_tx_for_debouncer = watch_tx.clone();
8484

85-
let owner_weak_picker = shared_picker.downgrade();
85+
let owner_weak_picker = shared_picker.weaken();
8686
let owner_frecency = shared_frecency.clone();
8787
let owner_git_workdir = git_workdir.clone();
8888

@@ -100,6 +100,9 @@ impl BackgroundWatcher {
100100

101101
// debouncer is shared with the owner thread, once it's dropped the thread is closed
102102
let debouncer = Arc::new(Mutex::new(Some(debouncer)));
103+
// Only the Linux per-dir-watch branch needs this clone; on other
104+
// platforms the owner thread never touches the debouncer.
105+
#[cfg(target_os = "linux")]
103106
let owner_debouncer = Arc::clone(&debouncer);
104107

105108
let owner_thread = std::thread::Builder::new()
@@ -111,16 +114,40 @@ impl BackgroundWatcher {
111114
break;
112115
};
113116

114-
if let Some(debouncer) = owner_debouncer.lock().as_mut() {
115-
process_watch_request(
116-
debouncer,
117-
&dir,
118-
&strong_picker,
119-
&owner_frecency,
120-
&owner_git_workdir,
121-
);
117+
// Only inotify (Linux) has no kernel-level recursion, so
118+
// it's the only platform that needs a per-subdir watch to
119+
// be registered at runtime. macOS FSEvents and Windows
120+
// ReadDirectoryChangesW are already watching recursively
121+
// from the base path (see `create_debouncer`), and
122+
// registering a second overlapping stream there produces
123+
// duplicate/out-of-order events.
124+
#[cfg(target_os = "linux")]
125+
{
126+
// Register the new directory with the debouncer, then
127+
// drop the mutex BEFORE doing picker-side work — see
128+
// the comment on `BackgroundWatcher::stop` for the
129+
// lock-ordering rationale.
130+
let mut guard = owner_debouncer.lock();
131+
let Some(debouncer) = guard.as_mut() else {
132+
break;
133+
};
134+
135+
if let Err(e) = debouncer.watch(&dir, RecursiveMode::NonRecursive) {
136+
warn!(
137+
?e,
138+
dir = %dir.display(),
139+
"Failed to init watcher for new directory"
140+
);
141+
}
122142
}
123143

144+
track_files_from_new_directories(
145+
&dir,
146+
&strong_picker,
147+
&owner_frecency,
148+
&owner_git_workdir,
149+
);
150+
124151
// Transient strong ref drops here, back
125152
// to weak-only before the next `recv()`.
126153
}
@@ -165,7 +192,7 @@ impl BackgroundWatcher {
165192
// Capture a weak handle instead and upgrade per-batch.
166193
let git_workdir_for_handler = git_workdir.clone();
167194
let shared_picker_for_watching = shared_picker.clone();
168-
let event_picker = shared_picker.downgrade();
195+
let event_picker = shared_picker.weaken();
169196
let mut debouncer = new_debouncer_opt(
170197
DEBOUNCE_TIMEOUT,
171198
Some(DEBOUNCE_TIMEOUT / 2), // tick rate for the event span
@@ -313,10 +340,30 @@ impl BackgroundWatcher {
313340
Ok(debouncer)
314341
}
315342

343+
/// Signal the watcher to shut down without blocking on its worker
344+
/// threads. Safe to call from any context, including while holding
345+
/// the [`SharedFilePicker`] write lock.
346+
///
347+
/// Both the debouncer's internal event loop and our owner thread
348+
/// may call `SharedFilePicker::write()` inside their handlers. A
349+
/// blocking join here would deadlock against a caller that already
350+
/// holds that lock (e.g. `stop_background_monitor` under a
351+
/// `shared_picker.write()` guard). Instead we:
352+
///
353+
/// * drop the `watch_tx` Sender — the owner thread's
354+
/// `watch_rx.recv()` returns `Err` and the thread exits at
355+
/// its next `recv`.
356+
/// * call `debouncer.stop_nonblocking()` — signals the debouncer
357+
/// event loop to exit on its next tick and drops the watcher,
358+
/// closing the FSEvent / inotify / ReadDirectoryChangesW stream.
359+
/// * detach both `JoinHandle`s.
360+
///
361+
/// In-flight handler invocations finish on their own (at most one
362+
/// more batch) once the caller releases any locks they hold.
316363
pub fn stop(&mut self) {
317364
self.watch_tx.take();
318365
if let Some(debouncer) = self.debouncer.lock().take() {
319-
debouncer.stop();
366+
debouncer.stop_nonblocking();
320367
}
321368

322369
self.owner_thread.take();
@@ -346,29 +393,6 @@ impl Drop for BackgroundWatcher {
346393
}
347394
}
348395

349-
/// Handle a single `watch_rx` entry: register a non-recursive watch on
350-
/// the directory and inject any files that already exist inside it.
351-
///
352-
/// Extracted so both the `recv_timeout` wakeup branch and the inner
353-
/// `try_recv` drain loop in the owner thread share one implementation.
354-
fn process_watch_request(
355-
debouncer: &mut Debouncer,
356-
dir: &Path,
357-
shared_picker: &SharedFilePicker,
358-
shared_frecency: &SharedFrecency,
359-
git_workdir: &Option<PathBuf>,
360-
) {
361-
// macos uses a single watcher per file directory which is
362-
if !cfg!(target_os = "macos") {
363-
match debouncer.watch(dir, RecursiveMode::NonRecursive) {
364-
Ok(()) => debug!("Added watch for new directory: {}", dir.display()),
365-
Err(e) => warn!("Failed to watch new directory {}: {}", dir.display(), e),
366-
}
367-
}
368-
369-
track_files_from_new_directories(dir, shared_picker, shared_frecency, git_workdir);
370-
}
371-
372396
#[tracing::instrument(name = "fs_events", skip(events, shared_picker, shared_frecency), level = Level::DEBUG)]
373397
fn handle_debounced_events(
374398
events: Vec<DebouncedEvent>,

0 commit comments

Comments
 (0)