Skip to content

Commit

Permalink
Use "aware" datetimes
Browse files Browse the repository at this point in the history
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 <leandro.lucarella@frequenz.com>
  • Loading branch information
llucax committed Nov 16, 2022
1 parent 7778405 commit b4ae9d7
Show file tree
Hide file tree
Showing 2 changed files with 13 additions and 11 deletions.
16 changes: 9 additions & 7 deletions src/frequenz/channels/utils/timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions tests/utils/test_timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit b4ae9d7

Please sign in to comment.