#12: Intro to GenServer

Episode notes

Elixir 1.4.2

Source code on GitHub

Elixir comes with a set of libraries known as OTP.

One of those libraries is the GenServer module, which provides a common interface to help handle concurrency.

In this episode we’ll get started with the basics of GenServer by using it to help us manage a shopping list.

Let’s get started.

Here we have an empty module called ‘ShoppingList’.

First we’ll add the use macro to bring in the GenServer module.

Now, let’s create a function named start_link, which we’ll use to start our shopping list process.

And inside it we’ll call the GenServer function of the same name, start_link.

We’ll use __MODULE__ to return our ShoppingList module, and then we’ll pass in any initial state we want to start with. We’ll use an empty list.

Once we call GenServer.start_link/3, the ‘init’ callback is triggered.

Let’s define it.

It takes one parameter, which is the initial state of an empty list included as the second argument in GenServer.start_link/3 .

We’ll call it ‘list’ since it represents our shopping list.

‘init’, along with most other GenServer functions, has a specific format it needs to return.

We’ll keep ours simple and return a 2 element tuple {:ok, 'list'} to get our ShoppingList started.

For a full list of return values the different GenServer functions expect, refer to the GenServer docs.

shopping_list.ex

defmodule ShoppingList do
  use GenServer

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

  def init(list) do
    {:ok, list}
  end

end

With that we can let’s fire up an IEx session and see what starting our GenServer looks like.

$ iex shopping_list.ex

The GenServer.start_link/3 and by extension our ShoppingList.start_link function will return a tuple with :ok and our process identifier or PID.

We’ll pattern match to grab the PID.

> {:ok, pid} = ShoppingList.start_link()
{:ok, #PID<0.111.0>}

Let’s check to make sure everything started correctly and that our process is still alive.

> Process.alive?(pid)
true

Great, our shopping list is up and running. Now let’s add a way to add and remove items to it.

Let’s go back to our module.

One important concept regarding GenServer is that they’re organized into two parts: the client and the server, each running in their own process.

Now you can split the client and server into separate modules, but for the purposes of our demo, we’ll stay simple and keep everything in the same module.

But we will organize our functions by whether they’re part of the client or the server.

Let’s start with our ‘start_link’ function, which is part of our ‘client’ api.

And our ‘init’ callback is part of our server callbacks.

shopping_list.ex

defmodule ShoppingList do
  use GenServer

  #Client
  def start_link do
    GenServer.start_link(__MODULE__, [])
  end

  #Server
  def init(state) do
    {:ok, state}
  end

end

Great, now let’s create a function for our client API named ‘add’, which will add items to our shopping list. It will take a PID and the item we want to add to it.

GenServer gives us two options on how we want to update our shopping list: synchronously, where we care about a response. And asynchronously, where we don’t care about a response.

Since we’re just adding items to our shopping list, we don’t necessarily care about a response so we’ll use GenServer.cast/2 to send an asynchronous request to the server.

GenServer.cast/2 will take the PID we passed in and the item we want to add to our shopping list.

And when GenServer.cast/2 is called, the ‘handle_cast’ server callback is invoked. Let’s add that now.

It will accept two parameters: our item, and our current shopping list.

Then we can update our our shopping list.

‘handle_cast’ expects a two element tuple as its return, with ‘:noreply’ as the first element and the updated shopping list as the second element.

Now that we can add items to our shopping list, let’s add a way to view it.

We’ll add another function to our client API named ‘view’, which will take a PID.

Since we care about a response here, we’ll use the synchronous GenServer.call/3

And we’ll pass in the PID we received and the atom ‘view’.

This will invoke the ‘handle_call’ server callback, so let’s implement that too.

The “view” atom we used will be passed in as the first argument for our ‘handle_call’ function, so let’s pattern match on it.

The second argument passed in is a two element tuple that includes the caller’s PID. We don’t need it for this demo, so let’s ignore it.

And finally the third argument is our existing state - in this case our current shopping list.

Since we just want to return our shopping list, according to the docs we’ll need to return a three element tuple, with ‘:reply’ as the first element.

The second element is what we want to return, in this case our shopping list.

And finally we need to return our shopping list as the state we want to continue with as the third element.

shopping_list.ex

defmodule ShoppingList do
  use GenServer

  #Client
  def start_link do
    GenServer.start_link(__MODULE__, [])
  end

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

  def view(pid) do
    GenServer.call(pid, :view)
  end

  #Server
  def init(list) do
    {:ok, list}
  end

  def handle_cast(item, list) do
    updated_list = [item|list]
    {:noreply, updated_list}
  end

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

end

With that let’s test it out.

Going back to our terminal, we’ll reload our module.

Now we need to add a few items to our shopping list it. Let’s add eggs, milk, and cheese.

Then let’s see if we can view our shopping list.

And great it worked.

> {:ok, pid} = ShoppingList.start_link()
{:ok, #PID<0.86.0>}
> ShoppingList.add(pid, "eggs")
:ok
> ShoppingList.add(pid, "milk")
:ok
> ShoppingList.add(pid, "cheese")
:ok
> ShoppingList.view(pid)
["cheese", "milk", "eggs"]

Let’s go back to our module and add a way for us to remove items from our shopping list.

Let’s name our new function ‘remove’ which will take our shopping list PID and the item we want to remove from the list.

We’ll then call GenServer.cast/2 since we don’t care about the response.

Again we’ll pass in the PID, and for our second argument we’ll use a two element tuple.

The first element will be an atom ‘remove’, which we can pattern match with in our callback.

Then we’ll use the item we want to remove as the second element.

Now we can define our corresponding ‘handle_cast’ callback.

In the first parameter we’ll pattern match against the first element, the atom “remove”, that we used in our two element tuple above.

Then our existing list will be passed in the second parameter.

One note - this ‘handle_cast’ function will never match unless it comes first.

Now we need to remove the item from our shopping list. We’ll use Enum.reject/2

And pass in our existing list.

Then we’ll write a function to return true if any item in the list matches what was passed in.

And then we can return another two element tuple with ‘:noreply’ and our updated list.

shopping_list.ex

defmodule ShoppingList do
  use GenServer

  #Client
  def start_link do
    GenServer.start_link(__MODULE__, [])
  end

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

  def view(pid) do
    GenServer.call(pid, :view)
  end

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

  # Server
  def init(list) do
    {:ok, list}
  end

  def handle_cast({:remove, item}, list) do
    updated_list = Enum.reject(list, fn(i) -> i == item end)
    {:noreply, updated_list}
  end

  def handle_cast(item, list) do
    updated_list = [item|list]
    {:noreply, updated_list}
  end

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

end

Let’s go back to the terminal and reload our module.

Then let’s try removing an item from the list.

 > r(ShoppingList)
warning: redefining module ShoppingList (current version defined in memory)
  shopping_list.ex:1

{:reloaded, ShoppingList, [ShoppingList]}
 > ShoppingList.view(pid)
["cheese", "milk", "eggs"]
 > ShoppingList.remove(pid, "cheese")
:ok
 > ShoppingList.view(pid)
["milk", "eggs"]

And great it was removed from our shopping list.

Now we just need a way to stop our ‘ShoppingList’ process.

Let’s go back to our module.

We’ll add a function named ‘stop’ to the client API that takes the PID we want to stop.

Then we’ll call GenServer.stop/3 with the PID that was passed in.

The shutdown reason of “normal”. And a timeout of “infinity”.

Since ‘normal’ and ‘infinity’ are default values, we don’t need to include them here, but I’ll leave them for clarity.

Now let’s implement the corresponding callback ‘terminate’.

It will take a ‘reason’ which we specified as ‘normal’. And since we don’t care about it for this demo, let’s ignore it.

And then our current shopping list.

Let’s also print out a message to help us see our shopping list shutting down.

And then we need to return a single atom :ok

shopping_list.ex

defmodule ShoppingList do
  use GenServer

  #Client
  def start_link() do
    GenServer.start_link(__MODULE__, [])
  end

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

  def view(pid) do
    GenServer.call(pid, :view)
  end

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

  def stop(pid) do
    GenServer.stop(pid, :normal, :infinity)
  end

  #Server
  def terminate(_reason, list) do
    IO.puts("We are all done shopping.")
    IO.inspect(list)
    :ok
  end
  def handle_cast({:remove, item}, list) do
    updated_list = Enum.reject(list, fn(i) -> i == item end)
    {:noreply, updated_list}
  end

  def handle_cast(item, list) do
    updated_list = [item|list]
    {:noreply, updated_list}
  end

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

  def init(list) do
    {:ok, list}
  end

end

Going back to our terminal one last time. Let’s reload our ShoppingList module.

Then let’s double check the items in our shopping list

 > ShoppingList.view(pid)
["milk", "eggs"]

Now let’s try stopping our Shopping List process.

 >ShoppingList.stop(pid)
We are all done shopping.
["milk", "eggs"]
:ok
 >Process.alive?(pid)
false

And if we can check and see that the process is was stopped.