GenServer in Erlang

Original

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 idea behind GenServer is simple - you start separate process, that holds some state, then on each incoming message(be that call or cast) it may change it internal state and also generate some response(in case of call)

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

Programming without GenServer, as you would done it manually
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 `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, data} = {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 a map, tuple or list with settings and state.

{state, state} means that we do not want to update the state and want to return state as 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:

1
2
3
4
5
6
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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, _from, 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 tying. Also you got timeout, named processes and stable, production proven error hanlding for free.

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

1
2
3
{:ok, pid} = GenServer.start_link(SimpleGenServerBehaviour, [])
counter = GenServer.call(SimpleGenServerBehaviour, :get_data)
GenServer.cast(SimpleGenServerBehaviour, :increment)

Better to implement start in the module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
defmodule Stack do
use GenServer

def start_link(defaut) do
GenServer.start_link(default)
end

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

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

# Server callbacks

@impl ture
def handle_call(:pop, _from, [h | t]) do
{:reply, h, t}
end

@impl true
def handle_cast({:push, item}, state) do
{:noreply, [item | state]}
end
end
Receiving Regular Messages

The goal of GenServer is to abstract the “receive” loop for developers, automatically handling system messages, support code changes, synchronous calls and more. Therefore, you should never call your own “recieve” inside the GenServer callbacks as doing will cause the GenServer misbehave.

Besides the synchronous and asynchronous communication provided by call/3 and cast/2, regular messages sent by functions such as Kernal/send2, Process.send_after/4 and similar, can be handled inside the handle_info/2 callback.

handle_info/2 can be used in many sinutations, such as handling monitor DOWN messages sent by Process.monitor/1. Another use case for handle_info/2 is to perform periodic work, with the help of Process.send_after/4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
defmodule MyApp.Periodically do
use GenServer

def start_link do
GenServer.start_link(__MODULE__, %{})
end

@impl true
def init(state) do
schedule_work() # Schedule work to be performed on start
{:ok, state}
end

@impl true
def handle_info(:work, state) do
# Do the desired work here
schedule_work() # Reschedule once more
{:noreply, state}
end

defp schedule_work() do
Process.send_after(self(), :work, 1000)
end
end
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×