Fix watch loop crash when file is deleted before stat() in _handle_watch_change
The expression `str(path.stat().st_mtime_ns) if path.exists() else None` has a TOCTOU race: the file can be deleted between exists() and stat(). The resulting FileNotFoundError propagates up to _watch_loop's except handler, which records a breaker failure and terminates the loop — no more push events are processed until restart. Replace with a try/except around stat() and return early on FileNotFoundError. The deletion event from watchfiles will handle cleanup.
This commit is contained in:
parent
d5e5733f67
commit
42922cd2bd
2 changed files with 22 additions and 1 deletions
|
|
@ -131,7 +131,12 @@ class FSPoller(BasePoller):
|
|||
return
|
||||
|
||||
if change in (Change.added, Change.modified):
|
||||
revision = str(path.stat().st_mtime_ns) if path.exists() else None
|
||||
try:
|
||||
revision = str(path.stat().st_mtime_ns)
|
||||
except FileNotFoundError:
|
||||
# File was deleted between the watchfiles event and our
|
||||
# stat() call. Skip — the deletion event will handle it.
|
||||
return
|
||||
await self._jobs.enqueue(
|
||||
self.source_id,
|
||||
uri,
|
||||
|
|
|
|||
|
|
@ -484,6 +484,22 @@ async def test_watch_deleted_then_added_enqueues_upsert(tmp_path, jobs, sync):
|
|||
assert queued[0].op is JobOp.UPSERT
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_watch_added_file_deleted_before_stat_does_not_crash(tmp_path, jobs, sync):
|
||||
"""If a file is deleted between the watchfiles event and the stat()
|
||||
call, the handler should return silently instead of raising
|
||||
FileNotFoundError and killing the watch loop."""
|
||||
from watchfiles import Change
|
||||
|
||||
poller = _fs_poller(tmp_path, jobs, sync)
|
||||
missing = tmp_path / "vanished.md"
|
||||
# File doesn't exist — simulate Change.added arriving after deletion.
|
||||
await poller._handle_watch_change(Change.added, missing)
|
||||
|
||||
# No job should be enqueued, and no exception should have propagated.
|
||||
assert await jobs.list_jobs(source_id="local") == []
|
||||
|
||||
|
||||
# --- FSPoller end-to-end smoke ---
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue