Skip to content

Commit 1570347

Browse files
Enable polling in file watcher
`inotify` does not work reliably with network file systems (e.g., NFS, CIFS) commonly used in cloud environments. These file systems may not propagate file system events correctly, causing `inotify` to miss changes. To ensure consistent file monitoring across these environments, polling is enabled by default in FileWatcher. Signed-off-by: Daniel Zullo <[email protected]>
1 parent 10a8afa commit 1570347

File tree

4 files changed

+31
-5
lines changed

4 files changed

+31
-5
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@
1414

1515
## Bug Fixes
1616

17-
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
17+
- Fixed `FileWatcher` to report events correctly on network file systems like CIFS. File polling is now enabled by default to ensure reliable and consistent file monitoring in these environments.

src/frequenz/channels/file_watcher.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import pathlib
2323
from collections import abc
2424
from dataclasses import dataclass
25+
from datetime import timedelta
2526
from enum import Enum
2627

2728
from watchfiles import Change, awatch
@@ -127,13 +128,22 @@ def __init__(
127128
self,
128129
paths: list[pathlib.Path | str],
129130
event_types: abc.Iterable[EventType] = frozenset(EventType),
131+
*,
132+
force_polling: bool = True,
133+
polling_interval: timedelta = timedelta(seconds=1),
130134
) -> None:
131135
"""Initialize this file watcher.
132136
133137
Args:
134138
paths: The paths to watch for changes.
135139
event_types: The types of events to watch for. Defaults to watch for
136140
all event types.
141+
force_polling: Whether to explicitly force file polling to check for
142+
changes. Note that even if set to False, file polling will still
143+
be used as a fallback when the underlying file system does not
144+
support event-based notifications.
145+
polling_interval: The interval to poll for changes. Only relevant if
146+
polling is enabled.
137147
"""
138148
self.event_types: frozenset[EventType] = frozenset(event_types)
139149
"""The types of events to watch for."""
@@ -144,7 +154,11 @@ def __init__(
144154
for path in paths
145155
]
146156
self._awatch: abc.AsyncGenerator[set[FileChange], None] = awatch(
147-
*self._paths, stop_event=self._stop_event, watch_filter=self._filter_events
157+
*self._paths,
158+
stop_event=self._stop_event,
159+
watch_filter=self._filter_events,
160+
force_polling=force_polling,
161+
poll_delay_ms=int(polling_interval.total_seconds() * 1_000),
148162
)
149163
self._awatch_stopped_exc: Exception | None = None
150164
self._changes: set[FileChange] = set()

tests/test_file_watcher.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,11 @@ async def test_file_watcher_filter_events(
9797

9898
assert awatch_mock.mock_calls == [
9999
mock.call(
100-
pathlib.Path(good_path), stop_event=mock.ANY, watch_filter=filter_events
100+
pathlib.Path(good_path),
101+
stop_event=mock.ANY,
102+
watch_filter=filter_events,
103+
force_polling=True,
104+
poll_delay_ms=1_000,
101105
)
102106
]
103107
for event_type in EventType:

tests/test_file_watcher_integration.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ async def test_file_watcher_deletes(tmp_path: pathlib.Path) -> None:
5454
tmp_path: A tmp directory to run the file watcher on. Created by pytest.
5555
"""
5656
filename = tmp_path / "test-file"
57-
file_watcher = FileWatcher(paths=[str(tmp_path)], event_types={EventType.DELETE})
57+
file_watcher = FileWatcher(
58+
paths=[str(tmp_path)],
59+
event_types={EventType.DELETE},
60+
force_polling=False,
61+
)
5862
write_timer = Timer(timedelta(seconds=0.1), SkipMissedAndDrift())
5963
deletion_timer = Timer(timedelta(seconds=0.25), SkipMissedAndDrift())
6064

@@ -113,7 +117,11 @@ async def test_file_watcher_exit_iterator(tmp_path: pathlib.Path) -> None:
113117
number_of_writes = 0
114118
expected_number_of_writes = 3
115119

116-
file_watcher = FileWatcher(paths=[str(tmp_path)])
120+
file_watcher = FileWatcher(
121+
paths=[str(tmp_path)],
122+
force_polling=True,
123+
polling_interval=timedelta(seconds=0.05),
124+
)
117125
timer = Timer(timedelta(seconds=0.1), SkipMissedAndDrift())
118126

119127
async for selected in select(file_watcher, timer):

0 commit comments

Comments
 (0)