How to perform actions periodically with Erlang's gen_server?
Asked Answered
C

4

37

I want to start a gen_server that additionally, will perform one action every minute.

What is the best way to schedule that?

Ceto answered 4/5, 2011 at 12:55 Comment(0)
U
60

You have two easy alternatives, use timer:send_interval/2 or erlang:send_after/3. send_interval is easier to setup, while send_after (when used in the Erlang module) is more reliable since it is a built-in function, see the Efficiency Guide.

Using send_after also ensures that the gen_server process is not overloaded. If you were using the send_interval function you would get a message regardless if the process can keep up or not. With send_after being called just before the return in handle_info you only schedule a new message once you handled the previous one. If you want more accurate time tracking you can still schedule a send_after with the time set dynamically to something lower than ?INTERVAL (or even 0) to catch up.

I would recommend something along the following lines in your gen_server:

-define(INTERVAL, 60000). % One minute

init(Args) ->
   ... % Start first timer
   erlang:send_after(?INTERVAL, self(), trigger),
   ...

handle_info(trigger, State) ->
   ... % Do the action
   ... % Start new timer
   erlang:send_after(?INTERVAL, self(), trigger),
   ...

Instead of trigger you could send something with a state if it is needed, like {trigger, Count} or something.

Unseal answered 4/5, 2011 at 13:22 Comment(5)
That's great! It's exactly what I'm doing at the moment. Thanks!Ceto
Isn't this going to get out of sync eventually, because the timer fires every 6000 + <duration of the action> ms?Grisaille
@PatrickOscity Sure, it might. If you want to be really sure, you can figure out a future time to schedule it to and calculate the exact amount of milliseconds to send after instead.Unseal
That might work, however it won't be helpful when the action runs longer than the duration of the interval I guess. I guess you would need a different approach for this kind of situation. Maybe you would need to start the next timer directly at the beginning of handle_info, before the actual action happens. Of course this means subsequent actions might overlap.Grisaille
@PatrickOscity Either you're okay with the actions running longer than their interval, in that case you don't need to change anything. If you're not okay with that, your interval is too short or you need to implement some skipping/locking mechanism.Unseal
G
7

To precisely control the timer, you may want to use erlang:start_timer, and save each timer reference you have created.

erlang:start_timer has a tiny difference with erlang:send_after, see http://www.erlang.org/doc/man/erlang.html#start_timer-3 and http://www.erlang.org/doc/man/erlang.html#send_after-3

Example use case:

init(Args) ->
    ...
    TRef = erlang:start_timer(?INTERVAL, self(), trigger),
    State = #state{tref = TRef},
    ...

handle_info({timeout, _Ref, trigger}, State) ->
    %% With this cancel call we are able to manually send the 'trigger' message 
    %% to re-align the timer, and prevent accidentally setting duplicate timers
    erlang:cancel(State#state.tref),
    ...
    TRef = erlang:start_timer(?INTERVAL, self(), trigger),
    NewState = State#state{tref = TRef},
    ...

handle_cast(stop_timer, State) ->
    TRef = State#state.tref,
    erlang:cancel(TRef),

    %% Remove the timeout message that may have been put in our queue just before 
    %% the call to erlang:cancel, so that no timeout message would ever get 
    %% handled after the 'stop_timer' message
    receive
        {timeout, TRef, _} -> void
        after 0 -> void
    end,
    ...
Guff answered 13/9, 2012 at 6:27 Comment(0)
L
1

There is actually a built-in mechanism within gen_server to accomplish the same thing. If the third element of response tuple of the init, handle_call, handle_cast or handle_info methods in the gen_server is an integer, a timeout message wil be sent to the process after that period of time in millisecs... which should be handled using handle_info. For eg :

init(Args) ->
   ... % Start first timer
   {ok, SomeState, 20000}. %% 20000 is the timeout interval

handle_call(Input, From, State) ->
   ... % Do something
   ... % Do something else
   {reply, SomeState, 20000}. %% 20000 is the timeout interval

handle_cast(Input, State) ->
   ... % Do something
   ... % Do something else
   {noreply, SomeState, 20000}. %% 20000 is the timeout interval


%% A timeout message is sent to the gen_server to be handled in handle_info %%
handle_info(timeout, State) ->
   ... % Do the action
   ... % Start new timer
   {noreply, SomeState, 20000}. %% "timeout" can be sent again after 20000 ms

Lahomalahore answered 5/5, 2011 at 3:30 Comment(3)
That is true. Although it means that you have to manipulate the timeout, which might not be a bad idea.Ceto
timeout is not intended for periodic execution. It is intended to launch some action or terminate when there nothing happen in this period. This timeout is terminated by each action even by some system (sys, proc_lib, ...) actions. Shortly, using timeout is discouraged to rely on except some "maintenance" stuff, like automatic termination or cleanups.Trickle
@Lahomalahore In docs its written If an integer time-out value is provided, a time-out occurs unless a request or a message is received within Timeout milliseconds. This will fail if gen_server is receiving messages.Traps
L
0

There is also the timer module, which could be used.

http://erldocs.com/R14B02/stdlib/timer.html?i=8&search=timer#cancel_timer/1

Ladybird answered 6/5, 2011 at 6:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.