What are some patterns to avoid performing heavy lifting tasks in a Genserver's init callback?

Hi everyone, I have been playing with Elixir for a bit and I was told in passing that performing heavy lifting in a Genserver’s init call is an anti-pattern. If so, what are some patterns to do it the right way ?

For example, in the code below, I attempt at sending a message to the process itself and performing the task async. However, it seems like due to some sort of race condition(?), the last line of the handle_info where the process prints out the character count never works.

I am curious about what might be happening here?

defmodule Nine.PartOne do
  use GenServer
  require Logger

  # API
  def start_link(state) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  @impl true
  def init(state) do
    Logger.info("I got started")
    file_path = Application.get_env(:adventofcode, :filepath)
    Process.send(self(), {:solve, file_path}, [])
    {:ok, state}
  end

  @impl true
  def handle_info({:solve, file_path}, _state) do
    IO.puts("I got the message") #this is printed
    res =
      File.read!(file_path)
      |> String.graphemes()
      |> Enum.reduce(
        0,
        fn _x, acc ->
          acc+1
        end
      )
    #this is never printed
    Logger.debug("The result is #{inspect(res)}")
  end
end

Welcome to the forum!

Have a look at handle_continue/2

If you look at init/1:

Returning {:ok, state, {:continue, continue}} is similar to {:ok, state} except that immediately after entering the loop the handle_continue/2 callback will be invoked with the value continue as first argument.


handle_info/2 is supposed to return a value just like handle_cast/2.

7 Likes

The handle_info should return {:noreply, new_state}

Your init is a common traditional method for heavy inits, pre-OTP 21.0. handle_continue is the current method for doing this.

2 Likes

Like others here suggested, the actual problem is that your handle_info does not return a valid result (it returns :ok from the Logger.debug() statement instead of {:noreply, state} or some other expected tuple).

As for handling heavy processing during initialization, handle_continue is indeed the pattern to use (as others here noted too).

Thanks everyone! I was not aware of handle_continue. I will look more into it.