Skip to content

Commit 2ec38f4

Browse files
authored
Improvements to the ConfigManagingActor (#565)
While working on the migration to the new `Actor` class I noticed several small issues with the `ConfigManagingActor`: - Use built-in types for typing - Improve documentation for `ConfigManagingActor` - Improve logging for `ConfigManagingActor` - Add missing type hints to attributes - Rename `conf_file` to `config_path` and accept `pathlib.Path` - Add clarification about watching the parent directory - Use match to process the file watcher events
2 parents aa75e4f + 3c725e7 commit 2ec38f4

File tree

3 files changed

+63
-42
lines changed

3 files changed

+63
-42
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
## Upgrading
88

99
- Upgrade to microgrid API v0.15.1. If you're using any of the lower level microgrid interfaces, you will need to upgrade your code.
10+
- The argument `conf_file` of the `ConfigManagingActor` constructor was renamed to `config_path`.
1011

1112
## New Features
1213

13-
<!-- Here goes the main new features and examples or instructions on how to use them -->
14+
- The `ConfigManagingActor` constructor now can accept a `pathlib.Path` as `config_path` too (before it accepted only a `str`).
1415

1516
## Bug Fixes
1617

src/frequenz/sdk/actor/_config_managing.py

Lines changed: 60 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
"""Read and update config variables."""
55

66
import logging
7-
import os
7+
import pathlib
88
import tomllib
99
from collections import abc
10-
from typing import Any, Dict
10+
from typing import Any, assert_never
1111

1212
from frequenz.channels import Sender
1313
from frequenz.channels.util import FileWatcher
@@ -20,73 +20,95 @@
2020

2121
@actor
2222
class ConfigManagingActor:
23-
"""
24-
Manages config variables.
23+
"""An actor that monitors a TOML configuration file for updates.
24+
25+
When the file is updated, the new configuration is sent, as a [`dict`][], to the
26+
`output` sender.
2527
26-
Config variables are read from file.
27-
Only single file can be read.
28-
If new file is read, then previous configs will be forgotten.
28+
When the actor is started, if a configuration file already exists, then it will be
29+
read and sent to the `output` sender before the actor starts monitoring the file
30+
for updates. This way users can rely on the actor to do the initial configuration
31+
reading too.
2932
"""
3033

3134
def __init__(
3235
self,
33-
conf_file: str,
36+
config_path: pathlib.Path | str,
3437
output: Sender[Config],
3538
event_types: abc.Set[FileWatcher.EventType] = frozenset(FileWatcher.EventType),
3639
) -> None:
37-
"""Read config variables from the file.
40+
"""Initialize this instance.
3841
3942
Args:
40-
conf_file: Path to file with config variables.
41-
output: Channel to publish updates to.
42-
event_types: Which types of events should update the config and
43-
trigger a notification.
43+
config_path: The path to the TOML file with the configuration.
44+
output: The sender to send the config to.
45+
event_types: The set of event types to monitor.
4446
"""
45-
self._conf_file: str = conf_file
46-
self._conf_dir: str = os.path.dirname(conf_file)
47-
self._file_watcher = FileWatcher(
48-
paths=[self._conf_dir], event_types=event_types
47+
self._config_path: pathlib.Path = (
48+
config_path
49+
if isinstance(config_path, pathlib.Path)
50+
else pathlib.Path(config_path)
4951
)
50-
self._output = output
51-
52-
def _read_config(self) -> Dict[str, Any]:
53-
"""Read the contents of the config file.
52+
# FileWatcher can't watch for non-existing files, so we need to watch for the
53+
# parent directory instead just in case a configuration file doesn't exist yet
54+
# or it is deleted and recreated again.
55+
self._file_watcher: FileWatcher = FileWatcher(
56+
paths=[self._config_path.parent], event_types=event_types
57+
)
58+
self._output: Sender[Config] = output
5459

55-
Raises:
56-
ValueError: if config file cannot be read.
60+
def _read_config(self) -> dict[str, Any]:
61+
"""Read the contents of the configuration file.
5762
5863
Returns:
5964
A dictionary containing configuration variables.
65+
66+
Raises:
67+
ValueError: If config file cannot be read.
6068
"""
6169
try:
62-
with open(self._conf_file, "rb") as toml_file:
70+
with self._config_path.open("rb") as toml_file:
6371
return tomllib.load(toml_file)
6472
except ValueError as err:
65-
logging.error("Can't read config file, err: %s", err)
73+
logging.error("%s: Can't read config file, err: %s", self, err)
6674
raise
6775

6876
async def send_config(self) -> None:
69-
"""Send config file using a broadcast channel."""
77+
"""Send the configuration to the output sender."""
7078
conf_vars = self._read_config()
7179
config = Config(conf_vars)
7280
await self._output.send(config)
7381

7482
async def run(self) -> None:
75-
"""Watch config file and update when modified.
76-
77-
At startup, the Config Manager sends the current config so that it
78-
can be cache in the Broadcast channel and served to receivers even if
79-
there hasn't been any change to the config file itself.
80-
"""
83+
"""Monitor for and send configuration file updates."""
8184
await self.send_config()
8285

8386
async for event in self._file_watcher:
84-
if event.type != FileWatcher.EventType.DELETE:
85-
if str(event.path) == self._conf_file:
87+
# Since we are watching the whole parent directory, we need to make sure
88+
# we only react to events related to the configuration file.
89+
if event.path != self._config_path:
90+
continue
91+
92+
match event.type:
93+
case FileWatcher.EventType.CREATE:
8694
_logger.info(
87-
"Update configs, because file %s was modified.",
88-
self._conf_file,
95+
"%s: The configuration file %s was created, sending new config...",
96+
self,
97+
self._config_path,
8998
)
9099
await self.send_config()
91-
92-
_logger.debug("ConfigManager stopped.")
100+
case FileWatcher.EventType.MODIFY:
101+
_logger.info(
102+
"%s: The configuration file %s was modified, sending update...",
103+
self,
104+
self._config_path,
105+
)
106+
await self.send_config()
107+
case FileWatcher.EventType.DELETE:
108+
_logger.info(
109+
"%s: The configuration file %s was deleted, ignoring...",
110+
self,
111+
self._config_path,
112+
)
113+
case _:
114+
assert_never(event.type)

tests/actor/test_config_manager.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,7 @@ async def test_update(self, config_file: pathlib.Path) -> None:
8080
config_channel: Broadcast[Config] = Broadcast(
8181
"Config Channel", resend_latest=True
8282
)
83-
_config_manager = ConfigManagingActor(
84-
conf_file=str(config_file), output=config_channel.new_sender()
85-
)
83+
_config_manager = ConfigManagingActor(config_file, config_channel.new_sender())
8684

8785
config_receiver = config_channel.new_receiver()
8886

0 commit comments

Comments
 (0)