Building your own GenServer
Sometimes it’s a good learning experience to try and reimplement some of the functionality that a standard library provides. So how much does it take to reimplement the simple parts of a GenServer?
The simple parts
A GenServer provides you with a lot of functionality out of the box:
The goal of a GenServer is to abstract the “receive” loop for developers, automatically handling system messages, support code change, synchronous calls and more
– The GenServer docs
Let’s focus on implementing the “receive” loop, synchronous calls and making sure that it fits in Supervision tree.
We’re going to more or less implement the Stack
example from the GenServer docs but without using a GenServer.
Creating the project and the first Stack module
We need a project to get us started and we’re going to need a supervisor. So mix new stack --sup
is is.
Start out simple by defining the interface we want. We’ll need push
and pop
functions to be a stack. And if we want to start the stack as worker
child of the appliction Supervisor we’ll also need a start_link
function.
The start_link
function
We’re not going for anything like a full re-implementation of GenServer
so let’s try and keep our start_link
as simple as possible.
Reading over the start_link/3
docs and looking at the possible return values we see that we´re supposed to return a {:ok, pid}
tuple with the pid of the server.
Start out by adding SimpleStack
to the Supervisor in Stack
Running the application now (mix run
) will result in a error:
So let’s start working on that start_link
function. Should be simple enough: We just need to start a new process with spawn_link
and return it’s pid.
Let’s try running that again: mix run --no-halt
Well, that didn’t work. But what is going on? Try adding max_restarts: 10
to the options sent to the supervisor like so:
Then run the application again. Now we get our [debug] Started
ten times. So the supervisor keeps calling start_link
.
Time to fire up iex
and do some digging:
Ah! Out process stops as soon as its done its work. The supervisor sees this and then restarts the process and keeps doing it until it reaches its max_restarts
limit.
The receive loop
Processes communicate via messages and each process has its own mailbox. We can use the receive do end
statement to wait for messages to arrive at our mailbox. First let’s start out by listening for a :pop
message that we’ll use to support the pop functionality of our stack.
I’ve changed the spaw_link
statement so that we can move the receive loop into its own function:
The receiver
function implements the receive loop. It gets initialized with an empty list.
The receive
statement lets us patterne match on incomming messages.
We handle a {:push, element}
message by adding the new element to the front of the list and after doing so we call the receiver
recursively to wait for the next message.
Pushing
Next up is implementing the push/1
function in the SimpleStack
module.
We know that we need to send a {:push, element}
message to the process we created in start_link
. But how do we find the pid
of that process again?
You know how you can register a GenServer
with a name by calling something like this: GenServer.start_link(Stack, [:hello], name: MyStack)
.
Turns out that giving a process a name is a pretty useful feature, and a feature that is provided by Process.register/2
.
Great, well extend the start_link
function to use that.
And now the push/1
function can easily send a message to the process with: send(:stack, {:push, element})
.
And so we have a simple form of GenServer.cast
.
Returning a response
The last thing we need to be able handle synchronous calls.
Again we do this by sending messages. We’ll define a new message: {:pop, pid}
.
So besides telling that we want to perform a pop on the stack we’ll also send along the PID of the process to send the result to.
The calling function pop/0
starts by sending the :pop
message. It finds its own PID
by calling self()
.
Then it waits for the response message with receive
.
The :pop
message is handled we take the first element, send it to the calling process and then recursively call the receiver/1
function with the popped stack.
And there we have the synchronous call we wanted. You can find the completed code here if you want to play around with it.