Skip to content

Commit 7d086e3

Browse files
committed
Subscribe to specific configuration keys
This forces a better configuration structure, by always having separate sections for separate entities, and avoids spurious updates and noise in the received configuration for other parties. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent bc3dfd5 commit 7d086e3

File tree

2 files changed

+88
-10
lines changed

2 files changed

+88
-10
lines changed

src/frequenz/sdk/config/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
"""Configuration management."""
55

66
from ._logging_actor import LoggerConfig, LoggingConfig, LoggingConfigUpdatingActor
7-
from ._manager import ConfigManager
7+
from ._manager import ConfigManager, InvalidValueForKeyError
88
from ._managing_actor import ConfigManagingActor
99
from ._util import load_config
1010

1111
__all__ = [
1212
"ConfigManager",
1313
"ConfigManagingActor",
14+
"InvalidValueForKeyError",
1415
"LoggerConfig",
1516
"LoggingConfig",
1617
"LoggingConfigUpdatingActor",

src/frequenz/sdk/config/_manager.py

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,26 @@
1919
_logger = logging.getLogger(__name__)
2020

2121

22+
class InvalidValueForKeyError(ValueError):
23+
"""An error indicating that the value under the specified key is invalid."""
24+
25+
def __init__(self, msg: str, *, key: str, value: Any) -> None:
26+
"""Initialize this error.
27+
28+
Args:
29+
msg: The error message.
30+
key: The key that has an invalid value.
31+
value: The actual value that was found that is not a mapping.
32+
"""
33+
super().__init__(msg)
34+
35+
self.key = key
36+
"""The key that has an invalid value."""
37+
38+
self.value = value
39+
"""The actual value that was found that is not a mapping."""
40+
41+
2242
class ConfigManager(BackgroundService):
2343
"""A manager for configuration files.
2444
@@ -106,17 +126,28 @@ def __repr__(self) -> str:
106126

107127
def new_receiver(
108128
self,
129+
key: str,
130+
/,
109131
*,
110132
skip_unchanged: bool = True,
111-
) -> Receiver[Mapping[str, Any] | None]:
112-
"""Create a new receiver for the configuration.
133+
) -> Receiver[Mapping[str, Any] | InvalidValueForKeyError | None]:
134+
"""Create a new receiver for receiving the configuration for a particular key.
113135
114136
This method has a lot of features and functionalities to make it easier to
115137
receive configurations, but it also imposes some restrictions on how the
116138
configurations are received. If you need more control over the configuration
117139
receiver, you can create a receiver directly using
118140
[`config_channel.new_receiver()`][frequenz.sdk.config.ConfigManager.config_channel].
119141
142+
### Filtering
143+
144+
Only the configuration under the `key` will be received by the receiver. If the
145+
`key` is not found in the configuration, the receiver will receive `None`.
146+
147+
The value under `key` must be another mapping, otherwise an error
148+
will be logged and a [`frequenz.sdk.config.InvalidValueForKeyError`][] instance
149+
will be sent to the receiver.
150+
120151
### Skipping superfluous updates
121152
122153
If there is a burst of configuration updates, the receiver will only receive the
@@ -133,26 +164,72 @@ def new_receiver(
133164
```
134165
135166
Args:
167+
key: The configuration key to be read by the receiver.
136168
skip_unchanged: Whether to skip sending the configuration if it hasn't
137169
changed compared to the last one received.
138170
139171
Returns:
140172
The receiver for the configuration.
141173
"""
142-
receiver = self.config_channel.new_receiver(name=str(self), limit=1)
174+
receiver = self.config_channel.new_receiver(name=f"{self}:{key}", limit=1)
175+
176+
def _get_key_or_error(
177+
config: Mapping[str, Any]
178+
) -> Mapping[str, Any] | InvalidValueForKeyError | None:
179+
try:
180+
return _get_key(config, key)
181+
except InvalidValueForKeyError as error:
182+
return error
183+
184+
key_receiver = receiver.map(_get_key_or_error)
143185

144186
if skip_unchanged:
145-
receiver = receiver.filter(WithPrevious(not_equal_with_logging))
187+
return key_receiver.filter(WithPrevious(_not_equal_with_logging))
146188

147-
return receiver
189+
return key_receiver
148190

149191

150-
def not_equal_with_logging(
151-
old_config: Mapping[str, Any], new_config: Mapping[str, Any]
192+
def _not_equal_with_logging(
193+
old_value: Mapping[str, Any] | InvalidValueForKeyError | None,
194+
new_value: Mapping[str, Any] | InvalidValueForKeyError | None,
152195
) -> bool:
153196
"""Return whether the two mappings are not equal, logging if they are the same."""
154-
if old_config == new_config:
197+
if old_value == new_value:
155198
_logger.info("Configuration has not changed, skipping update")
156-
_logger.debug("Old configuration being kept: %r", old_config)
157199
return False
200+
201+
if isinstance(new_value, InvalidValueForKeyError) and not isinstance(
202+
old_value, InvalidValueForKeyError
203+
):
204+
_logger.error(
205+
"Configuration for key %r has an invalid value: %r",
206+
new_value.key,
207+
new_value.value,
208+
)
158209
return True
210+
211+
212+
def _get_key(config: Mapping[str, Any], key: str) -> Mapping[str, Any] | None:
213+
"""Get the value from the configuration under the specified key.
214+
215+
Args:
216+
config: The configuration to get the value from.
217+
key: The key to get the value for.
218+
219+
Returns:
220+
The value under the key, or `None` if the key is not found.
221+
222+
Raises:
223+
InvalidValueForKeyError: If the value under the key is not a mapping.
224+
"""
225+
match config.get(key):
226+
case None:
227+
return None
228+
case Mapping() as value:
229+
return value
230+
case invalid_value:
231+
raise InvalidValueForKeyError(
232+
f"Value for key {key!r} is not a mapping: {invalid_value!r}",
233+
key=key,
234+
value=invalid_value,
235+
)

0 commit comments

Comments
 (0)