From 5a84c3d1b3ef8fa12bb0cbcee68924a1188afb2d Mon Sep 17 00:00:00 2001 From: Elzbieta Kotulska Date: Thu, 21 Nov 2024 11:38:28 +0100 Subject: [PATCH] Change FileWatcher to ignore watchfile heartbeat timeout. The rust_timeout parameter in awatch() controls how long the underlying Rust code waits for file changes before checking if it should stop. In result in non-windows system missleading debug log was printed every 5 seconds: ` DEBUG watchfiles.main:267: rust notify timeout, continuing` Set `rust_timeout` to sys.maxsize - 1 as we don't need frequent timeouts for proper interrupt handling on non-Windows systems. yield_on_timeout was changed to True to yield empty set on this timeout error instead of printing debug log. This is safe because: - Unix systems use native file system events (inotify/FSEvents) - Process interruption works reliably without shorter timeouts - The large timeout value doesn't affect file change detection performance --- src/frequenz/channels/file_watcher.py | 20 +++++++++++++++----- tests/test_file_watcher.py | 3 +++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/frequenz/channels/file_watcher.py b/src/frequenz/channels/file_watcher.py index a4dc3074..92b57e21 100644 --- a/src/frequenz/channels/file_watcher.py +++ b/src/frequenz/channels/file_watcher.py @@ -20,6 +20,7 @@ import asyncio import pathlib +import sys from collections import abc from dataclasses import dataclass from datetime import timedelta @@ -159,6 +160,10 @@ def __init__( watch_filter=self._filter_events, force_polling=force_polling, poll_delay_ms=int(polling_interval.total_seconds() * 1_000), + # Two arguments below are ok for non-Windows systems. + # For more details, refer awatch documentation. + rust_timeout=sys.maxsize - 1, + yield_on_timeout=True, ) self._awatch_stopped_exc: Exception | None = None self._changes: set[FileChange] = set() @@ -204,11 +209,16 @@ async def ready(self) -> bool: if self._awatch_stopped_exc is not None: return False - try: - self._changes = await anext(self._awatch) - except StopAsyncIteration as err: - self._awatch_stopped_exc = err - return False + # awatch will yield an empty set if rust notify timeout is reached. + # This is safety heartbeat mechanism for Windows. + # But on non-Windows systems, we don't need it, so we can ignore it. + # For more details, refer awatch documentation. + while len(self._changes) == 0: + try: + self._changes = await anext(self._awatch) + except StopAsyncIteration as err: + self._awatch_stopped_exc = err + return False return True diff --git a/tests/test_file_watcher.py b/tests/test_file_watcher.py index 720de807..6dcb1219 100644 --- a/tests/test_file_watcher.py +++ b/tests/test_file_watcher.py @@ -5,6 +5,7 @@ import pathlib +import sys from collections.abc import AsyncGenerator, Iterator, Sequence from typing import Any from unittest import mock @@ -102,6 +103,8 @@ async def test_file_watcher_filter_events( watch_filter=filter_events, force_polling=True, poll_delay_ms=1_000, + rust_timeout=sys.maxsize - 1, + yield_on_timeout=True, ) ] for event_type in EventType: