GenServer explained in 5 minutes

Comfortable learning curve for Elixir— part 4

Gaspar Chilingarov
Learn Elixir
4 min readJul 11, 2017

--

GenServer is essential part of OTP, which simplifies repeating tasks, letting programmer concentrate on logic of the application, and not on handling edge cases and repeated error handling.

Every time when GenServer callback is called :)

The exercise is at very bottom of the article, after explanation of GenServer internals.

The idea behind GenServer is rather simple — you start separate process, that holds some state, then on each incoming message (be that call or cast) it may change its internal state and also generate some response (in case of call).

In this manual calling process is named 🐌Alice and newly created process is 🐨Bob.

Programming without GenServer, as you would done it manually

Before we proceed I want to show code which is very simplified version of what happens in GenServer. Get yourself accustomed with concepts of spawn, spawn_link, send, receive before reading further.

defmodule SimpleGenServerMock do
def start_link() do
# runs in the caller context 🐌Alice
spawn_link(__MODULE__, :init, [])
end
def call(pid, arguments) do
# runs in the caller context 🐌Alice
send pid, {:call, self(), arguments}
receive
{:response, data} -> data
end
end
def cast(pid, arguments) do
# runs in the caller context 🐌Alice
send pid, {:cast, arguments}
end
def init() do
# runs in the server context 🐨Bob
initial_state = 1
loop(initial_state)
end
def loop(state) do
# runs in the server context 🐨Bob
receive command do
{:call, pid, :get_data} ->
# do some work on data here and update state
{new_state, response} = {state, state}
send pid, {:response, data}
loop(new_state)
{:cast, :increment} ->
# do some work on data here and update state
new_state = state + 1
loop(new_state)
end
end
end

Code initial_state = 1 is exactly same code we write in init callback. Internal state of the server is simply an integer. Usually it is map, tuple or list with settings and state.

{state, state} means that we do not want to update state and want to return state as a result. This is the code which goes in handle_call callback in 🐨Bob.

And code new_state = state + 1 is the code which goes into handle_cast callback, because we do not need to respond with result, we just change server 🐨Bobinternal state.

Working with module will look like:

pid = SimpleGenServerMock.start_link()
counter = SimpleGenServerMock.call(pid, :get_data)
IO.puts "Counter: #{counter}
SimpleGenServerMock.cast(pid, :increment)
counter = SimpleGenServerMock.call(pid, :get_data)
IO.puts "Counter: #{counter}

Same server with GenServer behaviour

Now if we want to re-write same code using GenServer it will look like this:

defmodule SimpleGenServerBehaviour do
use GenServer
def start_link() do
# runs in the caller context 🐌Alice
GenServer.start_link(__MODULE__, [])
end
def init(_) do
# runs in the server context 🐨Bob
{:ok, 1}
end
def handle_call(:get_data, _, state) do
# runs in the server context 🐨Bob
{:reply, state, state}
end
def handle_cast(:increment, state) do
# runs in the server context 🐨Bob
{:noreply, state+1}
end
end

While in this example it did not saved a lot of lines for more complicated code having GenServer deal with all complexity saves a lot of typing. Also you get timeouts, named processes and stable, production proven error handling for free 😃

Using GenServer behaviour is very similar to code we written before:

{:ok, pid} = GenServer.start_link()
counter = GenServer.call(pid, :get_data)
IO.puts "Counter: #{counter}
GenServer.cast(pid, :increment)
counter = GenServer.call(pid, :get_data)
IO.puts "Counter: #{counter}

GenServer.start_link

This method starts new GenServer process. Execution occurs in two separate contextes:

  • in 🐌Alice it adds link between 🐌Alice and 🐨Bob . Linked process in Elixir/Erlang means that if one of them dies, other also is killed. This has a benefit of taking down all dependent processes and preventing run away processes.
  • in newly spawned 🐨Bob first some code of GenServer works, which then calls init and awaits until init returns initial state, which is remembered by GenServer and will be passed to handlers
  • after this server is ready for use — it has initial state, it is linked to the caller and ready to process messages.

The benefit of using GenServer here is that it hides whole complexity of message loop inside. So it started process, initialized it and now waits for new messages.

GenServer.call and handle_call() and sleeping

GenServer.call is synchronous, blocking call to server which awaits results from 🐨Bob or timeouts. So you should not do any long data processing in handle_call() or it will easily timeout (default is 5 seconds).

So every function which you want to use in handle_call() should be asynchronous. For example if you want to postpone processing incoming data for 10 seconds — you will need store data in state, then use :timer.after call which will send message to 🐨Bob after 10 seconds, and only then, in message handler do the whole processing.

You should not use Process.sleep in GenServer handlers, because if timeout is big handle calls with timeout and crash server and also server will be unable to server another requests during that time.

Exercise

Rewrite stack machine you created in part 2 as GenServer behaviour. All commands you read from stdin should be forwarded to the server and it should respond to some of them as well.

No functions from module IO should be present in GenServer behaviour module at all. Implement print function as a call and the rest as cast or call .

About The Author

I’m Gaspar Chilingarov . I facilitate DevOps transition, help moving legacy applications to cloud and write high-performance Elixir apps. You can connect with me on Twitter, Facebook, LinkedIn and GitHub.

Found this post useful? Kindly tap the ❤ button below! :) Let’s spread word about Elixir.

--

--

Gaspar Chilingarov
Learn Elixir

I facilitate DevOps transition, help moving legacy applications to the cloud and write high-performance Elixir apps.