cult3

Understanding GenServer in Elixir

Aug 24, 2016

Table of contents:

  1. What is GenServer?
  2. How do you use GenServer?
  3. Starting the process
  4. Calling and Casting from the public API
  5. Initialising the server
  6. Handling Call requests
  7. Handling Cast requests
  8. Using our ShoppingList
  9. Conclusion

Over the last couple of weeks we have started to explore probably the most interesting aspect of working with Elixir (and Erlang) in that applications are built around this idea of small, isolated processes.

First we looked at Understanding Concurrency and Parallelism in Elixir and how we can run our code in isolation and make the most of a multi-core processor.

Next we looked at using Tasks as an abstraction of the basic spawn function. Tasks make it really easy to move sequential code off into a separate process so you can make the most of concurrency.

Over the last two weeks we’ve looked at storing state in Elixir processes, and then using Agents as an abstraction of state so we don’t have to write the boilerplate code ourselves. Storing state in a functional programming language requires us to continuously pass the state to a recursive function. Fortunately the Erlang Virtual Machine is very good at dealing with this type of work.

The next step of our exploration is to investigate GenServer. Whilst Agents are specifically used for storing state, GenServer is used as a general purpose abstraction for building generic servers in Elixir.

In today’s tutorial we will be exploring the how and why of using GenServer.

What is GenServer?

One of the core tenants of using Elixir (and Erlang) is that applications are built around isolated processes. This is such a common thing that it makes sense to have a standard way of writing this type of code.

Erlang has a module called :gen_server that defines a number of functions that should be implemented by a module that is a “generic server”. If you are familiar with other programming languages, :gen_server is a bit like an interface. In Erlang, a module that defines the functions that should be implemented by another module is called a behaviour.

In Elixir we have a module called GenServer. This Elixir module is a totally separate thing to the :gen_server module of Erlang. GenServer is a module that you add to your implementation module through the use macro:

defmodule ShoppingList do
  use GenServer
end

We haven’t looked at using macros in Elixir as that is a whole other topic to explore. However, in simple terms, when you use GenServer, Elixir is basically providing default implementations for the :gen_server behaviour module.

This means that any module that adds the GenServer module will automatically be a compliant :gen_server module. This allows you to only implement the functions that you require in your module, whilst leaving the rest of the functions to fall back to the defaults provided by GenServer.

How do you use GenServer?

So hopefully you now have a good idea of what GenServer is and how you would add it to your module. The next question is, “how do you use GenServer?”.

To use GenServer you basically just implement the functions that you require of your generic server. You don’t need to implement all of the functions of :gen_server because the GenServer module from Elixir will provide the default implementations.

The functions of a GenServer are basically split between the client and the server. As we saw in Working with State and Elixir Processes, you call the public API of the process from the client, and then you handle those requests on the server. This can be a bit confusing as the methods are part of the same module, but to make things clearer in this tutorial I will mark the split between the client and the server.

For the rest of this tutorial we will look at implementing the functions of GenServer.

Starting the process

The first function you will need to implement is for starting the server:

def start_link do
  GenServer.start_link(__MODULE__, :ok, [])
end

As you can see, in this function we simply need to call the start_link function on the GenServer module and pass it three arguments.

The first argument is providing the location of where the server callbacks are implemented. In this example I’m going to implement them as part of the same module and so I can use __MODULE__ instead of writing the name of the module. You could of course split the server callback functions into another module if you wanted to.

The next argument is a the initialisation arguments for the module. I’m not going to require any initialisation arguments in this example so I can simply provide the :ok atom.

And finally, the third argument is a list of options that can be used to set certain values. Again we don’t need to worry about these options right now so I will just provide an empty list as the third argument.

Calling and Casting from the public API

Next we need to provide the public API for the module. These are the functions that will be invoked by other modules of your application:

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

def add(pid, item) do
  GenServer.cast(pid, {:add, item})
end

In the example above I’ve added two functions, one for reading the shopping list and one for adding a new item to the shopping list.

The read/1 function accepts the pid of the process as the only argument. We can use the call/2 function of the GenServer.module to send the request to the process. When using the call/2 function, the process will block until it has received a response from the server.

The add/2 function accepts the pid of the process and the item to add to the shopping list. This time I’ve used the cast/2 function. This function will return immediately as it does not wait for a response from the server.

On one hand, the fact that the response is immediate is a good thing, but on the other hand you don’t know if the item was successfully added to the list. Its up to your discretion to chose the appropriate function for your use case. Generally speaking, if you want to make sure the request was successful, use call/2.

Now that we have a way to start the process and a couple of functions to interact with as a public API, it’s now time to turn our attention to implementing the server side functions.

Initialising the server

The first function I will implement will be for initialising the server:

def init(:ok) do
  {:ok, []}
end

This function is automatically called first when the process is started via the start_link function. As you can see, the only argument of this function is the argument we passed as the second argument to the start_link function from earlier (in this case :ok).

In this function you can do whatever you need to do to initialise the server. The return value of this function should be a tuple where the first item is the atom :ok and the second argument is the initial state of the server (in this case an empty list).

Handling Call requests

Next we need to handle the incoming requests from the public API. First we will handle the :read request:

def handle_call({:read}, _from, list) do
  {:reply, list, list}
end

As you can see, to handle call requests we implement the handle_call/3 function and then we use pattern matching to handle the specific request by matching against the arguments of the request.

The second argument of this function is the pid of the process that sent the request. For the most part you can just ignore this value. To ignore the value you can use an underscore and optionally give it a name. If you don’t use an underscore the Erlang compiler will complain that there is an unused variable.

And finally the third argument is the current state.

The return value of a handle_call/3 function should be a tuple where the first argument is the atom :reply, the second argument is the value to be returned to the client, and the third argument should be the state. In this case I want to return the full list so the second and third arguments are just the same.

Handling Cast requests

Next we can handle the cast request:

def handle_cast({:add, item}, list) do
  {:noreply, list ++ [item]}
end

Once again we use the generic handle_cast/2 function and then allow pattern matching to work it’s magic to find the correct implementation for the given arguments.

Handling cast requests is a bit simpler than handling call requests. The first difference is that we don’t have the from pid from the client in the arguments to this function because we never need to send a response.

In the body of the function you would handle the request. In this case I’m simply appending the item to the end of the list.

The return value of this function should be a tuple where the first item is the atom :noreply and the second item is the state.

Using our ShoppingList

Here is our new ShoppingList module that implements GenServer:

defmodule ShoppingList do
  use GenServer

  # Client API

  def start_link do
    GenServer.start_link(__MODULE__, :ok, [])
  end

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

  def add(pid, item) do
    GenServer.cast(pid, {:add, item})
  end

  # Server Callbacks

  def init(:ok) do
    {:ok, []}
  end

  def handle_call({:read}, from, list) do
    {:reply, list, list}
  end

  def handle_cast({:add, item}, list) do
    {:noreply, list ++ [item]}
  end
end

To use this module, fire up iex and pass the name of the file at the command prompt:

iex shopping_list.ex

Next we can walk through starting the server, adding a couple of items, and then reading those items back:

# Start the server
{:ok, pid} = ShoppingList.start_link()

# Adding some items
ShoppingList.add(pid, "milk")
ShoppingList.add(pid, "bread")
ShoppingList.add(pid, "cheese")

# Read the items back
ShoppingList.read(pid)

Conclusion

This was a basic introduction to understanding GenServer, what it is, why it’s important, and how to use it.

GenServer is a specification for writing generic server processes in Elixir. It is simply a module of Elixir that provides default implementations for the :gen_server Erlang behaviour.

You could implement :gen_server yourself, but it is the product of a lot of hardwork from very clever people. By using the :gen_server behaviour it also makes it a lot easier to read and understand other Elixir and Erlang code.

There are still a couple of things we haven’t covered in respect to GenServer, including implementing the other functions of the :gen_server module.

But understanding what, why, and how is the most important stuff, everything else is better handled with an actual good use case for the functionality. As we explore more complex uses of GenServer we will no doubt cover all of the functionality of the module.

In the simple example we have looked at today we could of just used an Agent as we just need to store state. Agents are just simple versions of GenServer, but by using GenServer we have a lot more power.

We will explore that power in the coming weeks.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.