OTP 21 introduces handle_continue callback to GenServer

Posted on September 9, 2018

When using GenServers, it is sometimes necessary to perform long-running code during the initialization. The common pattern for handling this is to send a message to self() from within the init callback, and then perform the long-running work within the handle_info function for the message we just sent:

  
  
    defmodule Server do
  use GenServer

  def init(arg) do
    send(self(), :finish_init)
    state = do_quick_stuff(arg)
    {:ok, state}
  end

  def handle_info(:finish_init, state) do
    state = do_slow_stuff(state)
    {:noreply, state}
  end

  # edited for brevity
end

  
  

By performing only quick operations within init/1 itself, the GenServer can return sooner to the caller (i.e. the caller won’t be blocked unnecessarily). Then, the GenServer will process the :finish_init  message it sent itself and finish its initialization in parallel. Future messages it will receive will then be processed with a fully initialized state.

There is a risk for race conditions, however: clients could have sent messages to the GenServer that arrive before the :finish_init  call. This case is even more likely to happen with named GenServers, as clients could send messages at any time (using only the process’ name as they won’t require to know the pid).

OTP 21 introduces the handle_continue/2 callback. With this concept, when a :continue response is returned by a callback, the corresponding handle_continue/2 function is called, but no messages from the inbox are processed until it has been executed.

Therefore, when using OTP 21, the code above could be changed to

  
  
    defmodule Server do
  use GenServer

  def init(arg) do
    state = do_quick_stuff(arg)
    {:ok, state, {:continue, :finish_init}}
  end

  def handle_continue(:finish_init, state) do
    state = do_slow_stuff(state)
    {:noreply, state}
  end

  # edited for brevity
end

  
  

This way, messages in the process’ mailbox will be ignored until handle_continue(:finish_init, state) is done executing and our state is properly initialized. No race conditions, yay!

This is an OTP feature, therefore being able to use it depends on the OTP version you’re running. However, if you want to be able to @impl the handle_continue/2 callback implementation, you’ll need to be using Elixir 1.7 as the GenServer behaviour from earlier versions doesn’t specify this callback.


Would you like to see more Elixir content like this? Sign up to my mailing list so I can gauge how much interest there is in this type of content.