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).

1 Like

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 :slight_smile:

8 Likes

Oh brilliant! So in init I just need to return {:ok, state, timeout}? So easy!

I was going to do Process.send_after(self(), :exit, 1_000), store that timer reference in the state and then cancel_timer and reset the timer whenever a command was received.

Thanks!

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.

5 Likes

You may want to check out this other thread, which speaks to this. In it, @lehoff mentions:

Do note that the timeout is cancelled whenever a message is received.

Ah, I see @sasajuric has just mentioned the same thing. That thread is still a good resource for a discussion on how exactly to implement the timers if you handle them manually.

Ok, I think I finally get it now.

In init I can return {:ok, state, timeout}, and if this genserver does not receive any messages it will NOT just exit, but send itself a :timeout message. This must be handled in the the handle_info(:timeout, _state) in which I can then return a {:stop, :normal, state} which WILL cause the genserver to exit.

If the genserver receives a new message the original timeout is canceled. In order to renew it, the message handler (handle_info | handle_cast | handle_call) must return the timeout in a similar fashion that init originally did.

1 Like

On the same note, is there a way to make an Agent automatically timeout? I’d assume so but just figured this would be the place to ask.

1 Like

Is this particular implementation working for you, or if you haven’t implemented it yet, could you let us know if it does/doesn’t?

I went with the dead simple timeout mentioned in the other thread as a temporary kluge as I was busy working on other aspects. I can do this because I do not have aggregates proper, as each ibGib process is much like an immutable snapshot of an aggregate’s state. So they are extremely cheap, as any work done that would normally be done in the aggregate process’ command handler is actually done in the “next state” process’ init (or just after). This allows for “branching aggregates” so to speak, but I digress…

But it would be awesome to have the dynamic timeout and your feedback may spur me to go ahead and implement it. :smile:

Resurrecting because I’m doing something similar … I found this: Erlang -- gen_server

If option {hibernate_after,HibernateAfterTimeout} is present, the gen_server process awaits any message for HibernateAfterTimeout milliseconds and if no message is received, the process goes into hibernation automatically (by calling proc_lib:hibernate/3).

I think we can use it like this:

GenServer.start_link(__MODULE__, args, hibernate_after: 10_000)

While hibernation isn’t quite the same as termination, it apparently does ‘deflate’ the process memory usage down to a bare minimum.

1 Like

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). ^.^

1 Like

I have a similar situation that I do not know how to handle.
I have a process that needs to be stopped after a timeout. This can be easily accomplished using the additional timeout on reply. But I also have a periodic timer that sends messages to itself from time to time to refresh state from DB (it can change from another app). I accomplish this by:

defp trigger_state_update_call do
    Process.send_after(self(), {:refresh_state}, refresh_timeout())
end

and then handle the messages like this:

 def handle_info({:refresh_state}, %RestaurantState{id: id, updated_at: updated_at} = state) do
      {:ok, db_updated_at} = restaurants_domain().load_restaurant_updated_at(id)
state =
  if NaiveDateTime.compare(updated_at, db_updated_at) != :eq do
    load_restaurant_state(id)
  else
    state
  end

trigger_state_update_call()
{:noreply, state}

end

Since from the refresh call I do not add the timeout it will never be closed. Also the refresh timeout is smaller than the process timeout.
Basically this refresh messages I do not want to be counted on timeout time, to be ignored somehow.
Can I accomplish this without creating a manual timer that I need to cancel and recreate on each call?

1 Like

Since from the refresh call I do not add the timeout it will never be closed. Also the refresh timeout is smaller than the process timeout.
Basically this refresh messages I do not want to be counted on timeout time, to be ignored somehow.
Can I accomplish this without creating a manual timer that I need to cancel and recreate on each call?

Hi @silviurosu, I am now encountering exactly the same issue. Do you remember how you solved it?

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.

2 Likes

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_active timestamp in the state, and send a :check_timeout delayed message to the GenServer itself with Process.send_after/4 and a timeout equal to the desired inactivity timeout

  • Handle the :check_timeout message with handle_info, checking if enough time elapsed since last_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.

4 Likes

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.

2 Likes

Is this essential a debounced/throttled (I’m not sure which in the nuance of the two this is) method?

I’m implementing a spellcheck that I want to have triggered X amount of time after last keydown and was looking to use this method. Unless there’s a better way?