GenServer Abridged

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:

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.