Timers¤
A receiver that sends a message regularly.
Quick Start¤
Important
This quick start is provided to have a quick feeling of how to use this module, but it is extremely important to understand how timers behave when they are delayed.
We recommend emphatically to read about missed ticks and drifting before using timers in production.
If you need to do something as periodically as possible (avoiding
drifts), you can use
a Timer
like this:
Periodic Timer Example
import asyncio
from datetime import datetime, timedelta
from frequenz.channels.timer import Timer
async def main() -> None:
async for drift in Timer(timedelta(seconds=1.0), TriggerAllMissed()):
print(f"The timer has triggered at {datetime.now()} with a drift of {drift}")
asyncio.run(main())
This timer will tick as close as every second as possible, even if the loop is busy doing something else for a good amount of time. In extreme cases, if the loop was busy for a few seconds, the timer will trigger a few times in a row to catch up, one for every missed tick.
If, instead, you need a timeout, for example to abort waiting for other receivers after
a certain amount of time, you can use a Timer
like
this:
Timeout Example
import asyncio
from datetime import timedelta
from frequenz.channels import Anycast, select, selected_from
from frequenz.channels.timer import Timer
async def main() -> None:
channel = Anycast[int](name="data-channel")
data_receiver = channel.new_receiver()
timer = Timer(timedelta(seconds=1.0), SkipMissedAndDrift())
async for selected in select(data_receiver, timer):
if selected_from(selected, data_receiver):
print(f"Received data: {selected.message}")
timer.reset()
elif selected_from(selected, timer):
drift = selected.message
print(f"No data received for {timer.interval + drift} seconds, giving up")
break
asyncio.run(main())
This timer will rearm itself automatically after it was triggered, so it will trigger again after the selected interval, no matter what the current drift was. So if the loop was busy for a few seconds, the timer will trigger immediately and then wait for another second before triggering again. The missed ticks are skipped.
Missed Ticks And Drifting¤
A Timer
can be used to send a messages at regular
time intervals, but there is one fundamental issue with timers in the asyncio world:
the event loop could give control to another task at any time, and that task can take
a long time to finish, making the time it takes the next timer message to be received
longer than the desired interval.
Because of this, it is very important for users to be able to understand and control how timers behave when they are delayed. Timers will handle missed ticks according to a missing tick policy.
The following built-in policies are available:
SkipMissedAndDrift
: A policy that drops all the missed ticks, triggers immediately and resets.SkipMissedAndResync
: A policy that drops all the missed ticks, triggers immediately and resyncs.TriggerAllMissed
: A policy that triggers all the missed ticks immediately until it catches up.
Policies¤
Skip Missed And Drift¤
A policy that drops all the missed ticks, triggers immediately and resets.
The SkipMissedAndDrift
policy will
behave effectively as if the timer was
reset every time it is triggered. This means
the start time will change and the drift will be accumulated each time a tick is
delayed. Only the relative drift will be returned on each tick.
The reset happens only if the delay is larger than the delay tolerance, so it is possible to ignore small delays and not drift in those cases.
Example
This example represents a timer with interval 1 second and delay tolerance of 0.1 seconds.
-
The first tick,
T0
, happens exactly at time 0. -
The second tick,
T1.2
, happens at time 1.2 (0.2 seconds late), so the timer triggers immediately but drifts a bit (0.2 seconds), so the next tick is expected at 2.2 seconds. -
The third tick,
T2.2
, happens at 2.3 seconds (0.1 seconds late), so it also triggers immediately but it doesn't drift because the delay is under thedelay_tolerance
. The next tick is expected at 3.2 seconds. -
The fourth tick,
T4.2
, triggers at 4.3 seconds (1.1 seconds late), so it also triggers immediately but the timer has drifted by 1.1 seconds, so a potential tickT3.2
is skipped (not triggered). -
The fifth tick,
T5.3
, triggers at 5.3 seconds so it is right on time (no drift) and the same happens for tickT6.3
, which triggers at 6.3 seconds.
Skip Missed And Re-Sync¤
A policy that drops all the missed ticks, triggers immediately and resyncs.
If ticks are missed, the
SkipMissedAndResync
policy will
make the Timer
trigger immediately and it will
schedule to trigger again on the next multiple of the
interval, effectively skipping any missed
ticks, but re-syncing with the original start time.
Example
This example represents a timer with interval 1 second.
-
The first tick
T0
happens exactly at time 0. -
The second tick,
T1
, happens at time 1.2 (0.2 seconds late), so it triggers immediately. But it re-syncs, so the next tick is still expected at 2 seconds. This re-sync happens on every tick, so all ticks are expected at multiples of 1 second, not matter how delayed they were. -
The third tick,
T2
, happens at time 2.3 (0.3 seconds late), so it also triggers immediately. -
The fourth tick,
T3
, happens at time 4.3 (1.3 seconds late), so it also triggers immediately, but there was also a tick expected at 4 seconds,T4
, which is skipped according to this policy to avoid bursts of ticks. -
The sixth tick,
T5
, happens at 5.1 (0.1 seconds late), so it triggers immediately again. -
The seventh tick,
T6
, happens at 6.0, right on time.
Trigger All Missed¤
A policy that triggers all the missed ticks immediately until it catches up.
The TriggerAllMissed
policy will
trigger all missed ticks immediately until it catches up with the current time.
This means that if the timer is delayed for any reason, when it finally gets some
time to run, it will trigger all the missed ticks in a burst. The drift is not
accumulated and the next tick will be scheduled according to the original start
time.
Example
This example represents a timer with interval 1 second.
-
The first tick,
T0
happens exactly at time 0. -
The second tick,
T1
, happens at time 1.2 (0.2 seconds late), so it triggers immediately. But it re-syncs, so the next tick is still expected at 2 seconds. This re-sync happens on every tick, so all ticks are expected at multiples of 1 second, not matter how delayed they were. -
The third tick,
T2
, happens at time 2.3 (0.3 seconds late), so it also triggers immediately. -
The fourth tick,
T3
, happens at time 4.3 (1.3 seconds late), so it also triggers immediately. -
The fifth tick,
T4
, which was also already delayed (by 0.3 seconds), triggers immediately too, resulting in a small catch-up burst. -
The sixth tick,
T5
, happens at 5.1 (0.1 seconds late), so it triggers immediately again. -
The seventh tick,
T6
, happens at 6.0, right on time.
Timer¤
A receiver that sends a message regularly.
Timer
s are started by default after they are
created. This can be disabled by using auto_start=False
option when creating
them. In this case, the timer will not be started until
reset()
is called. Receiving from the timer
(either using receive()
or using the
async iterator interface) will also start the timer at that point.
Timers need to be created in a context where
a asyncio
loop is already running. If no
loop
is specified, the current running loop
is used.
Timers can be stopped by calling stop()
.
A stopped timer will raise
a ReceiverStoppedError
or stop the async
iteration as usual.
Once a timer is explicitly stopped, it can only be started again by explicitly
calling reset()
(trying to receive from it
or using the async iterator interface will keep failing).
Timer messages are timedelta
s containing the drift of the
timer, i.e. the difference between when the timer should have triggered and the time
when it actually triggered.
This drift will likely never be 0
, because if there is a task that is
running when it should trigger, the timer will be delayed. In this case the
drift will be positive. A negative drift should be technically impossible,
as the timer uses asyncio
s loop monotonic clock.
Warning
Even when the asyncio
loop's monotonic clock is a float
, timers use
int
s to represent time internally. The internal time is tracked in
microseconds, so the timer resolution is 1 microsecond
(interval
must be at least
1 microsecond).
This is to avoid floating point errors when performing calculations with time, which can lead to issues that are very hard to reproduce and debug.
If the timer is delayed too much, then it will behave according to the
missed_tick_policy
. Missing
ticks might or might not trigger a message and the drift could be accumulated or not
depending on the chosen policy.
Custom Missed Tick Policies¤
A policy to handle timer missed ticks.
To implement a custom policy you need to subclass
MissedTickPolicy
and implement the
calculate_next_tick_time
method.
Example
This policy will just wait one more second than the original interval if a tick is missed:
class WaitOneMoreSecond(MissedTickPolicy):
def calculate_next_tick_time(
self, *, interval: int, scheduled_tick_time: int, now: int
) -> int:
return scheduled_tick_time + interval + 1_000_000
async def main() -> None:
timer = Timer(
interval=timedelta(seconds=1),
missed_tick_policy=WaitOneMoreSecond(),
)
async for drift in timer:
print(f"The timer has triggered with a drift of {drift}")
asyncio.run(main())