As you may guess, this article is not about rebuilding the Elixir GenServer. It’s already there and it works great. And that’s what interests me the most: why it works great?

Also, we will not discuss what GenServer does and its generic concepts. If you are not familiar at all with the it, I suggest starting with the docs. They do a great job explaining the basics:

The problem

I’ve been using Elixir for quite a while now. But I always had some doubts on some specific aspect of the GenServers: the asynchronous requests ( handle_cast/2 ). To be more explicit:

is the GenServer state consistent across multiple async requests that take a various amount of time to execute (eg. running an expensive function inside one of the handle_cast/2 implementation)? are the async requests executed in the same order they were cast even if some of the casts take longer than others?

In other words, are GenSeerver async requests susceptible to race conditions?

Let’s find out

My confusion ended when my colleagues pointed me to the Erlang GenServer docs: http://erlang.org/doc/design_principles/des_princ.html#id63247

Among other useful info, you can find also a very basic implementation of the GenServer. I do not have Erlang experience, but I found the code readable enough. So I decided to rebuild it in Elixir, even add some extra features and shed some light on my dilemma.

MyGenserver

MyGenserver , that’s what we’ll call the project:

mix new my_genserver

Before we start, don’t expect that we’ll end up with a fully featured GenServer. There will be many bits missing. But the basic functionality will be there at the end of our little experiment.

start_link / init

The GenServer normally starts with a start_link/3 function. For our experiment we will ignore the options , so it will take only 2 arguments:

We pass a module and arguments to the start_link/2 function. The module will be the one implementing the GenServer behaviour. Let’s call it Example, so we could start the server like this: start_link(Example, :ok) .

spawn/3 creates a new process, running a function server_init/2 which does 2 things:

expects that Example module defines function init/1 . It calls it and expects an {:ok, state} response (congratulations! Our Example can now implement the init/1 callback)

module defines function . It calls it and expects an response (congratulations! Our can now implement the callback) starts a loop/2 function, passing the Example module and the initial state of the server. The loop is the heart of the GenServer and actually answers all my questions above

But let’s not rush into conclusions and continue the implementation.

call / handle_call

The call/2 function takes the MyGenserver pid, and a request. It then sends a message to the pid, with 3 elements tuple:

the action, :call

self() which is the calling process

which is the calling process request - the arguments we want to pass to the server

Inside the loop/2 there is a receive function that listens to those messages and pattern matches on the action. In this case on {:call, _, _} . We then expect that the passed module ( Example ) will implement a handle_call/3 callback, that returns {reply, response, state} .

Call makes a synchronous server request. The calling process is blocked until it receives a response from MyGenserver. So we send back the response with send(parent_pid, {:response, response}) . The call/2 receives and returns the response.

The last thing happening in the loop is to call itself, with the module ( Example ) and the new MyGenserver state.

cast / handle_cast

As said above, the async server request, cast/2 is the actual reason I tried to rewrite this basic version of the GenServer. By this time however things are quite clear.

There’s nothing special about cast . It is very similar to call , except it’s not sending a response back to the “casting” process, and will not block it.

MyGenserver process mailbox manages the order of the functions execution, in a first in first out manner. And it’s sequentially consumed by the loop function, which also keeps the state consistency.

It doesn’t matter if many async requests are cast to the MyGenserver . And it doesn’t matter if some of those requests are expensive, time-consuming functions. They will be executed in the order they were cast. There will be no race conditions between them.

With the mystery solved, let’s quickly write the remaining callbacks.

stop / terminate

The same pattern as above, with one big difference. The loop is interrupted and the process will die with the specified reason.

handle_info

handle_info callback does not have an associated API function. It catches all unmatched messages sent to MyGenserver and updates the state of the server.

The 49 Lines GenServer

Let’s put all of this together and see how it looks: