Skip to content

Commit d3aaa01

Browse files
committed
Add a global instance for the config manager
This global instance can be used as a single point where any actor can obtain a receiver to receive configuration updates. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 59b1184 commit d3aaa01

File tree

2 files changed

+154
-0
lines changed

2 files changed

+154
-0
lines changed

src/frequenz/sdk/config/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33

44
"""Configuration management."""
55

6+
from ._global import (
7+
get_config_manager,
8+
initialize_config_manager,
9+
shutdown_config_manager,
10+
)
611
from ._logging_actor import LoggerConfig, LoggingConfig, LoggingConfigUpdatingActor
712
from ._manager import ConfigManager
813
from ._managing_actor import ConfigManagingActor
@@ -14,5 +19,8 @@
1419
"LoggerConfig",
1520
"LoggingConfig",
1621
"LoggingConfigUpdatingActor",
22+
"get_config_manager",
23+
"initialize_config_manager",
1724
"load_config",
25+
"shutdown_config_manager",
1826
]

src/frequenz/sdk/config/_global.py

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

0 commit comments

Comments
 (0)