A behaviour module for implementing the server of a client-server relation.
A GenServer is a process like any other Elixir process and it can be used to keep state, execute code asynchronously and so on. The advantage of using a generic server process (GenServer) implemented using this module is that it will have a standard set of interface functions and include functionality for tracing and error reporting. It will also fit into a supervision tree.
The GenServer behaviour abstracts the common client-server interaction. Developers are only required to implement the callbacks and functionality they are interested in.
Let's start with a code example and then explore the available callbacks. Imagine we want a GenServer that works like a stack, allowing us to push and pop elements:
defmodule Stack do
use GenServer
# Callbacks
# if you mark a function with @impl when that function is not a callback.
@impl true
def init(stack) do
{:ok, stack}
end
@impl true
def handle_call(:pop, _from, [head | tail]) do
{:reply, head, tail}
end
@impl true
def handle_cast({:push, element}, state) do
{:noreply, [element | state]}
end
end
# Start the server
{:ok, pid} = GenServer.start_link(Stack, [:hello])
# This is the client
GenServer.call(pid, :pop)
#=> :hello
GenServer.cast(pid, {:push, :world})
#=> :ok
GenServer.call(pid, :pop)
#=> :world
We start our Stack
by calling start_link/2
, passing the module with the server implementation and its initial argument (a list representing the stack containing the element :hello
). We can primarily interact with the server by sending two types of messages. call messages expect a reply from the server (and are therefore synchronous) while cast messages do not.
Every time you do a GenServer.call/3
, the client will send a message that must be handled by the handle_call/3
callback in the GenServer. A cast/2
message must be handled by handle_cast/2
. There are 8 possible callbacks to be implemented when you use a GenServer
. The only required callback is init/1
.
Although in the example above we have used GenServer.start_link/3
and friends to directly start and communicate with the server, most of the time we don't call the GenServer
functions directly. Instead, we wrap the calls in new functions representing the public API of the server.
Here is a better implementation of our Stack module:
defmodule Stack do
use GenServer
# Client
def start_link(default) when is_list(default) do
GenServer.start_link(__MODULE__, default)
end
def push(pid, element) do
GenServer.cast(pid, {:push, element})
end
def pop(pid) do
GenServer.call(pid, :pop)
end
# Server (callbacks)
@impl true
def init(stack) do
{:ok, stack}
end
@impl true
def handle_call(:pop, _from, [head | tail]) do
{:reply, head, tail}
end
@impl true
def handle_cast({:push, element}, state) do
{:noreply, [element | state]}
end
end
For example, we could start and register our Stack
server locally as follows:
# Start the server and register it locally with name MyStack
{:ok, _} = GenServer.start_link(Stack, [:hello], name: MyStack)
# Now messages can be sent directly to MyStack
GenServer.call(MyStack, :pop)
#=> :hello