GenServer explained in 5 minutes
Comfortable learning curve for Elixir— part 4
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.
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 🐨Bob
internal 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 ofGenServer
works, which then callsinit
and awaits untilinit
returns initial state, which is remembered byGenServer
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.