Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/containers/test-installation/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# This Dockerfile is used to test the installation of the python package in
# multiple platforms in the CI. It is not used to build the package itself.

FROM --platform=${TARGETPLATFORM} python:3.11-slim
FROM python:3.11-slim

RUN apt-get update -y && \
apt-get install --no-install-recommends -y \
Expand Down
10 changes: 9 additions & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
# Frequenz channels Release Notes

## Upgrading

- `FileWatcher`: The file polling mechanism is now forced by default. This provides reliable and consistent file monitoring on network file systems (e.g., CIFS). However, it may have a performance impact on local file systems or when monitoring a large number of files.
- To disable file polling, set the `force_polling` parameter to `False`.
- The `polling_interval` parameter defines the interval for polling changes. This is relevant only when polling is enabled and defaults to 1 second.

## New Features

- `Timer.reset()` now supports setting the interval and will restart the timer with the new interval.

## Bug Fixes

- `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.
- `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.
- Reports file events correctly on network file systems like CIFS.

- `Timer.stop()` and `Timer.reset()` now immediately stop the timer if it is running. Before this fix, the timer would continue to run until the next interval.
16 changes: 15 additions & 1 deletion src/frequenz/channels/file_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import pathlib
from collections import abc
from dataclasses import dataclass
from datetime import timedelta
from enum import Enum

from watchfiles import Change, awatch
Expand Down Expand Up @@ -127,13 +128,22 @@ def __init__(
self,
paths: list[pathlib.Path | str],
event_types: abc.Iterable[EventType] = frozenset(EventType),
*,
force_polling: bool = True,
polling_interval: timedelta = timedelta(seconds=1),
) -> None:
"""Initialize this file watcher.

Args:
paths: The paths to watch for changes.
event_types: The types of events to watch for. Defaults to watch for
all event types.
force_polling: Whether to explicitly force file polling to check for
changes. Note that even if set to False, file polling will still
be used as a fallback when the underlying file system does not
support event-based notifications.
polling_interval: The interval to poll for changes. Only relevant if
polling is enabled.
"""
self.event_types: frozenset[EventType] = frozenset(event_types)
"""The types of events to watch for."""
Expand All @@ -144,7 +154,11 @@ def __init__(
for path in paths
]
self._awatch: abc.AsyncGenerator[set[FileChange], None] = awatch(
*self._paths, stop_event=self._stop_event, watch_filter=self._filter_events
*self._paths,
stop_event=self._stop_event,
watch_filter=self._filter_events,
force_polling=force_polling,
poll_delay_ms=int(polling_interval.total_seconds() * 1_000),
)
self._awatch_stopped_exc: Exception | None = None
self._changes: set[FileChange] = set()
Expand Down
6 changes: 5 additions & 1 deletion tests/test_file_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,11 @@ async def test_file_watcher_filter_events(

assert awatch_mock.mock_calls == [
mock.call(
pathlib.Path(good_path), stop_event=mock.ANY, watch_filter=filter_events
pathlib.Path(good_path),
stop_event=mock.ANY,
watch_filter=filter_events,
force_polling=True,
poll_delay_ms=1_000,
)
]
for event_type in EventType:
Expand Down
12 changes: 10 additions & 2 deletions tests/test_file_watcher_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ async def test_file_watcher_deletes(tmp_path: pathlib.Path) -> None:
tmp_path: A tmp directory to run the file watcher on. Created by pytest.
"""
filename = tmp_path / "test-file"
file_watcher = FileWatcher(paths=[str(tmp_path)], event_types={EventType.DELETE})
file_watcher = FileWatcher(
paths=[str(tmp_path)],
event_types={EventType.DELETE},
force_polling=False,
)
write_timer = Timer(timedelta(seconds=0.1), SkipMissedAndDrift())
deletion_timer = Timer(timedelta(seconds=0.25), SkipMissedAndDrift())

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

file_watcher = FileWatcher(paths=[str(tmp_path)])
file_watcher = FileWatcher(
paths=[str(tmp_path)],
force_polling=True,
polling_interval=timedelta(seconds=0.05),
)
timer = Timer(timedelta(seconds=0.1), SkipMissedAndDrift())

async for selected in select(file_watcher, timer):
Expand Down
Loading