How do we implement a reset-able countdown timer with a GenServer?
1) perform a task after fixed amount of time, say every 60 seconds
2) have a way to reset the countdown back to 60 seconds before the timer elapses
I have looked at How to perform actions periodically with Erlang's gen_server? but it doesn't quite cover the aspect of resting the timer before the countdown elapses.
Thanks.
How can I schedule code to run every few hours in Elixir or Phoenix framework? defines how to do a periodic job.
To implement a cancel using that as a base you could do the following:
defmodule MyApp.Periodically do
use GenServer
def start_link(_name \\ nil) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
def reset_timer() do
GenServer.call(__MODULE__, :reset_timer)
end
def init(state) do
timer = Process.send_after(self(), :work, 60_000)
{:ok, %{timer: timer}}
end
def handle_call(:reset_timer, _from, %{timer: timer}) do
:timer.cancel(timer)
timer = Process.send_after(self(), :work, 60_000)
{:reply, :ok, %{timer: timer}}
end
def handle_info(:work, state) do
# Do the work you desire here
# Start the timer again
timer = Process.send_after(self(), :work,60_000)
{:noreply, %{timer: timer}}
end
# So that unhanded messages don't error
def handle_info(_, state) do
{:ok, state}
end
end
This maintains a reference to the timer, allowing it to be cancelled. Every time the :work
message is received, a new timer is created and stored in the state of the GenServer.
If you did it in Erlang, as per the other question you referenced...
You save the timer reference and call erlang:cancel_timer/1 to stop it from firing (if it hasn't already). You have to watch out for this 'fired already' race condition, where the trigger message is already in your message queue when you cancel the timer. You might or might not care about this, but if it's important that you never carry out the action after the trigger has been cancelled then you need to create a reference (or you could use a counter) when you setup a timed message, and when you get a trigger you have to check that it relates to the latest one.
The code from the other question then becomes:
-define(INTERVAL, 60000). % One minute
init(Args) ->
...
% Start first timer
MyRef = erlang:make_ref(),
{ok, TRef} = erlang:send_after(?INTERVAL, self(), {trigger, MyRef}),
...
{ok, State#your_record{
timer = TRef,
latest = MyRef
}}.
% Trigger only when the reference in the trigger message is the same as in State
handle_info({trigger, MyRef}, State = #your_record{latest=MyRef}) ->
% Do the action
...
% Start next timer
MyRef = erlang:make_ref(),
{ok, TRef} = erlang:send_after(?INTERVAL, self(), trigger),
...
{ok, State#your_record{
timer = TRef,
latest = MyRef
}}.
% Ignore this trigger, it has been superceeded!
handle_info({trigger, _OldRef}, State) ->
{ok, State}.
And something like this to reset the timer:
handle_info(reset, State = #your_record{timer=TRef}) ->
% Cancel old timer
erlang:cancel_timer(TRef),
% Start next timer
MyNewRef = erlang:make_ref(),
{ok, NewTRef} = erlang:send_after(?INTERVAL, self(), trigger),
{ok, State#your_record{
timer = NewTRef,
latest = MyNewRef
}}.
A call might be more appropriate for the cancel function, but that depends on your app so it's up to you.
Technically, it's not necessary to cancel the timer, because once you've created a new state with your new reference, even if the old timer carries on, it'll be ignored when it fires, but I think it's best to tidy up really.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With