I work on an application that tracks users and we do exactly what you are specifying. It will aggregate certain values for the user and it will shut down after 60 seconds. There is a callback for GenServers handle_continue/2 that you can use to set and reset timers for the process. Make sure you start the processes under a DynamicSupervisor and give them a restart strategy of :temporary
Example:
def MyProc do
use GenServer, restart: :temporary
@timeout :timer.seconds(60)
def start_link(opts) do
GenServer.start_link(__MODULE__, [], opts)
end
def init([]) do
# This will set the initial state and continue to set_timer
{:ok, %{timer: nil}, {:continue, :set_timer}}
end
def poke(pid) do
GenServer.call(pid, :poke)
end
def do_work(pid) do
GenServer.cast(pid, :do_work)
end
def handle_call(:poke, _from, :state) do
# We have received a call. Do the work and continue to set_timer
IO.puts("ouch")
{:reply, :ok, state, {:continue, :set_timer}}
end
def handle_cast(:do_work, state) do
IO.puts("Working...")
# You can continue from handle_cast/2 & handle_info/2 and even handle_continue/2
{:noreply, state, {:continue, :set_timer}}
end
def handle_info(:graceful_stop, state) do
IO.inspect("PID #{inspect(self()} received inactivity shutdown. Bye!")
{:shutdown, :normal, state}
end
def handle_continue(:set_timer, state) do
# Forget the old timer if it exists
case state do
%{timer: timer} when is_reference(timer) -> :timer.cancel(timer)
_ -> nil
end
# Send this process the stop message after the timeout expires
timer_ref = Process.send_after(self(), @timeout, :graceful_stop)
{:noreply, %{state | timer: timer_ref}}
end
end
The handle_continue callback is useful for many reasons. First, it will not block the calling process on your init/1 callback. Any heavy initialization of the GenServer should be done with handle_continue. Second, it allows processing to continue work after the process has replied to the caller. For example, we would not want to make the user wait on a response from this process because we are resetting the timer. Third, it allows for code to be shared easily. All you have to do is put {:continue, :set_timer} after each action that indicates the process should remain alive and you can be confident that the timeout will be extended.