From b4ae9d789eae4ea82868cfa21cbfc54f5ef1ff74 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Wed, 16 Nov 2022 12:27:47 +0100 Subject: [PATCH] Use "aware" datetimes When creating datetimes we need to make sure to include timezone information, otherwise we will get exceptions raised when comparing to other "aware" datetime objects. Using "native" datetime object (without timezone information) is very dangerous as we might mix datetimes created for different timezones without knowing, introducing very obscure bugs. More info about "aware" and "native" datetimes: https://docs.python.org/3/library/datetime.html#aware-and-naive-objects Signed-off-by: Leandro Lucarella --- src/frequenz/channels/utils/timer.py | 16 +++++++++------- tests/utils/test_timer.py | 8 ++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/frequenz/channels/utils/timer.py b/src/frequenz/channels/utils/timer.py index 258f0caa..1a115456 100644 --- a/src/frequenz/channels/utils/timer.py +++ b/src/frequenz/channels/utils/timer.py @@ -4,7 +4,7 @@ """A timer receiver that returns the timestamp every `interval`.""" import asyncio -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Optional from frequenz.channels.base_classes import Receiver @@ -15,6 +15,8 @@ class Timer(Receiver[datetime]): Primarily for use with [Select][frequenz.channels.Select]. + The timestamp generated is a timezone-aware datetime using UTC as timezone. + Example: When you want something to happen with a fixed period: @@ -59,11 +61,11 @@ def __init__(self, interval: float) -> None: """ self._stopped = False self._interval = timedelta(seconds=interval) - self._next_msg_time = datetime.now() + self._interval + self._next_msg_time = datetime.now(timezone.utc) + self._interval def reset(self) -> None: """Reset the timer to start timing from `now`.""" - self._next_msg_time = datetime.now() + self._interval + self._next_msg_time = datetime.now(timezone.utc) + self._interval def stop(self) -> None: """Stop the timer. @@ -75,20 +77,20 @@ def stop(self) -> None: self._stopped = True async def receive(self) -> Optional[datetime]: - """Return the current time once the next tick is due. + """Return the current time (in UTC) once the next tick is due. Returns: - The time of the next tick or `None` if + The time of the next tick in UTC or `None` if [stop()][frequenz.channels.Timer.stop] has been called on the timer. """ if self._stopped: return None - now = datetime.now() + now = datetime.now(timezone.utc) diff = self._next_msg_time - now while diff.total_seconds() > 0: await asyncio.sleep(diff.total_seconds()) - now = datetime.now() + now = datetime.now(timezone.utc) diff = self._next_msg_time - now self._next_msg_time = now + self._interval diff --git a/tests/utils/test_timer.py b/tests/utils/test_timer.py index 9a88fe21..9fb3d3d6 100644 --- a/tests/utils/test_timer.py +++ b/tests/utils/test_timer.py @@ -6,7 +6,7 @@ import asyncio import logging from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timezone from typing import Optional from frequenz.channels import Anycast, Select, Sender, Timer @@ -30,13 +30,13 @@ class _TestCase: ] fail_count = 0 for test_case in test_cases: - start = datetime.now() + start = datetime.now(timezone.utc) count = 0 async for _ in Timer(test_case.delta): count += 1 if count >= test_case.count: break - actual_duration = (datetime.now() - start).total_seconds() + actual_duration = (datetime.now(timezone.utc) - start).total_seconds() expected_duration = test_case.delta * test_case.count tolerance = expected_duration * 0.1 @@ -72,7 +72,7 @@ async def send(ch1: Sender[int]) -> None: senders = asyncio.create_task(send(chan1.get_sender())) select = Select(msg=chan1.get_receiver(), timer=timer) - start_ts = datetime.now() + start_ts = datetime.now(timezone.utc) stop_ts: Optional[datetime] = None while await select.ready(): if select.msg: