Skip to content

Commit 45b68bf

Browse files
authored
Fix timer reset() while it is being waited on (#241)
If the timer was reset while it was being waited on, the next tick time was not recalculated and the timer would wait for the original time instead of the new one. This means a timeout could fire even if there was actually no timeout (because the timer was reset).
2 parents 079aff0 + 878ba9b commit 45b68bf

File tree

3 files changed

+41
-2
lines changed

3 files changed

+41
-2
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,4 @@
145145

146146
## Bug Fixes
147147

148-
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
148+
* `Timer`: Fix bug that was causing calls to `reset()` to not reset the timer, if the timer was already being awaited.

src/frequenz/channels/timer.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -696,10 +696,15 @@ async def ready(self) -> bool: # noqa: DOC502
696696

697697
now = self._now()
698698
time_to_next_tick = self._next_tick_time - now
699+
699700
# If we didn't reach the tick yet, sleep until we do.
700-
if time_to_next_tick > 0:
701+
# We need to do this in a loop also reacting to the reset event, as the timer
702+
# could be reset while we are sleeping, in which case we need to recalculated
703+
# the time to the next tick and try again.
704+
while time_to_next_tick > 0:
701705
await asyncio.sleep(time_to_next_tick / 1_000_000)
702706
now = self._now()
707+
time_to_next_tick = self._next_tick_time - now
703708

704709
# If a stop was explicitly requested during the sleep, we bail out.
705710
if self._stopped:

tests/test_timer_integration.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# License: MIT
2+
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Integration tests for the timer."""
5+
6+
7+
import asyncio
8+
from datetime import timedelta
9+
10+
import async_solipsism
11+
import pytest
12+
13+
from frequenz.channels.timer import Timer
14+
15+
16+
@pytest.mark.integration
17+
async def test_timer_timeout_reset(
18+
event_loop: async_solipsism.EventLoop, # pylint: disable=redefined-outer-name
19+
) -> None:
20+
"""Test that the receiving is properly adjusted after a reset."""
21+
22+
async def timer_wait(timer: Timer) -> None:
23+
await timer.receive()
24+
25+
async with asyncio.timeout(2.0):
26+
async with asyncio.TaskGroup() as task_group:
27+
timer = Timer.timeout(timedelta(seconds=1.0))
28+
start_time = event_loop.time()
29+
task_group.create_task(timer_wait(timer))
30+
await asyncio.sleep(0.5)
31+
timer.reset()
32+
33+
run_time = event_loop.time() - start_time
34+
assert run_time >= 1.5

0 commit comments

Comments
 (0)