Skip to content

Rewatch a directory that was deleted and recreated - #61372

Open
marcalc wants to merge 2 commits into
zed-industries:mainfrom
marcalc:fix-recreated-directory-watch
Open

Rewatch a directory that was deleted and recreated#61372
marcalc wants to merge 2 commits into
zed-industries:mainfrom
marcalc:fix-recreated-directory-watch

Conversation

@marcalc

@marcalc marcalc commented Jul 21, 2026

Copy link
Copy Markdown

Objective

Addresses the delete-then-recreate cause reported under #53901 (@Akizay's reproduction) — a different root cause from the macOS FSEvents fd-saturation case in #61072, so this intentionally does not use a closing keyword.

When a directory is deleted and quickly recreated while it is being repopulated — e.g. a build or test step that clears an output directory and then rewrites it — most of the newly-written files never appear in the project panel (nor do buffers/git status reflect them) until the workspace is reloaded. @Akizay reproduced this on Linux with a small Python script that rmtrees a directory and immediately recreates it with 20 files + 20 subdirectories; only the first one or two show up.

Root cause

Proven empirically on Linux/inotify (see Testing): the failure is a stale watch registration in the fs crate, not fd budgets and not an LSP.

  1. Each watched directory has its own inotify watch (inotify is not recursive). FsWatcher tracks these in a registrations map keyed by path.
  2. When the directory is deleted, the kernel silently invalidates its inotify watch (IN_IGNORED), but FsWatcher's registrations entry lingers, because the delete and the recreate coalesce within one FS_WATCH_LATENCY (100ms) debounce window, so the worktree processes the path as still-present (metadata exists again) and never unwatches it.
  3. When the worktree rescans the recreated directory and calls watcher.add(path), add_existing_path finds the lingering registration and short-circuits with "path to watch is already watched" — so the new inode is never watched. Every file written into it afterwards produces no FS event and is lost until an unrelated rescan.
  4. Separately, scan_dir established the directory's watch only after enumerating its contents (read_dir), leaving a TOCTOU window even when a fresh watch is created.

This matches @Akizay's observation that inserting a sleep longer than FS_WATCH_LATENCY between the delete and the recreate fixes it: with the delete processed in its own window, the worktree unwatches the path, so the later add re-registers cleanly.

Solution

  • fs: record each registration's inode. In add_existing_path, only treat a path as already-watched when the inode is unchanged; if it changed (the directory was replaced), drop the stale registration and re-register a fresh watch on the new inode. Unix-only (inodes are unavailable elsewhere, and macOS/Windows use recursive watches so the recreated subdirectory is already covered by the root watch).
  • worktree: in scan_dir, establish the directory's watch before read_dir, so a child created after enumeration but before the watch would otherwise be active is still delivered as an event rather than lost.

Known limitation: if the OS reuses the exact same inode number for the recreated directory, the inode comparison can't detect the replacement. In practice a freshly recreated directory gets a new inode (confirmed in the test), and this is strictly better than the previous behavior, which never recovered.

Disclosure per the AI policy: I investigated and developed this with an LLM agent (Claude Code / Opus 4.8), reviewing and directing each step; the measurements below are from real runs I can defend in review.

Testing

New integration test test_rapid_delete_recreate_dir_shows_all_children (worktree, RealFs) reproduces @Akizay's scenario: populate test_results with 20 files + 20 dirs, then remove it and immediately recreate + repopulate it with each child ~10ms apart (so the burst spans more than FS_WATCH_LATENCY), and assert all 40 entries are present.

Run on real Linux inotify (Debian 12 aarch64, in Docker, since the bug can't reproduce on macOS FSEvents which is recursive):

  • Before the fix: 38 of 40 entries missing (deterministic).
  • After the fix: 0 missing; test passes in ~2.3s.
  • cargo test -p worktree -p fs: 61 + 18 passed, 0 failed (no regressions).
  • cargo clippy -p fs -p worktree --tests and cargo fmt --check: clean.

Self-Review Checklist:

  • I've reviewed my own diff for quality, security, and reliability
  • Tests cover the new/changed behavior
  • Performance impact has been considered and is acceptable

Release Notes:

  • Fixed files created in a directory that was deleted and immediately recreated (for example by a build or test step that clears and repopulates an output directory) not appearing until the workspace was reloaded

A directory that is deleted and quickly recreated while it is being
repopulated (e.g. a build or test step that clears then rewrites an
output directory) only reflected the entries that existed at the instant
it was rescanned; later files never appeared until the workspace was
reloaded.

The delete and recreate coalesce within one FS_WATCH_LATENCY window, so
the worktree treats the path as still-present and never unwatches it.
inotify has already invalidated the kernel watch on the old inode, but
FsWatcher's registration lingers, so the rescan's watcher.add is
short-circuited as already-watched and the new inode is never watched.

Track each registration's inode and re-register when it changes, and
establish a scanned directory's watch before enumerating its contents.

See zed-industries#53901.
@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label Jul 21, 2026
@zed-community-bot zed-community-bot Bot added the first contribution the author's first pull request to Zed. NOTE: the label application is automated via github actions label Jul 21, 2026
@ChristopherBiscardi ChristopherBiscardi added the area:project panel Feedback for files tree view label Jul 22, 2026
@SomeoneToIgnore SomeoneToIgnore added the area:scanning Worktree scanning related PRs. label Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:project panel Feedback for files tree view area:scanning Worktree scanning related PRs. cla-signed The user has signed the Contributor License Agreement first contribution the author's first pull request to Zed. NOTE: the label application is automated via github actions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants