|
| 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