Skip to content

Commit 195a93c

Browse files
Fix ready() method in FileWatcher (#318)
The ready() method now returns False when an exception is set. Fixes #317
2 parents 6d1769f + 1d9bb89 commit 195a93c

File tree

3 files changed

+45
-2
lines changed

3 files changed

+45
-2
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+
- `FileWatcher`: Fixed `ready()` method to return False when an error occurs. Before this fix, `select()` (and other code using `ready()`) never detected the `FileWatcher` was stopped and the `select()` loop was continuously waking up to inform the receiver was ready.

src/frequenz/channels/file_watcher.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ class FileWatcher(Receiver[Event]):
7070
the [`path`][frequenz.channels.file_watcher.Event.path] where the change was
7171
observed.
7272
73+
Note:
74+
The owner of the [`FileWatcher`][frequenz.channels.file_watcher.FileWatcher]
75+
receiver is responsible for recreating the `FileWatcher` after it has been
76+
cancelled or stopped.
77+
For example, if a [`Task`][asyncio.Task] uses an asynchronous iterator to consume
78+
events from the `FileWatcher` and the task is cancelled, the `FileWatcher` will
79+
also stop. Therefore, the same `FileWatcher` instance cannot be reused for a new
80+
task to consume events. In this case, a new FileWatcher instance must be created.
81+
7382
# Event Types
7483
7584
The following event types are available:
@@ -185,6 +194,7 @@ async def ready(self) -> bool:
185194
self._changes = await anext(self._awatch)
186195
except StopAsyncIteration as err:
187196
self._awatch_stopped_exc = err
197+
return False
188198

189199
return True
190200

tests/test_file_watcher_integration.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import pytest
1111

12-
from frequenz.channels import select, selected_from
12+
from frequenz.channels import ReceiverStoppedError, select, selected_from
1313
from frequenz.channels.file_watcher import Event, EventType, FileWatcher
1414
from frequenz.channels.timer import SkipMissedAndDrift, Timer
1515

@@ -99,3 +99,36 @@ async def test_file_watcher_deletes(tmp_path: pathlib.Path) -> None:
9999
# Can be more because the watcher could take some time to trigger
100100
assert number_of_write >= 3
101101
assert number_of_events == 2
102+
103+
104+
@pytest.mark.integration
105+
async def test_file_watcher_exit_iterator(tmp_path: pathlib.Path) -> None:
106+
"""Test breaking the file watcher iterator.
107+
108+
Args:
109+
tmp_path: A tmp directory to run the file watcher on. Created by pytest.
110+
"""
111+
filename = tmp_path / "test-file"
112+
113+
number_of_writes = 0
114+
expected_number_of_writes = 3
115+
116+
file_watcher = FileWatcher(paths=[str(tmp_path)])
117+
timer = Timer(timedelta(seconds=0.1), SkipMissedAndDrift())
118+
119+
async for selected in select(file_watcher, timer):
120+
if selected_from(selected, timer):
121+
filename.write_text(f"{selected.message}")
122+
elif selected_from(selected, file_watcher):
123+
number_of_writes += 1
124+
if number_of_writes == expected_number_of_writes:
125+
file_watcher._stop_event.set() # pylint: disable=protected-access
126+
break
127+
128+
ready = await file_watcher.ready()
129+
assert ready is False
130+
131+
with pytest.raises(ReceiverStoppedError):
132+
file_watcher.consume()
133+
134+
assert number_of_writes == expected_number_of_writes

0 commit comments

Comments
 (0)