Lucid Simple

Simple OTP Idioms: using handle_info (Part 1)

This week’s post is a 5-minute read.

This is the first of a series of posts on simple OTP idioms, patterns and tricks.

We’ll start with a few posts on handle_info, each week covering some some easy pattern that can be covered in a few minutes.

This week we’ll talk about how to use handle_info to slightly defer the initialization of an expensive to set up server.

A simple example

You’re writing an application in Elixir and you’ve got a GenServer that is costly to initialize:

defmodule MyServer do
  use GenServer

  def init([]) do
    state = something_slow()

    {:ok, state}
  end
end

The problem with this is that ‘MyServer.start_link’ has to block until ‘init’ finishes.

If ‘init’ is slow, then any caller trying to create an instance of ‘MyServer’ will be temporarily blocked.

The issue is made worse if there are multiple servers set up this way because OTP Supervisors start each of their children serially. So if you have 10 child servers that each take 6 seconds to initialize, then the supervisor will be blocked for a minute while it tries to get each one started.

We’d like to avoid this situation if we can.

One possible solution

A common workaround is to trigger a timeout from init so that start_link will return immeditely. The expensive initialization code is then moved to a handle_info callback:

defmodule MyServer do
  use GenServer

  def init([]) do
    {:ok, [], 0}
  end

  def handle_info(:timeout, _state) do
    state = something_slow()

    {:noreply, state}
  end
end

Note that the return tuple from init has a 3rd element, which is 0. In this case, 0 simply means “timeout immediately”.

The timeout will trigger the handle_info callback allowing you to do your drawn out setup out of band without making everyone else wait.

I’ve received conflicting information on whether or not the timeout handler will be guaranteed to be the first message handled by your server.

If it’s not, and your server receives a different message first, then your server will most likely crash because its state will not have been fully initialized at that point.

This seems unlikely, although it might be possible.

A similar approach

It’s worth noting that you don’t have to use timeout to achieve similar results:

defmodule MyServer do
  use GenServer

  def init([]) do
    send self(), :my_fancy_setup

    {:ok, []}
  end

  def handle_info(:my_fancy_setup, _state) do
    state = something_slow()

    {:noreply, state}
  end
end

As you can see from above, the real mechanism for deferred initialization isn’t the timeout - it’s using init to stuff a message into the mailbox that tells it to finish its initialization next.

Triggering a timeout is just one means to that; directly using send to queue up a post initialization function (also via the handle_info callback) is another.

Which approach is best?

If the state of your server needs to be periodically updated on a schedule (e.g a Cache), then setting things up with timeouts might be the way to go.

For everything else, the second approach is probably best.

You’ll see several variations of these techniques in open source projects if you hunt for them (see the links below).

You don’t have to use them yourself, but it can save you time if you know how to spot them.

Closing

Here are some relevant examples of handle_info that you might be interested in:

If you read through the examples, it will be worth your time to checkout how each ‘init’ function is setup. A few are explicitly setting timeouts, while others use different approaches.

Next week’s post will cover using handle_info for looping and polling.

Thanks for following along.

– Jim