diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ebe95534d..277a4f885 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,17 @@ # Frequenz Python SDK Release Notes +## Summary + + + +## Upgrading + + + +## New Features + + + ## Bug Fixes -* Fix bug with `LoggingConfigUpdater` not updating root logger level. -* The `frequenz-quantities` dependency requirement was widened to allow any v1.x version (it was pinned to `1.0.0rc3` before). +- Fix a bug in the resampler that could end up with an *IndexError: list index out of range* exception when a new resampler was added while awaiting the existing resampler to finish resampling. diff --git a/src/frequenz/sdk/timeseries/_resampling.py b/src/frequenz/sdk/timeseries/_resampling.py index 07c06361f..c00411601 100644 --- a/src/frequenz/sdk/timeseries/_resampling.py +++ b/src/frequenz/sdk/timeseries/_resampling.py @@ -14,7 +14,6 @@ from collections.abc import AsyncIterator, Callable, Coroutine, Sequence from dataclasses import dataclass from datetime import datetime, timedelta, timezone -from typing import cast from frequenz.channels.timer import Timer, TriggerAllMissed, _to_microseconds from frequenz.quantities import Quantity @@ -495,25 +494,24 @@ async def resample(self, *, one_shot: bool = False) -> None: self._config.resampling_period, ) + # We need to make a copy here because we need to match the results to the + # current resamplers, and since we await here, new resamplers could be added + # or removed from the dict while we awaiting the resampling, which would + # cause the results to be out of sync. + resampler_sources = list(self._resamplers) results = await asyncio.gather( *[r.resample(self._window_end) for r in self._resamplers.values()], return_exceptions=True, ) self._window_end += self._config.resampling_period - # We need the cast because mypy is not able to infer that this can only - # contain Exception | CancelledError because of the condition in the list - # comprehension below. - exceptions = cast( - dict[Source, Exception | asyncio.CancelledError], - { - source: results[i] - for i, source in enumerate(self._resamplers) - # CancelledError inherits from BaseException, but we don't want - # to catch *all* BaseExceptions here. - if isinstance(results[i], (Exception, asyncio.CancelledError)) - }, - ) + exceptions = { + source: result + for source, result in zip(resampler_sources, results) + # CancelledError inherits from BaseException, but we don't want + # to catch *all* BaseExceptions here. + if isinstance(result, (Exception, asyncio.CancelledError)) + } if exceptions: raise ResamplingError(exceptions) if one_shot: