Understanding GenServer

The center-piece of the Erlang VM is the actor/process model. The creators of the language, have had the foresight to distill the essence of the patterns that engineers routinely do while building software in this system, into re-usable pieces. GenServer is one such example.

Processes 101

While working with processes, it’s good to follow certain practices. There are clients and there are servers. A server is a unit of code that provides some functionality and a client is one that consumes said functionality. Depending on the situation, roles may be reversed, i.e. a server may in turn consume functionality from another server and in that context it is a client.

Let’s kick things off by building a painfully simple and trivialized DNS Sever. You give it a hostname (facebook.com, msn.com etc.) and it gives you an IP (102.12.3.19 for example). Here’s the code:

And here is how we would consume the DNS services this server provides (in iex):

iex(2)> {:ok, dns_server_pid} = SimpleDNS.start
{:ok, #PID<0.134.0>}
iex(3)> SimpleDNS.add_entry dns_server_pid, “www.facebook.com", “10.1.1.1”
:ok
iex(4)> SimpleDNS.get_entry dns_server_pid, “www.facebook.com"
“10.1.1.1”

Adding a new Service

Now, to understand what GenServer really provides, let’s first go through the exercise of adding a new service to the server. Let’s say we want to get a listing of all of the hostname’s and their corresponding IP’s. This new service, we’ll call get_all_entries. Here’s the updated code:

Notice, how the only parts that changed were:

  1. We added the get_all_entries method.
  2. We added a clause in the receive block in the loop method to handle this new request.

And this is basically, all we’d need to do if we were using GenServer. To every service request that comes to our server, the server responds with a response and an updated state.

GenServer version

So, without further ado here is how the code would look with GenServer:

Notice, how much simpler the code is now. No more, get_response method. No more, danger of messing things up by forgetting to pass self() in the tuple that you send to the server.

Each handle_call function implementation corresponds with what we send to the server in the corresponding method. Example, the get_all_entries method returns the atom :get_all_entries and the handle_call signature is:

def handle_call(:get_all_entries)

We are taking full advantage of erlang’s pattern matching on method signatures.

Every handle_call is responsible for returning three things at a minimum:

  1. Was the call successful?
  2. The response
  3. And the updated state

If we look at the return value from the def handle_call({:add_entry, hostname, new_ip}) it is {:reply, :ok, updated_entries}. Here the response to the caller, is the second entry in the tuple, the atom :ok. And the updated state is the third-entry updated_entries.

Final thoughts

This is just scratching the surface of GenServer’s and I hope I have piqued your interest and removed some of the mystery surrounding this stuff. Definitely check out the Erlang GenServer man page for more details.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.