Fix discover() crash when file is deleted during stat()

A file deleted between os.walk() and path.stat() raises
FileNotFoundError, which propagated uncaught and failed the entire
discover() sweep. With enough failures this trips the circuit
breaker, silencing the poller.

Catch FileNotFoundError around the stat() call and skip the file.
The next sweep (or watchfiles) will emit the DELETE event.
This commit is contained in:
Chris McDonough 2026-06-01 07:29:51 -04:00
parent d5e5733f67
commit 7d80eb4f83
2 changed files with 25 additions and 1 deletions

View file

@ -139,7 +139,12 @@ class FSSource:
if not self.filter.include_file(str(path)):
continue
uri = path.as_uri()
revision = str(path.stat().st_mtime_ns)
try:
revision = str(path.stat().st_mtime_ns)
except FileNotFoundError:
# File was deleted between the os.walk() and stat() call.
# Skip it — the next sweep (or watchfiles) will emit DELETE.
continue
seen.add(uri)
previous = snapshot.get(uri)
kind = (

View file

@ -131,6 +131,25 @@ async def test_fs_source_discover_respects_extension_filter(fs_root: Path):
assert (fs_root / "b.txt").as_uri() not in uris
@pytest.mark.asyncio
async def test_fs_source_discover_skips_file_deleted_during_stat(fs_root: Path):
"""A file deleted between os.walk() and stat() should be silently
skipped instead of crashing the entire discover() sweep."""
src = FSSource(root=fs_root, supported_extensions=[".md", ".txt"])
events = []
async for event in src.discover(since=None):
events.append(event)
# Delete a file mid-iteration so the next stat() hits a missing file.
victim = fs_root / "b.txt"
if victim.exists():
victim.unlink()
uris = {e.uri for e in events}
# a.md and sub/c.md should still appear; b.txt may or may not depending
# on iteration order, but the key assertion is no exception was raised.
assert (fs_root / "a.md").as_uri() in uris
assert (fs_root / "sub" / "c.md").as_uri() in uris
@pytest.mark.asyncio
async def test_fs_source_discover_respects_ignore_patterns(fs_root: Path):
src = FSSource(