GenServer handle_continue/2

March 20, 2022

  • elixir

I recently learned about a delightful new(ish) feature in Elixir’s GenServer that managed to slip under my radar. This feature is the handle_continue callback it has been around since Elixir 1.7 requires OTP 21+ to work.

A common anti-pattern when working with GenServers is putting slow setup code in the GenServer’s init function (which is synchronous) causing a delayed start of the server. One workaround is to send a message to ourselves from the server and catch it in handle_info so that we can do the heavy work asynchronously and update the state.

In a past project I did it like this with Process.send_after:

defmodule FrontLine.CameraWorker do
  use GenServer
  require Logger

  @frame_rate 30
  @frame_interval 1000 / @frame_rate |> trunc()
  @img_width 640

  def start_link do
    GenServer.start_link(__MODULE__, [])
  end

  def init([]) do
    Picam.set_size(@img_width, 0)
    Picam.set_hflip(true)

    Process.send_after(self(), :next_frame, @frame_interval)
    {:ok, []}
  end

  def handle_info(:next_frame, state) do
    data = Picam.next_frame() |> Base.encode64()
    UiWeb.Endpoint.broadcast("video:lobby", "next_frame", %{base64_data: data})
    Process.send_after(self(), :next_frame, @frame_interval)
    {:noreply, state}
  end
end

To use handle_continue you will need to add a {:continue, _something} tuple to the end of your return tuple from init. To illustrate an example, let’s say we need to fetch a few records from the database so that we can hydrate our initial state for the GenServer, we can now do it this way:

defmodule MyServer do
  use GenServer

  @impl GenServer
  def init(args) do
    {:ok, args, {:continue, :fetch_records}}
  end

  @impl GenServer
  def handle_continue(:fetch_records, state) do
    new_state = Map.put(state, :records, fetch_records())
    {:noreply, new_state}
  end
end

The return type is the same as handle_cast and you can even chain multiple continue operations in a row, for example:

defmodule MyServer do
  use GenServer

  @impl GenServer
  def init(args) do
    {:ok, args, {:continue, :fetch_records}}
  end

  @impl GenServer
  def handle_continue(:fetch_records, state) do
    new_state = Map.put(state, :records, fetch_records())
    {:noreply, new_state, {:continue, :compute_meaning_of_life}}
  end

  @impl GenServer
  def handle_continue(:compute_meaning_of_life, state) do
    new_state = Map.put(state, :meaning_of_life, 40 + 2)
    {:noreply, new_state}
  end
end

Another overlooked use case is to allow handle_call to return a value and do some async work in the handle_continue without making the caller wait:

  @impl GenServer
  def handle_call(:compute, _ref, state) do
    IO.puts("in compute")
    {:reply, 42, state, {:continue, :post_compute}}
  end

  @impl GenServer
  def handle_continue(:post_compute, state) do
    IO.puts("in post_compute")
    Process.sleep(1000)
    IO.puts("finished post_compute")
    {:noreply, state}
  end

Calling the :compute function will return the result immediately and then perform the :post_compute in handle_continue.

iex(1)> GenServer.call(pid, :compute)
in compute
in post_compute
42
iex(2)>
finished post_compute

To read more visit the official documentation here: https://hexdocs.pm/elixir/GenServer.html#c:handle_continue/2


© 2022, Dorian Karter