Using timers in actors
The Coyote actor programming model has built-in support for timers. Timers are themselves a type of Actor
that
send a TimerElapsedEvent
upon timeout to the actor that created the timer. Timers are handy for
modeling a common type of asynchronous behavior in distributed systems, so you can find more bugs in
your code relating to how it deals with the uncertainty of various kinds of timeouts.
Timers provide a once only timeout event or a periodic timeout, which can continually send such events on a user-defined interval until they are stopped.
To make use of timers, you must include the Microsoft.Coyote.Actors.Timers
namespace. You can
start a non-periodic timer using the function StartTimer
.
TimerInfo StartTimer(TimeSpan startDelay, TimerElapsedEvent customEvent)
StartTimer
takes as argument:
1. TimeSpan startDelay
, which is the amount of time to wait before sending the timeout event.
2. TimerElapsedEvent customEvent
, an optional custom event (of a user-specified subclass of
TimerElapsedEvent
) to raise instead of the default TimerElapsedEvent
.
StartTimer
returns TimerInfo
, which contains information about the created non-periodic timer.
The non-periodic timer is automatically disposed after it timeouts. You can also pass the
TimerInfo
to the StopTimer
method to manually stop and dispose the timer. The timer enqueues
events of type TimerElapsedEvent
(or of a user-specified subclass of TimerElapsedEvent
) in the
inbox of the actor that created it. The TimerElapsedEvent
contains as payload, the same
TimerInfo
returned during the timer creation. If you create multiple timers, then the TimerInfo
object can be used to distinguish the sources of the different TimerElapsedEvent
events.
You can start a periodic timer using the function StartPeriodicTimer
.
TimerInfo StartPeriodicTimer(TimeSpan startDelay, TimeSpan period, TimerElapsedEvent customEvent)
StartPeriodicTimer
takes as argument:
1. TimeSpan startDelay
, which is the amount of time to wait before sending the first timeout
event.
2. TimeSpan period
, which is the time interval between timeout events.
3. TimerElapsedEvent customEvent
, an optional custom event (of a user-specified subclass of
TimerElapsedEvent
) to raise instead of the default TimerElapsedEvent
.
Periodic timers work similarly to normal timers, however you need to manually stop a periodic timer
using the StopTimer
method if you want it to stop sending TimerElapsedEvent
events. Note that
when an actor halts, it automatically stops and disposes all its periodic and
non-periodic timers.
A sample which demonstrates the use of such timers is provided in the Timers Sample on github, which is explained in detail below.
First you need to declare on your Actor that it is expecting to receive the TimerElapsedEvent
so
the class is defined like this:
[OnEventDoAction(typeof(TimerElapsedEvent), nameof(HandleTimeout))]
internal class Client : Actor
{
...
}
To kick things off the initialization method starts a non-periodic timer:
protected override Task OnInitializeAsync(Event initialEvent)
{
Console.WriteLine("<Client> Starting a non-periodic timer");
this.StartTimer(TimeSpan.FromSeconds(1));
return base.OnInitializeAsync(initialEvent);
}
The HandleTimeout
method then receives this timeout and starts a periodic timer as follows:
private void HandleTimeout(Event e)
{
TimerElapsedEvent te = (TimerElapsedEvent)e;
this.WriteMessage("<Client> Handling timeout from timer");
this.WriteMessage("<Client> Starting a period timer");
this.PeriodicTimer = this.StartPeriodicTimer(TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(1), new CustomTimerEvent());
}
In this case we use a CustomTimerEvent
instead of the default TimerElapsedEvent
, this custom event is defined as follows:
internal class CustomTimerEvent : TimerElapsedEvent
{
/// <summary>
/// Count of timeout events processed.
/// </summary>
internal int Count;
}
This custom event makes it possible to route the period timeouts to a different handler using this on the actor class:
[OnEventDoAction(typeof(CustomTimerEvent), nameof(HandlePeriodicTimeout))]
In the HandlePeriodicTimeout method we count the number of timeouts and stop when we reach 3:
private void HandlePeriodicTimeout(Event e)
{
this.WriteMessage("<Client> Handling timeout from periodic timer");
if (e is CustomTimerEvent ce)
{
ce.Count++;
if (ce.Count == 3)
{
this.WriteMessage("<Client> Stopping the periodic timer");
this.StopTimer(this.PeriodicTimer);
}
}
}
Notice how we can cast the Event
into our custom CustomTimerEvent
, and we get the same instance
of CustomTimerEvent
on each period call, so this way the CustomTimerEvent
can contain useful
state, in this case the Count
.
The output of this program is as follows:
<Client> Starting a non-periodic timer
<Client> Handling timeout from timer
<Client> Starting a period timer
<Client> Handling timeout from periodic timer
<Client> Handling timeout from periodic timer
<Client> Handling timeout from periodic timer
<Client> Stopping the periodic timer
Implementation notes
Note that timers are implemented differently depending on what mode your program is running in.
Normal “production” mode execution uses an optimized timer built on System.Threading.Timer
.
Timers that run during systematic testing, however, are quite different and are implemented in a
MockTimerActor
class. This mock actually removes all concept of real time intervals, and replaces
that with a random decision to timeout or not to timeout. This makes your test run much faster. But
it can also make your test see a lot more timeout events that you may be expecting. This is good
for finding bugs, but can overwhelm the test with timeout events. To reduce the number of timeouts
there is a --timeout-delay
command line option on the coyote test
tool. This timeout delay is
given to the mock timer and it will only fire a timeout when RandomInteger(delay) == 0
. The
default value of --timeout-delay
is 10, which means timeouts only fire once every 10 times the
timer gets scheduled by the systematic testing runtime. This turns out to be a pretty good default
that stops your test from getting too overwhelmed by timeout events, but if you still see too many
timeout events in your test logs, then you can increase this number.