homanchou
Is there a way to self terminate a genserver after no activity?
When using simple processes we can spawn a recursive loop with a timeout like this:
iex> receive do
...> {:hello, msg} -> msg
...> after
...> 1_000 -> "nothing after 1s"
...> end
after 1s the process will exit.
Is there a similar way of shutting down a genserver after a period of inactivity?
The longer explanation of why I might want this is that I’m looking at using at genservers as a way to load up aggregate instances in domain driven design/cqrs/event-sourcing (Bryan Hunter’s talk: https://vimeo.com/97318824). Each aggregate is initialized with the state of some business entity, they accept commands, they update state, etc. In his talk he loads ups a simple process for each aggregate with a 45 second timeout, but mentions when going to production maybe we want to use genserver or genFSM. I don’t really need these aggregates supervised since they can recreated whenever we need to submit a command (can stick the process into a registry for serialized access).
Marked As Solved
jwarlander
During initialization, you can return with a timeout (see GenServer docs). Same for handle_call etc. Then just take care of the timeout event by, eg., stopping… Or whatever you need to do 
Also Liked
sasajuric
Keep in mind that the timeout is reset when a new message arrives, and you need to set it again explicitly. If you forget to do that in just one of handle_* clauses, the timeout might never happen.
lucaong
Or even, do exactly what @1player suggested, but instead of scheduling the :check_timeout message every second:
-
Upon every call that counts for the timeout, update the
last_activetimestamp in the state, and send a:check_timeoutdelayed message to the GenServer itself withProcess.send_after/4and a timeout equal to the desired inactivity timeout -
Handle the
:check_timeoutmessage withhandle_info, checking if enough time elapsed sincelast_active. If so, return{:stop, :normal, state}, otherwise{:noreply, state}.
It is basically the same, but it removes the need to poll every second/minute/etc.
OvermindDL1
Not just deflate, it does indeed run a GC over it but it also throws away the stack above the current call context, but it will still receive and process messages as normal after that. Hibernation does not terminate at all, just think of it as running a GC in such a way that you can’t return from any current function (which thankfully a genserver handles for you). ^.^
1player
If I understand correctly, what you or @silviurosu want is:
- Terminate the GenServer after X if no messages are received
- Periodically refresh the GenServer state, but do not count this timer in the process timeout.
Very naively, I would avoid using the GenServer timeout feature, but just using a timestamp and a check_timeout message.
Store and update a last_active timestamp when the process has last done some work. Have a check_timeout message sent every second (or minute or hour), and comparing the current timestamp with last_active, decide whether you should terminate the server or not.
This way you do not have to cancel and recreate a timer each time, and just update last_active everywhere except when receiving that refresh message.
silviurosu
I have aded a small snippet and the explanation below:
defmodule Carts.Service.Restaurant do
use GenServer
@restaurant_timeout 15_000
@refresh_timeout 10_000
def handle_continue(:init, id) do
state = load_restaurant_state(id)
# send messages to self from time to time to refresh state in restaurant has been changed
trigger_state_update_call()
# send messages to self to timeout the process
state = trigger_timeout_call(state)
{:noreply, state}
end
def handle_call({:any_message}, _from, state) do
#handle any_message logic
state = trigger_timeout_call(state)
{:reply, resp, state, restaurant_timeout()}
end
def handle_info({:refresh_state}, state) do
# load restaurant state
state = if state_changed?(state), do: reload_state(), else: state
trigger_state_update_call()
{:noreply, state}
end
def handle_info(:timeout, state) do
if state.idle_timer, do: Process.cancel_timer(state.idle_timer)
{:stop, :normal, state}
end
defp trigger_state_update_call do
Process.send_after(self(), {:refresh_state}, @refresh_timeout)
end
defp trigger_timeout_call(state) do
if state.idle_timer, do: Process.cancel_timer(state.idle_timer)
idle_timer = Process.send_after(self(), :timeout, @restaurant_timeout)
%{state | idle_timer: idle_timer}
end
end
When the process starts I init two timeouts to self, one to refresh the state and one to timeout the genserver.
Refresh state timeout periodically checks for changes, updates state and triggers another refresh. This is independent of anything else from the genserver.
Timeout timer is the same with the difference that if any other message comes to the process I cancel the old timer and start it again. So as long as the genserver will receive calls it will start the timeout again. If there is no call during the timeout period it will execute the :timeout message and stop.
I hope is clear.







