Understanding GenServer

Table of Contents

This article is about how Elixir GenServer work rather than how to use it. If you are interested in knowing how to use GenServer, I would highly recommend the getting started guide and Programming Elixir by Dave Thomas.

Recently my friend came up with a very interesting question about GenServer: why the server functions (handle_call, handle_cast etc) are called as callbacks. This sounded simple at first -they are invoked when the GenServer receives a message- but when I tried to explain this to him, I realized there's lot more than meets the eye and I don't understand it all. So I decided to try to write1 my own version of GenServer to learn how Elixir GenServer could have been implemented.

1 Basics

Let's start with a simple sender-receiver process setup, after all GenServer is just a shiny interface on top of state storing process along with some more features such as tracing and ability to fit in nicely with supervision tree.

defmodule OddServer do

  @doc """
  Spawns server and initializes current number to 1.
  """
  @spec init() :: pid
  def init() do
    spawn(__MODULE__, :run, [1])
  end

  @doc """
  Provided as callback to spawn, stores state and calls itself recursively.
  """
  def run(current) do
    receive do
      {:next, pid} ->
        send pid, current
        run(current + 2)

      {:skip} ->
        run(current + 2)

      _ ->
        IO.puts "Non sense message received."
        run(current)
    end
  end
end

OddServer is a simple process which replies with next odd integer to the pid which is sent to it. It will also skip a odd integer if message {:skip} is received. Let's quickly check if it does what we hope it does:

# you can save the above snippet as oddserver.ex and fire up `iex oddserver.ex`

oddserver = OddServer.init()
send oddserver, {:next, self()}
receive do
  n -> IO.puts "Received #{n}"
end

Received 1
:ok

To check if this really works, we should get next integer as 3. But interacting with our OddServer is very cumbersome. Let's pack this interaction into our OddServer as client API, similar to what we write for GenServer. This will not only make using OddServer easier, it will also hide internal working of OddServer for good, so that we can safely change how odd integers are generated if we want to.

defmodule APIOddServer do
  ## Client API
  def next(oddserver) do
    send oddserver, {:next, self()}
    receive do
      n -> n
    end
  end

  def skip(oddserver) do
    send oddserver, {:skip}
    :ok
  end

  # rest of the functions remain same as `OddServer`

  @doc """
  Spawns server and initializes current number to 1.
  """
  @spec init() :: pid
  def init() do
    spawn(__MODULE__, :run, [1])
  end

  @doc """
  Provided as callback to spawn, stores state and calls itself recursively.
  """
  def run(current) do
    receive do
      {:next, pid} ->
        send pid, current
        run(current + 2)

      {:skip} ->
        run(current + 2)

      _ ->
        IO.puts "Non sense message received."
        run(current)
    end
  end
end

Let's see if consequent integers are odd:

oddserver = APIOddServer.init()

IO.inspect APIOddServer.next(oddserver)
IO.inspect APIOddServer.next(oddserver)
IO.inspect APIOddServer.next(oddserver)

IO.inspect APIOddServer.skip(oddserver)

IO.inspect APIOddServer.next(oddserver)
1
3
5
:ok
9

Much better!

2 OddServer using GenServer

Now that we have a simple state storing process that has a nice interface, let's write similar server using GenServer:

defmodule OddGenServer do
  use GenServer

  ## Client API
  def next(oddserver) do
    GenServer.call(oddserver, :next)
  end

  def skip(oddserver) do
    GenServer.cast(oddserver, :skip)
  end

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

  ## Callbacks
  def handle_call(:next, _from, current) do
    {:reply, current, current + 2}
  end

  def handle_cast(:skip, current) do
    {:noreply, current + 2}
  end
end

Note that OddGenServer has same API as APIOddServer; so we can use exact same lines of code to test OddGenServer:

{:ok, oddserver} = OddGenServer.start_link()

IO.inspect OddGenServer.next(oddserver)
IO.inspect OddGenServer.next(oddserver)
IO.inspect OddGenServer.next(oddserver)

IO.inspect OddGenServer.skip(oddserver)

IO.inspect OddGenServer.next(oddserver)
1
3
5
:ok
9

3 SimServer

You may have already noticed striking similarities between the basic process APIOddServer and OddGenServer. If we just compare what code we had to write to get an interface to APIOddServer, we can guess code for GenServer.call and GenServer.cast:

def call(pid, msg) do
  send pid, {msg, self()}
  receive do
    result -> result
  end
end

def cast(pid, msg) do
  send pid, msg
  :ok
end

And now we just have to find out how the receive loop is implemented. Let's start with :next: when the process receives :next, it should invoke the callback, send the result and continue. Where as in the case of :skip, the process doesn't need to send anything back. The message given by user is not enough to know if the client is waiting for a reply, call and cast should add more information to the message.

We can easily find out what message is sent by GenServer.call and GenServer.cast, by making it send message to an echoing process.

defmodule Echo do
  def run do
    receive do
      msg -> IO.inspect msg
    end
  end
end

echo_pid = spawn(Echo, :run, [])
GenServer.cast(echo_pid, :skip)
GenServer.call(echo_pid, :next)
{:"$gen_cast", :skip}
{:"$gen_call", {#PID<0.130.0>, #Reference<0.1258433368.354680834.72997>}, :next}

And that's it! we have enough ingredients to bake our own GenServer, let's call it SimServer -if GenServer is general, SimServer is simple!

defmodule SimServer do

  def call(pid, msg) do
    send pid, {:"$gen_call", self(), msg}
    receive do
      reply -> reply
    end
  end

  def cast(pid, msg) do
    send pid, {:"$gen_cast", msg}
    :ok
  end

  def start(start_state) do
    spawn(__MODULE__, :run, [start_state])
  end

  def run(state) do
    receive do
      {:"$gen_call", from, msg} ->
        {reply, new_state} = handle_call(msg, from, state)
        send from, reply
        run(new_state)

      {:"$gen_cast", msg} ->
        {new_state} = handle_cast(msg, state)
        run(new_state)

      # everything else handled as `handle_info`
      msg ->
        {new_state} = handle_info(msg, state)
        run(new_state)
    end
  end
end

It might look that we are done with the implementation, but there is still one last thing left: providing default implementation for handle_call, handle_cast and handle_info. At this point I did some cheating and looked into source code of GenServer to see how default implementations are provided. That is done is using hook and quote which helps in treating code as data. Since this looks a bit our of scope of what we set out to do, I'm going to skip that.

Along with the __using__ hook which is invoked when use is called, I saw @callback directive, which is used to define behaviours. According to the documentation:

Behaviours in Elixir (and Erlang) are a way to separate and abstract the generic part of a component (which becomes the behaviour module) from the specific part (which becomes the callback module).

A behaviour module defines a set of functions and macros (referred to as callbacks) that callback modules implementing that behaviour must export…

So, this explains our original question: why the functions handle_call, handle_cast are called as callbacks -because they have to be implemented in the callback modules. This has nothing to do with function being passed!

Anyhow, we should complete the task we started and complete our toy GenServer.

defmodule SimServer do

  @callback handle_call(any, pid, any) :: {atom, any}
  @callback handle_cast(any, any) :: {any}
  @callback handle_info(any, any) :: {any}

  def call(pid, msg) do
    send pid, {:"$gen_call", self(), msg}
    receive do
      reply -> reply
    end
  end

  def cast(pid, msg) do
    send pid, {:"$gen_cast", msg}
    :ok
  end

  def start(module, start_state) do
    spawn(__MODULE__, :run, [module, start_state])
  end

  def run(module, state) do
    receive do
      {:"$gen_call", from, msg} ->
        {reply, new_state} = module.handle_call(msg, from, state)
        send from, reply
        run(module, new_state)

      {:"$gen_cast", msg} ->
        {new_state} = module.handle_cast(msg, state)
        run(module, new_state)

      # everything else handled as `handle_info`
      msg ->
        {new_state} = module.handle_info(msg, state)
        run(module, new_state)
    end
  end
end

Now let's implement a OddServer using SimServer:

defmodule OddSimServer do
  @behaviour SimServer

  ## Client API
  def next(oddserver) do
    SimServer.call(oddserver, :next)
  end

  def skip(oddserver) do
    SimServer.cast(oddserver, :skip)
  end

  def start() do
    SimServer.start(__MODULE__, 1)
  end

  ## callbacks
  def handle_call(:next, _from, current) do
    {current, current + 2}
  end

  def handle_cast(:skip, current) do
    {current + 2}
  end

  def handle_info(_msg, state) do
    IO.puts "Non sense message received"
    {state}
  end
end

And finally let test our implementation using the same lines of code:

oddserver = OddSimServer.start()

IO.inspect OddSimServer.next(oddserver)
IO.inspect OddSimServer.next(oddserver)
IO.inspect OddSimServer.next(oddserver)

IO.inspect OddSimServer.skip(oddserver)

IO.inspect OddSimServer.next(oddserver)
1
3
5
:ok
9

Footnotes:

1

Another way is to read how exactly it is implemented on Github (which is conviniently point to by the documentation)

Creative Commons License

Date: 2018-01-07

Author: Anurag Peshne

Emacs 25.2.2 (Org mode 9.1.14)

Validate