The library formerly known as chronos - renamed to allow upload to
hex.pm
.
Erlang comes with some good utilities for timers, but there are some shortcomings that might become an issue for you as they did for me.
kairos tries to hide the book keeping from the user in a way that will be very familiar to those who have tried to implement protocols from the telecommunications realm.
In addition to the abstraction the kairos distribution also shows how
to design the APIs of your code in such a way that it makes it easier
to test the code. This part requires the use of the meck application
by Adam Lindberg or some serious manual hacking... I am going to show
the meck way of doing it. See the ping_test
module in the examples
directory.
Abstracting time as it is suggested (with or with kairos) will give you a design where you can test how timers work very fast. You can trust things will work in real life since kairos comes with a test suite that shows that it does what you would expect. The burden on you is then to write a test that shows that your component works as it should for a sequence of events where some of them happens to be timers expiring. And you do not have to wait for the timers to expire since time has been abstracted.
Below the description of how to use kairos there is a brief overview of what the existing timer solutions has to offer so you can make an informed choice about which timer solution fits your problem the best.
The design of kairos was influenced by the problems with the existing timer solutions and shaped by the needs when implementing telecom protocols, which uses timers extensively.
Instead of having a single global timer server kairos allows you to create as many or as few timer serves as you see fit.
start_link(ServerName)
will start a new timer server where
ServerName :: atom().
Once you have started a timer server you can start timers using
start_timer(ServerName, TimerName, Timeout, Callback)
where
ServerName :: atom() | pid().
TimerName :: term()
Timeout :: pos_integer()
Callback :: {module(), atom(), [term[]]}
The timer will be given a unique name - if you start the timer again
it will be restarted - and when the Timeout
ms has passed the
Callback
function will be called.
In some cases you want to cancel a timer and for that you can use
stop_timer(ServerName, TimerName)
In this case the Callback
function will not be called.
kairos keeps track of all the running timers so your application code can concentrate on what it is supposed to do without having to do tedious book keeping.
Getting rid of tedious book keeping is not the only thing you get from using kairos. By putting all your timers in the hands of kairos you get a set-up that is very easy to mock so that you can abstract time out of your tests.
One way of doing this is to provide a timer_expiry
function as part
of the API for the component you are creating. One of the arguments
should be the name of the timer and if you start more instances of the
component you need to have the name of the component in the call as
well.
timer_expiry(TimerName) ->
gen_server:call(?SERVER, {timer_expiry, TimerName}).
In the code you can request a timer like this:
kairos:start_timer(ServerName,
timer_4,
{my_mod, timer_expiry, [timer_4]})
and then handling the timeout becomes very simple:
handle_call({timer_expiry, timer_4}, _From, State) ->
...
That is the basic set-up and while testing you have to mock kairos. This is easy to do with the meck application and should be quite simple to do with any mocking library.
So you ensure that you have control over kairos:
meck:new(kairos),
meck:expect(kairos, start_timer,
fun(_,_) -> 42 end)
As part of the test you check that the timer was requested to start:
meck:called(kairos, start_timer, [my_server, timer_4])
And when you come to the point in the test where you want to see the effects of the timer expiry you simply call
my_mod:timer_expiry(timer_4)
This approach lends itself well to property based testing and unit testing.
The approach is the same as with meck, the only things you need to change is the mocking of kairos.
The most common way of using mocking with EQC is through the eqc_component
, where
you have to specify an api_spec/1
function:
api_spec() ->
#api_spec{
language = erlang,
modules = [
#api_module{
name = kairos,
functions = [
#api_fun{
name = start_link, arity = 1},
#api_fun{
name = start_link, arity = 0},
#api_fun{
name = start_timer, arity = 4},
#api_fun{
name = stop_timer, arity = 2} ]}]}.
You can then specify the callouts to these in the _callouts/2
function for a
command:
my_command_callouts(S, Args) ->
?CALLOUT(kairos, start_timer, [ServerName, TimerName, Duration, MFA], ok).
This will give you a very precise description of all aspects of the protocol that your component is following. Yes, timers are part of the protocol.
This is an excellent module in terms of abstraction: it provides all the functionality you would want in order to start and stop timers.
Unfortunately there can only be one timer server for each Erlang node and that is that it can very easily become overloaded - see the [http://erlang.org/doc/efficiency_guide/commoncaveats.html] entry on the timer module.
As the Efficiency Guide states the erlang:send_after/3
and
erlang:start_timer/3
are much more efficient than the timer module,
but using them directly requires some book keeping which can clutter
your code unnecessarily.
I am assuming that you use Erlang/OTP to develop your software - if not you can skip this section! And in that case you probably never got to this line since the kairos abstraction is too high level for your taste...
For gen_server
and gen_fsm
you can specify a timer in the result
tuple from your handler functions, e.g., for gen_fsm
you can return
{next_state,NextStateName,NewStateData,Timeout}
. If the gen_server
does not receive another message within Timeout
milliseconds a timeout
event will happen. The problem with this approach is that if any
message arrives the timer is cancelled and in many cases you want to
see a specific message before you cancel the timer or you want to have
multiple timers running.
You can have multiple timers with gen_fsm
by using the
gen_fsm:start_timer/2
function. The down side is that you have to do
book keeping of timer references if you want to cancel the timer. This
is similar to using the erlang module's timers mentioned above.
The downside is that there is not equivalent of
gen_fsm:start_timer/2
for gen_server
so for that you have to use
one of the other solutions.
The kairos timer server is designed to be used as a process that is owned by one process. There are a number of reasons for this:
- When a timer server dies you want to be notified and take appropriate action.
- When the starting process dies you want the timer server to go away.
The former can be dealt with by simple supervision, but the fixing the latter would require much more complexity in the kairos code.
If an arbitrary number of process link to the timer server you need to protect the timer server against the possible death of any of them. This can be done by adding a layer on top of kairos that deals with this. Hence, in order to keep kairos simple this has deliberately been left out. Also, so far no one has come up with a use case where sharing the timer server is required. This leads me to believe that such cases will have special traits leading to the need for custom code in each case.
Based on the use cases kairos has been used for so far it seems that it is a useful little utility, that does not need a huge additional feature set.
kairos has been used several places by now (mid-2021) so v1.0 it is.
Just add
dep_kairos = https://github.com/lehoff/kairos <version>
to your Makefile and it should be fine.
If you are using rebar to build your project you should add the following to your dependencies:
{kairos, {git, "git://github.com/lehoff/kairos.git", {tag, <version>}}}
A: kairos comes with a property based testing suite that validates that the timers can be started, stopped and restarted as expected and that they expire as expected.
A: chronos was already taken on hex.pm
and given that Kairos in ancient Greek means "the right, critical, or opportune moment" is seemed like a good replacement.