Skip to content

Commit 7954aa8

Browse files
committed
WIP
1 parent d49cddb commit 7954aa8

File tree

3 files changed

+626
-1
lines changed

3 files changed

+626
-1
lines changed
Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,88 @@
11
# License: MIT
22
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
33

4-
"""Read and update config variables."""
4+
"""Configuration management.
5+
6+
Example: App configuring the global config manager.
7+
```python
8+
import asyncio
9+
import dataclasses
10+
import sys
11+
12+
import marshmallow
13+
14+
from frequenz.channels import select, selected_from
15+
from frequenz.sdk.actor import Actor
16+
from frequenz.sdk.config import (
17+
initialize_config,
18+
config_manager,
19+
LoggingConfigUpdatingActor,
20+
ConfigManager,
21+
)
22+
23+
@dataclasses.dataclass
24+
class ActorConfig:
25+
name: str
26+
27+
class MyActor(Actor):
28+
def __init__(self, config: ActorConfig) -> None:
29+
self._config = config
30+
31+
async def _run(self) -> None:
32+
receiver = ...
33+
config_receiver = await config_manager().new_receiver(schema=ActorConfig)
34+
35+
async for selected in select(receiver, config_receiver):
36+
if selected_from(selected, receiver):
37+
...
38+
elif selected_from(selected, config_receiver):
39+
self._config = selected.message
40+
# Restart whatever is needed after a config update
41+
42+
43+
@dataclasses.dataclass
44+
class AppConfig:
45+
positive_int: int = dataclasses.field(
46+
default=42,
47+
metadata={"validate": marshmallow.validate.Range(min=0)},
48+
)
49+
my_actor: ActorConfig | None = None
50+
logging: LoggingConfig = LoggingConfig()
51+
52+
async def main() -> None:
53+
config_manager = initialize_config_manager(["config.toml"])
54+
try:
55+
# Receive the first configuration
56+
initial_config = await config_manager.new_receiver(schema=AppConfig,
57+
wait_for_first=True)
58+
except asyncio.TimeoutError:
59+
print("No configuration received in time")
60+
sys.exit(1)
61+
62+
actor = MyActor(ActorConfig(name=initial_config.my_actor))
63+
actor.start()
64+
await actor
65+
```
66+
"""
567

668
from ._actor import ConfigManagingActor
69+
from ._global import config_manager, initialize_config, shutdown_config_manager
770
from ._logging_config_updater import (
871
LoggerConfig,
972
LoggingConfig,
1073
LoggingConfigUpdatingActor,
1174
)
75+
from ._manager import ConfigManager
1276
from ._util import load_config
1377

1478
__all__ = [
79+
"ConfigManager",
1580
"ConfigManagingActor",
1681
"LoggingConfig",
1782
"LoggerConfig",
1883
"LoggingConfigUpdatingActor",
1984
"load_config",
85+
"config_manager",
86+
"initialize_config",
87+
"shutdown_config_manager",
2088
]

src/frequenz/sdk/config/_global.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Global config manager."""
5+
6+
import asyncio
7+
import logging
8+
import pathlib
9+
from collections import abc
10+
from collections.abc import Sequence
11+
from datetime import timedelta
12+
13+
from frequenz.channels.file_watcher import EventType
14+
15+
from ._manager import ConfigManager
16+
17+
_logger = logging.getLogger(__name__)
18+
19+
# pylint: disable=global-statement
20+
_CONFIG_MANAGER: ConfigManager | None = None
21+
"""Global instance of the ConfigManagingActor.
22+
23+
This is the only instance of the ConfigManagingActor that should be used in the
24+
entire application. It is created lazily on the first access and should be
25+
accessed via the `get_config_manager` function.
26+
"""
27+
28+
29+
def initialize_config( # pylint: disable=too-many-arguments
30+
config_paths: Sequence[pathlib.Path],
31+
/,
32+
*,
33+
event_types: abc.Set[EventType] = frozenset(EventType),
34+
force_polling: bool = True,
35+
name: str = "global",
36+
polling_interval: timedelta = timedelta(seconds=5),
37+
wait_for_first_timeout: timedelta = timedelta(seconds=5),
38+
) -> ConfigManager:
39+
"""Initialize the singleton instance of the ConfigManagingActor.
40+
41+
Args:
42+
config_paths: Paths to the TOML configuration files.
43+
event_types: The set of event types to monitor.
44+
force_polling: Whether to force file polling to check for changes.
45+
name: The name of the config manager.
46+
polling_interval: The interval to poll for changes. Only relevant if
47+
polling is enabled.
48+
wait_for_first_timeout: The timeout to use when waiting for the first
49+
configuration in
50+
[`new_receiver`][frequenz.sdk.config.ConfigManager.new_receiver] if
51+
`wait_for_first` is `True`.
52+
53+
Returns:
54+
The global instance of the ConfigManagingActor.
55+
56+
Raises:
57+
RuntimeError: If the config manager is already initialized.
58+
"""
59+
_logger.info(
60+
"Initializing config manager %s for %s with events=%s, force_polling=%s, "
61+
"polling_interval=%s, wait_for_first_timeout=%s",
62+
name,
63+
config_paths,
64+
[event.name for event in event_types],
65+
force_polling,
66+
polling_interval,
67+
wait_for_first_timeout,
68+
)
69+
70+
global _CONFIG_MANAGER
71+
if _CONFIG_MANAGER is not None:
72+
raise RuntimeError("Config already initialized")
73+
74+
_CONFIG_MANAGER = ConfigManager(
75+
config_paths,
76+
event_types=event_types,
77+
name=name,
78+
force_polling=force_polling,
79+
polling_interval=polling_interval,
80+
wait_for_first_timeout=wait_for_first_timeout,
81+
auto_start=True,
82+
)
83+
84+
return _CONFIG_MANAGER
85+
86+
87+
async def shutdown_config_manager(
88+
*,
89+
msg: str = "Config manager is shutting down",
90+
timeout: timedelta | None = timedelta(seconds=5),
91+
) -> None:
92+
"""Shutdown the global config manager.
93+
94+
This will stop the config manager and release any resources it holds.
95+
96+
Note:
97+
The config manager must be
98+
[initialized][frequenz.sdk.config.initialize_config] before calling this
99+
function.
100+
101+
Args:
102+
msg: The message to be passed to the tasks being cancelled.
103+
timeout: The maximum time to wait for the config manager to stop. If `None`,
104+
the method will only cancel the config manager actor and return immediately
105+
without awaiting at all (stopping might continue in the background). If the
106+
time is exceeded, an error will be logged.
107+
108+
Raises:
109+
RuntimeError: If the config manager is not initialized.
110+
"""
111+
_logger.info("Shutting down config manager (timeout=%s)...", timeout)
112+
113+
global _CONFIG_MANAGER
114+
if _CONFIG_MANAGER is None:
115+
raise RuntimeError("Config not initialized")
116+
117+
if timeout is None:
118+
_CONFIG_MANAGER.actor.cancel(msg)
119+
_logger.info(
120+
"Config manager cancelled, stopping might continue in the background."
121+
)
122+
else:
123+
try:
124+
async with asyncio.timeout(timeout.total_seconds()):
125+
await _CONFIG_MANAGER.actor.stop(msg)
126+
_logger.info("Config manager stopped.")
127+
except asyncio.TimeoutError:
128+
_logger.warning(
129+
"Config manager did not stop within %s seconds, it might continue "
130+
"stopping in the background",
131+
timeout,
132+
)
133+
134+
_CONFIG_MANAGER = None
135+
136+
137+
def config_manager() -> ConfigManager:
138+
"""Return the global config manager.
139+
140+
Note:
141+
The config manager must be
142+
[initialized][frequenz.sdk.config.initialize_config] before calling this
143+
function.
144+
145+
Returns:
146+
The global instance of the config manager.
147+
148+
Raises:
149+
RuntimeError: If the config manager is not initialized.
150+
"""
151+
if _CONFIG_MANAGER is None:
152+
raise RuntimeError("Config not initialized")
153+
return _CONFIG_MANAGER

0 commit comments

Comments
 (0)