Rewatch a directory that was deleted and recreated - #61372
Open
marcalc wants to merge 2 commits into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
fscrate, not fd budgets and not an LSP.FsWatchertracks these in aregistrationsmap keyed by path.IN_IGNORED), butFsWatcher'sregistrationsentry lingers, because the delete and the recreate coalesce within oneFS_WATCH_LATENCY(100ms) debounce window, so the worktree processes the path as still-present (metadata exists again) and never unwatches it.watcher.add(path),add_existing_pathfinds 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.scan_direstablished 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
sleeplonger thanFS_WATCH_LATENCYbetween the delete and the recreate fixes it: with the delete processed in its own window, the worktree unwatches the path, so the lateraddre-registers cleanly.Solution
fs: record each registration's inode. Inadd_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: inscan_dir, establish the directory's watch beforeread_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: populatetest_resultswith 20 files + 20 dirs, then remove it and immediately recreate + repopulate it with each child ~10ms apart (so the burst spans more thanFS_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):
cargo test -p worktree -p fs: 61 + 18 passed, 0 failed (no regressions).cargo clippy -p fs -p worktree --testsandcargo fmt --check: clean.Self-Review Checklist:
Release Notes: