Buliding a GenServer
Navigation
Home GenServer IntroductionGenServer ExamplesBuilding a GenServer from scratch
Let’s delve into crafting a simplified GenServer-like implementation using Elixir’s fundamental primitives such as spawn_link
and send
. This exercise will give us a clearer insight into the inner workings of GenServers.
For the sake of simplicity, we will focus on implementing only the commonly used callbacks: init/1
, handle_call/3
, handle_cast/2
, and handle_info/2
.
defmodule MyGenServer do
# Callbacks to implement
@callback init(term()) :: {:ok, term()}
@callback handle_call(term(), pid(), term()) :: {:reply, term(), term()}
@callback handle_cast(term(), term()) :: {:noreply, term()}
@callback handle_info(term(), term()) :: {:noreply, term()}
# == Public API ==
def start_link(module, args) do
{:ok, spawn_link(__MODULE__, :server_init, [module, args])}
end
def call(server_pid, args) do
send(server_pid, {:call, self(), args})
receive do
{:response, response} -> response
end
end
def cast(server_pid, args) do
send(server_pid, {:cast, args})
end
def stop(server_pid, reason \\ :normal) do
send(server_pid, {:stop, reason})
end
# == Internal implementation ==
def server_init(module, args) do
{:ok, state} = module.init(args)
genserver_loop(module, state)
end
# Recursively loop and wait for messages
def genserver_loop(module, state) do
receive do
{:call, parent_pid, args} ->
{:reply, response, new_state} = module.handle_call(args, parent_pid, state)
send(parent_pid, {:response, response})
genserver_loop(module, new_state)
{:cast, args} ->
{:noreply, new_state} = module.handle_cast(args, state)
genserver_loop(module, new_state)
{:stop, reason} ->
module.terminate(reason, state)
exit(reason)
request ->
{:noreply, new_state} = module.handle_info(request, state)
genserver_loop(module, new_state)
end
end
end
Using our GenServer
defmodule Stack do
@behaviour MyGenServer
@impl true
def init(args) do
{:ok, args}
end
@impl true
def handle_call(:get_stack, _from, state) do
{:reply, state, state}
end
@impl true
def handle_call(:pop, _from, [num | state]) do
{:reply, num, state}
end
@impl true
def handle_cast({:push, num}, state) do
IO.inspect(num, label: "PUSH")
{:noreply, [num | state]}
end
@impl true
def handle_info(:stats, state) do
IO.inspect("Stack length: #{length(state)}")
{:noreply, state}
end
end
{:ok, stack_server_pid} = MyGenServer.start_link(Stack, [])
MyGenServer.cast(stack_server_pid, {:push, 1})
MyGenServer.cast(stack_server_pid, {:push, 2})
MyGenServer.cast(stack_server_pid, {:push, 3})
MyGenServer.call(stack_server_pid, :get_stack) |> IO.inspect(label: "STACK")
MyGenServer.call(stack_server_pid, :pop) |> IO.inspect(label: "POP")
MyGenServer.call(stack_server_pid, :get_stack) |> IO.inspect(label: "STACK")
send(stack_server_pid, :stats)