How to synchronize the asynchronous within a GenServer

Notes

Since Elixir’s processes communicate via message-passing, most communication happens asynchronously.

Sometimes we want to synchronize operations. How can we do that?

The basic way to do that is to send a message and immediately wait for a reply (in a receive block). But what can we do when we’re using a GenServer? In those cases, we shouldn’t use a receive block – we rely on handle_info/2 instead.

Setting the stage

Suppose you have an asynchronous process that your app uses. In this case, let’s pretend it’s a websocket library. Here’s a sample code that acts asynchronously (but it’s most definitely not a websocket 😃):

defmodule WebSocket do
  use GenServer

  def start_link(caller) do
    GenServer.start_link(__MODULE__, caller)
  end

  def send_message(pid, msg) do
    GenServer.cast(pid, {:send_message, msg})
  end

  def init(caller) do
    {:ok, %{caller: caller}}
  end

  def handle_cast({:send_message, msg}, state) do
    response = "#{msg} to you too!"
    send(state.caller, {:message_received, response})
    {:noreply, state}
  end
end

Now, suppose your application wraps that service with a GenServer to manage the connection. Let’s say this is your GenServer (which I call Coordinator due to lack of creativity):

defmodule Coordinator do
  use GenServer

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

  def greet(pid, msg) do
    GenServer.call(pid, {:greet, msg})
  end

  def init(_opts) do
    {:ok, pid} = WebSocket.start_link(self())
    {:ok, %{async_pid: pid}}
  end

  def handle_call({:greet, msg}, from, state) do
    :ok = WebSocket.send_message(state.async_pid, msg)
    {:reply, :ok, state}
  end

  def handle_info({:message_received, msg}, state) do
    # How do we send the message to the caller?
    dbg(msg)
    {:noreply, state}
  end
end

Our goal

What we want is for the rest of our code to be able to use the Coordinator.greet/2 function in a way that seems synchronous.

This is our desired usage:

response = Coordiantor.greet(pid, "Hello world")
# => "Hello world to you too!"

The problem

Currently, our Coordinator.greet/2 function just returns :ok. And later (in an async fashion), our handle_info/2 callback gets called with the websocket response. At that point, we’ve already replied to the caller of greet/2.

So, how can we make our GenServer’s API seem synchronous?

The solution

Enter GenServer.reply/2.

We can store the from value from our handle_call/3 callback in our state, and change our reply tuple from a :reply 3-tuple, to a :noreply 2-tuple.

defmodule Coordinator do
  # other code

  def handle_call({:greet, msg}, from, state) do
    :ok = WebSocket.send_message(state.async_pid, msg)
    {:noreply, Map.put(state, :caller, from)}
  end
end

Then, in our handle_info/2 callback, when we receive the response message, we can reply to the caller with GenServer.reply/2:

defmodule Coordinator do
  # other code

  def handle_call({:greet, msg}, from, state) do
    :ok = WebSocket.send_message(state.async_pid, msg)
    {:noreply, Map.put(state, :caller, from)}
  end

  def handle_info({:message_received, msg}, state) do
    GenServer.reply(state.caller, msg)
    {:noreply, state}
  end
end

Now, the caller of Coordinator.greet/2 will receive the response message as the return value of greet/2 (like a regular function call), and it knows nothing (nor should it care) about the asynchronous process required to send the message and receive the response.

Want the latest Elixir Streams in your inbox?

    No spam. Unsubscribe any time.