GenServer Abridged
Let’s take a moment and take a big step back to talk about what GenServer is, and how it fits into the Erlang/Elixir model.
Processes, processes, everywhere…
Erlang/Elixir is based upon the “Actor Model”. It’s the idea that you break your application down into separate processes which have their own memory which is explicitly isolated from all other processes.
The Rules:
- You may crash. Crashing will NOT cause any other processes to crash.
- Erlang processes communicate via messages only.
- Doing a hot-code upgrade? It happens via message.
- Debugging and examining memory? It happens via message.
- “GenServer, make me a sandwich”, send it a message.
- “GenServer, sudo make me a sandwich”, it’s still a message.
Anatomy of a GenServer
GenServer is a Generic Server. It’s not the only server-type available but it’s the most common and likely the only one we’ll use in this design. A GenServer has its own state which it keeps internally and does “stuff” when it receives messages.
Let’s talk about the simplest GenServer you could have. One that stores a single number and increments it and decrements it on demand.
Startup
def start_link(initialnumber) do
GenServer.start_link(__MODULE__, initialnumber)
end
This is the API we expose to the user by convention. In our example we’ll pass the number we want our counter to start at. All our function does is call GenServer.start_link, giving it the name of our module and our initial variable.
The GenServer module contains the code that spawns a new GenServer process, makes a note of our callback module name and calls init(initialnumber) inside our new process.
Initialization
def init(initialnumber) do
{:ok, initialnumber}
end
This is the function that defines the initial state of our GenServer. In our trivial example, our state is just a number - the initial value being that that the user provided. As such, it simply returns {:ok, initialnumber}. It’s a tuple which contains :ok (informs GenServer that we initialized without error), and initialvalue which is our internal state.
GenServer then puts our process to sleep… ZZzz… waiting for messages it its mailbox.
I can haz message?
def handle_cast(:inc, state) do
newstate = state + 1
{:noreply, newstate}
end
def handle_cast(:dec, state) do
{:noreply, state-1}
end
There are two basic types of OTP messages, calls and casts. In this example, we’re using a cast. A cast is a one-way message that expects no reply. A cast message can be lost and that’s okay. Casts are the UDP of OTP. To process incoming casts you use the handle_cast callback.
Your callback responds with {:noreply, newstate}. :noreply (because there’s no reply, and newstate because - well, we want our number to change!)
Reading the counter
def handle_call(:query, from, state) do
number = state
{:reply, number, state}
end
The second type of OTP message is a call which does require a reply. This is the “TCP” of OTP. We will use this whenever we need to retrieve a value from another process. I’ve been overly verbose in assigning the variable number so you could see which of the arguments in the respose was the actual reply, and which was the state we pass back.
Things to note.
GenServer is really an infinitely resursive function, you can think of it like this in very abridged pseudocode:
function start_link(callbackModule, data) {
newstate = callbackModule.init(data) # Allows the programmer to set
# the initial state as they wish
GenServerMainLoop(callbackModule, newstate) # Hands control over to
# GenServerMailLoop
}
function GenServerMainLoop(callbackModule, state) {
msg = receive_message() # blocks until a
# message is received
newstate = callbackModule.handle_cast(msg, state) # passes control to user
# code which responds
# to the message with a
# potential new state
GenServerMainLoop(callbackModule, newstate) # to recurse is to be divine
}
The Joy of this model from a performance perspective is garbage collection. In an application it is not unusual to have hundreds of thousands of GenServers running in a system. Any variables that are not “state”, fall immediately out of scope when GenServerMainLoop is called which makes garbage collection trivial.
No more “stop the world, I need to garbage-collect” several minute long delays when Java decides it needs to do a cleanup on isle 4.
There’s more folks
So there is a lot more to GenServers, specifically around hot-code loading, supervision trees, links, monitors, et al… but this should suffice as a primer to get us started.