Skip to content

Commit a8df3f3

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 a8df3f3

File tree

4 files changed

+25
-5
lines changed

4 files changed

+25
-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: 12 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,19 @@ def __init__(
127128
self,
128129
paths: list[pathlib.Path | str],
129130
event_types: abc.Iterable[EventType] = frozenset(EventType),
131+
*,
132+
enable_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+
enable_polling: Whether to enable polling.
142+
polling_interval: The interval to poll for changes. Only relevant if
143+
polling is enabled.
137144
"""
138145
self.event_types: frozenset[EventType] = frozenset(event_types)
139146
"""The types of events to watch for."""
@@ -144,7 +151,11 @@ def __init__(
144151
for path in paths
145152
]
146153
self._awatch: abc.AsyncGenerator[set[FileChange], None] = awatch(
147-
*self._paths, stop_event=self._stop_event, watch_filter=self._filter_events
154+
*self._paths,
155+
stop_event=self._stop_event,
156+
watch_filter=self._filter_events,
157+
force_polling=enable_polling,
158+
poll_delay_ms=int(polling_interval.total_seconds() * 1_000),
148159
)
149160
self._awatch_stopped_exc: Exception | None = None
150161
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=1000,
101105
)
102106
]
103107
for event_type in EventType:

tests/test_file_watcher_integration.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,12 @@ 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+
enable_polling=True,
61+
polling_interval=timedelta(seconds=0.11),
62+
)
5863
write_timer = Timer(timedelta(seconds=0.1), SkipMissedAndDrift())
5964
deletion_timer = Timer(timedelta(seconds=0.25), SkipMissedAndDrift())
6065

@@ -113,7 +118,7 @@ async def test_file_watcher_exit_iterator(tmp_path: pathlib.Path) -> None:
113118
number_of_writes = 0
114119
expected_number_of_writes = 3
115120

116-
file_watcher = FileWatcher(paths=[str(tmp_path)])
121+
file_watcher = FileWatcher(paths=[str(tmp_path)], enable_polling=False)
117122
timer = Timer(timedelta(seconds=0.1), SkipMissedAndDrift())
118123

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

0 commit comments

Comments
 (0)