homanchou

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

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

Also Liked

sasajuric

sasajuric

Author of Elixir In Action

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

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

OvermindDL1

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

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

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.

Where Next?

Popular in Questions Top

LegitStack
I’m hoping you guys can give me some general advice and perhaps code examples if you’re feeling up to it. I’m very interested in Elixir,...
New
Kagamiiiii
Student & New to elixir. Nice language. I want to convert a english character, e.g. “a”, which is stored in a variable, to it’s asci...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New

Other popular topics Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35421 110
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
New

We're in Beta

About us Mission Statement