GenServer usage of continue

I learn that for this code below it is better to use continue to load info as doing this way does not guarantee that :load_info will be the first message processed…
But i don’t see how it won’t be the first message. init has to finish execution before any client has the pid to query for a “non updated” state. So this should be safe isn’t it?

defmodule Continue do
  use GenServer

  def start_link(default \\ %{}) when is_map(default) do
    GenServer.start_link(__MODULE__, default)
  end

  @impl true
  def init(state) do
    send(self(), :load_info)
    {:ok, state}
  end

  @impl true
  def handle_info(:load_info, state) do
    Process.sleep(10_000)
    new_state = Map.put(state, :info, :loaded_info)
    {:noreply, new_state}
  end

  @impl true
  def handle_call(:state, _from, state) do
    {:reply, state, state}
  end
end

Believe me, it isn’t safe. If it was handle_continue wouldn’t have been necessary.

Also you could use a named GenServer, you can send messages to it as soon as it got spawned, and as init already runs in the new process, it can already receive messages into the inbox.

1 Like

Init isn’t even the first thing that gets called after the process is spawned; there stuff in :gen_server, and stuff in an even stranger module called :gen. There is quite a bit of time between spawning and the first statement of init, for a stray message to come in and mess up your plans to be first.

3 Likes

I started getting curious on this because i tried to see if i can get say empty map by using the stated code and i can’t. Even when i use a named GenServer and started sending messages GenServer.call(:cont, :state) straight after. It will timeout because of the 10 sec sleep. Meaning :state gets processed after handle_info.

I believe there must be a reason why continue is provided. Just my own curiosity bug biting.

You’d need to simulate considerable load for the timing issue to surface - it’s not likely to show up until you have more processes demanding CPU time than System.schedulers()

1 Like

It is easy to emulate such situation. Just assume that for some reason the action in init/1 is taking a little bit longer than expected:

defmodule Foo do
  def init(state) do
    Process.sleep(1000)
    send(self(), :load_info)
    {:ok, state}
  end

  def handle_info(msg, state) do
    IO.inspect msg, label: :msg
    {:noreply, state}
  end

  # helper loop
  def loop do
    case Process.whereis(Foo) do
      nil -> loop()
      pid -> send(pid, :surprise)
    end
  end
end

Then start the process and in another process send message to it:

iex> t = Task.start(&Foo.loop/0)
{:ok, #PID<0.154.0>}
iex> pid = GenServer.start_link(Foo, [], name: Foo)
msg: :surprise
{:ok, #PID<0.156.0>}
msg: :load_info
10 Likes