Skip to content

Commit 564949b

Browse files
committed
WIP: Add tests and fix some bugs
1 parent ce36bab commit 564949b

File tree

3 files changed

+290
-38
lines changed

3 files changed

+290
-38
lines changed

noxfile.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,28 @@ def formatting(session: nox.Session) -> None:
2828
@nox.session
2929
def pylint(session: nox.Session) -> None:
3030
"""Run pylint to do lint checks."""
31-
session.install("-e", ".[docs]", "pylint", "pytest", "nox")
31+
session.install(
32+
"-e",
33+
".[docs]",
34+
"pylint",
35+
"pytest",
36+
"nox",
37+
"async-solipsism",
38+
)
3239
session.run("pylint", *check_dirs, *check_files)
3340

3441

3542
@nox.session
3643
def mypy(session: nox.Session) -> None:
3744
"""Run mypy to check type hints."""
38-
session.install("-e", ".[docs]", "pytest", "nox", "mypy")
45+
session.install(
46+
"-e",
47+
".[docs]",
48+
"pytest",
49+
"nox",
50+
"mypy",
51+
"async-solipsism",
52+
)
3953

4054
common_args = [
4155
"--namespace-packages",
@@ -72,7 +86,13 @@ def docstrings(session: nox.Session) -> None:
7286
@nox.session
7387
def pytest(session: nox.Session) -> None:
7488
"""Run all tests using pytest."""
75-
session.install("pytest", "pytest-cov", "pytest-mock", "pytest-asyncio")
89+
session.install(
90+
"pytest",
91+
"pytest-cov",
92+
"pytest-mock",
93+
"pytest-asyncio",
94+
"async-solipsism",
95+
)
7696
session.install("-e", ".")
7797
session.run(
7898
"pytest",

src/frequenz/channels/util/_periodic_timer.py

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ class PeriodicTimer(Receiver[timedelta]):
8080
as the timer uses `asyncio`s loop monotonic clock.
8181
8282
If the timer is delayed too much (see `delay_tolerance`), then the timer
83-
will behave according to the `missed_ticks_behaviour`. Missing ticks might
83+
will behave according to the `missed_tick_behavior`. Missing ticks might
8484
or might not trigger a message and the drift could be accumulated or not.
8585
8686
The timer accepts an optional `loop`, which will be used to track the time.
@@ -130,7 +130,7 @@ class PeriodicTimer(Receiver[timedelta]):
130130
```python
131131
timer = PeriodicTimer(timedelta(seconds=1.0),
132132
auto_start=False,
133-
missed_ticks_behaviour=MissedTickBehavior.SKIP_AND_DRIFT,
133+
missed_tick_behavior=MissedTickBehavior.SKIP_AND_DRIFT,
134134
delay_tolerance=timedelta(0),
135135
)
136136
select = Select(bat_1=receiver1, heavy_process=receiver2, timeout=timer)
@@ -161,7 +161,7 @@ def __init__(
161161
interval: timedelta,
162162
*,
163163
auto_start: bool = True,
164-
missed_ticks_behaviour: MissedTickBehavior = MissedTickBehavior.TRIGGER_ALL,
164+
missed_tick_behavior: MissedTickBehavior = MissedTickBehavior.TRIGGER_ALL,
165165
delay_tolerance: timedelta | None = None,
166166
loop: asyncio.AbstractEventLoop | None = None,
167167
) -> None:
@@ -175,11 +175,11 @@ def __init__(
175175
instance is created. This can only be `True` if there is
176176
already a running loop or an explicit `loop` that is running
177177
was passed.
178-
missed_ticks_behaviour: The behaviour of the timer when it misses
178+
missed_tick_behavior: The behavior of the timer when it misses
179179
a tick. See the documentation of `MissedTickBehavior` for
180180
details.
181181
delay_tolerance: The maximum delay that is tolerated before
182-
adjusting ticks according to `missed_ticks_behaviour`. If
182+
adjusting ticks according to `missed_tick_behavior`. If
183183
a tick is delayed less than this, then it is not considered
184184
a missed tick. By default this is set to 1% of the interval.
185185
Bear in mind that some `MissedTickBehavior`s ignore this value.
@@ -193,16 +193,16 @@ def __init__(
193193
self._interval: timedelta = interval
194194
"""The time to between timer ticks."""
195195

196-
self._missed_ticks_behaviour: MissedTickBehavior = missed_ticks_behaviour
197-
"""The behaviour of the timer when it misses a tick.
196+
self._missed_tick_behavior: MissedTickBehavior = missed_tick_behavior
197+
"""The behavior of the timer when it misses a tick.
198198
199199
See the documentation of `MissedTickBehavior` for details.
200200
"""
201201

202202
self._delay_tolerance: timedelta = (
203203
(interval / 100) if delay_tolerance is None else delay_tolerance
204204
)
205-
"""The maximum allowed delay before triggering `missed_ticks_behaviour`.
205+
"""The maximum allowed delay before triggering `missed_tick_behavior`.
206206
207207
If a tick is delayed less than this, then it is not considered a missed
208208
tick. Bear in mind that some `MissedTickBehavior`s ignore this value.
@@ -213,24 +213,28 @@ def __init__(
213213
)
214214
"""The event loop to use to track time."""
215215

216-
self._stopped: bool = False
216+
self._stopped: bool = True
217217
"""Wether the timer was requested to stop.
218218
219-
If there is no request to stop, any receiving method will start it by
220-
calling `reset()`. If this is True, receiving methods will raise
221-
a `ReceiverStoppedError`.
219+
If this is `False`, then the timer is running.
220+
221+
If this is `True`, then it is stopped or there is a request to stop it
222+
or it was not started yet:
223+
224+
* If `_next_msg_time` is `None`, it means it wasn't started yet (it was
225+
created with `auto_start=False`). Any receiving method will start
226+
it by calling `reset()` in this case.
222227
223-
When the timer is not `auto_start`ed, this will be `False`, but
224-
`_next_msg_time` will be `None`, indicating receiving methods that they
225-
should start the timer.
228+
* If `_next_msg_time` is not `None`, it means there was a request to
229+
stop it. In this case receiving methods will raise
230+
a `ReceiverClosedError`.
226231
"""
227232

228233
self._next_tick_time: float | None = None
229234
"""The absolute (monotonic) time when the timer should trigger.
230235
231-
If this is `None`, it means the timer either didn't started (if
232-
`_stopped` is `False`) or was explicitly stopped (if `_stopped` is
233-
`True`).
236+
If this is `None`, it means the timer didn't start yet, but it should
237+
be started as soon as it is used.
234238
"""
235239

236240
self._current_drift: timedelta | None = None
@@ -255,21 +259,21 @@ def interval(self) -> timedelta:
255259
return self._interval
256260

257261
@property
258-
def missed_ticks_behaviour(self) -> MissedTickBehavior:
259-
"""The behaviour of the timer when it misses a tick.
262+
def missed_tick_behavior(self) -> MissedTickBehavior:
263+
"""The behavior of the timer when it misses a tick.
260264
261265
Returns:
262-
The behaviour of the timer when it misses a tick.
266+
The behavior of the timer when it misses a tick.
263267
"""
264-
return self._missed_ticks_behaviour
268+
return self._missed_tick_behavior
265269

266270
@property
267271
def delay_tolerance(self) -> timedelta:
268-
"""The maximum allowed delay before triggering `missed_ticks_behaviour`.
272+
"""The maximum allowed delay before triggering `missed_tick_behavior`.
269273
270274
Returns:
271275
The maximum allowed delay before triggering
272-
`missed_ticks_behaviour`.
276+
`missed_tick_behavior`.
273277
"""
274278
return self._delay_tolerance
275279

@@ -318,7 +322,7 @@ def stop(self) -> None:
318322
You can restart the timer with `reset()`.
319323
"""
320324
self._stopped = True
321-
self._next_tick_time = None
325+
self._next_tick_time = -1.0
322326

323327
async def ready(self) -> bool:
324328
"""Wait until the timer interval passed.
@@ -340,18 +344,18 @@ async def ready(self) -> bool:
340344
if self._current_drift is not None:
341345
return True
342346

343-
# If a stop was explicitly requested, we bail out.
344-
if self._stopped:
345-
return False
346-
347-
# If there is no stop requested but we don't have a time for the next
348-
# message, then we reset() (so we start the timer as of now).
347+
# If `_next_tick_time` is `None`, it means it was created with
348+
# `auto_start=False` and should be started.
349349
if self._next_tick_time is None:
350350
self.reset()
351351
assert (
352352
self._next_tick_time is not None
353353
), "This should be assigned by reset()"
354354

355+
# If a stop was explicitly requested, we bail out.
356+
if self._stopped:
357+
return False
358+
355359
now = self._loop.time()
356360
time_to_next_tick = self._next_tick_time - now
357361
# If we didn't reach the tick yet, sleep until we do.
@@ -365,18 +369,18 @@ async def ready(self) -> bool:
365369
# calculate the next tick time and return.
366370
if (
367371
self._current_drift <= self._delay_tolerance
368-
or self._missed_ticks_behaviour is MissedTickBehavior.TRIGGER_ALL
372+
or self._missed_tick_behavior is MissedTickBehavior.TRIGGER_ALL
369373
):
370374
self._next_tick_time = self._next_tick_time + self._interval.total_seconds()
371375
return True
372376

373377
# At this point we have a considerable delay and need to skip ticks
374378

375-
if self._missed_ticks_behaviour is MissedTickBehavior.SKIP_AND_DRIFT:
379+
if self._missed_tick_behavior is MissedTickBehavior.SKIP_AND_DRIFT:
376380
self.reset()
377381
return True
378382

379-
if self._missed_ticks_behaviour is MissedTickBehavior.SKIP_AND_RESYNC:
383+
if self._missed_tick_behavior is MissedTickBehavior.SKIP_AND_RESYNC:
380384
# We need to resync (align) the next tick time to the current time
381385
total_missed, reminder = divmod(self._current_drift, self._interval)
382386
delta_to_next_tick = self._interval * (total_missed + 1) - reminder
@@ -399,7 +403,9 @@ def consume(self) -> timedelta:
399403
Raises:
400404
ReceiverStoppedError: if the timer was stopped via `stop()`.
401405
"""
402-
if self._stopped:
406+
# If it was stopped and there it no pending result, we raise
407+
# (if there is a pending result, then we still want to return it first)
408+
if self._stopped and self._current_drift is None:
403409
raise ReceiverStoppedError(self)
404410

405411
assert (

0 commit comments

Comments
 (0)