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
|
return
|
||||||
|
|
||||||
if change in (Change.added, Change.modified):
|
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(
|
await self._jobs.enqueue(
|
||||||
self.source_id,
|
self.source_id,
|
||||||
uri,
|
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
|
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 ---
|
# --- FSPoller end-to-end smoke ---
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue