Elixir: an introduction to server architecture and GenServer behaviour

Erlang (and Elixir) strongly advocates concurrency, scalability, and fault tolerance via multiprocessing and the use of specific design patterns based on it.
An Erlang process is not an operative system process: it is faster, lighter, with much smaller memory footprint.
By spawning hundreds, thousands or even millions of these processes you can achieve great scalability, as
this recent post
explains.

The server/client architecture builds on such lightweight processes, can handle state, and is one of the keys to the
great results that can be achieved using Elixir.

Let’s build a server starting from this simple calculator module code:


defmodule Calculator do
  def calc(value, operator, n) do
    case operator do
      :+ -> value + n
      :- -> value - n
      :* -> value * n
      :/ -> value / n
    end
  end  
end

Calculator.calc 50, :-, 8
# 42

This code doesn’t handle state, just as you would expect when using a functional language: for multiple operations you
need to pipe the functions:

Calculator.calc(50, :-, 8) 
|> Calculator.calc(:-, 21) 
|> Calculator.calc(:*,2)

Spawning an Erlang process is very easy, fast and cheap:
pid = spawn fn -> end
The variable pid is a reference to the process and can be used to communicate with it.
A server is basically a never-ending process
that recursively handles some kind of requests. Let’s build some looping process first, with the ability to
handle state as well. Copy and paste this code in your iex shell:


defmodule Server do
  def start do
    spawn fn -> loop(1) end
  end

  defp loop(n) do
    IO.puts "looping #{n} times"
    :timer.sleep 1000
    loop n+1
  end
end

Server.start
# looping 1 times
# looping 2 times
# ...

Every second a new message is printed on the screen: these letters come from the spawned server process,
which is non blocking (you can still use the repl) and kind of stateful
(the number is incremented after each iteration).

Let’s go back to our calculator example. Let’s write the CalcServer module that complements the
Calculator core module. Here the loop function will at first handle the incoming
messages, then recursively call itself with the new updated state:


defmodule CalcServer do
  import Calculator

  def start do
    spawn fn -> loop(0) end
  end

  def loop value do
    new_value = receive do
      {:value, caller} -> 
        send caller, value
        value
      {operator, n} -> calc(value, operator, n)
    end
    loop new_value
  end
end

Let’s play a little with it:


calc = CalcServer.start
send  calc, {:+, 23}
send  calc, {:-, 2}
send  calc, {:*, 2}

The variable calc references the calculator server process (returned by spawn),
so we can send messages to it. These messages then get pattern-matched in the loop
receive block, precisely here:
{operator, n} -> calc(value, operator, n)
The subsequent loop function will be called with the calculation result,
courtesy of the Calculator module and its function calc that has
been imported using the import Calculator directive.
The messages we sent up until this point were fire-and-forget, meaning we didn’t receive any response from the server.

But now we need to read the total, so we can resort to bidirectional communication. First we need to send
the appropriate message to the server, which replies to our process with another message that we can read in our
main process:


send calc, {:value, self}
receive do
  total -> IO.puts "Current total is #{total}"
end
# Current total is 42

We have successfully built from scratch a simple server that relies on processes and asyncronous communication.
Of course this is buggy and trivial code, but it was useful to analyze some details behind the GenServer architecture.

Now that we have a grasp of how things works, we can convert the existing code to use the GenServer behaviour. GenServer stands for “generic server”,
an abstraction/design-pattern that helps build servers with consistent and predictable interface. Here is the new server code:


defmodule CalcGenServer do
  use GenServer

  import Calculator

  def start do
    GenServer.start __MODULE__, 0
  end

  def handle_call :value, _, value do
    {:reply, value, value}
  end
 
  def handle_cast {operator, n}, value do
    {:noreply, calc(value, operator, n)}
  end
end

In order to use the GenServer behaviour we only need to define a few functions: start, with the
module name to delegate calls and casts (here the current module with __MODULE__), and the start value 0.

Casts are requests that don’t need an answer, fire-and-forget. Calls are requests that require an answer, and here we have
one example of each: reading the total value is a call, while operations with the calculator are handled with a cast.

Now, here is how we can use the new server:


{:ok, calc} = CalcGenServer.start
GenServer.cast calc, {:+, 84}
GenServer.cast calc, {:/, 2}
GenServer.call calc, :value 
# 42.0

This is fairly straightforward, and we don’t need to listen explicitly for responses anymore, as we did with the
previous example.

In this post we just scratched the surface of GenServer, for a detailed introduction and explanation I strongly recommend reading the official Elixir
documentation.
Happy coding!

Leave a Reply

wpDiscuz