awochna

Elixir State Management: Agent or GenServer?

03 March 2017

I love Elixir and it’s functional style. That’s probably not much of a secret to you at this point, but one of the questions I often get is about transitioning to a functional style from an object oriented one. Luckily, I have some really good experience with this, coming from Ruby, where everything is an object. More to the point, the question really comes down to managing some sort of state in a software system beyond simple data structures being passed around, a replacement for the objects they previously used. Elixir processes can manage state in the same way that long-lived objects do in a Ruby or Javascript application and we have two OTP abstractions that make this sort of thing easy: Agent and GenServer. Agent is built on top of GenServer so let’s start exploring there.

GenServer

GenServer is exactly what it kinda sounds like, a general server. In this case, it’s not a web server or an application server, but more like a “state” server. A GenServer process can hold and manage state, but it can also perform calculations, wait for completion, process incoming messages, and do things completely on it’s own. It runs as a separate process (on the BEAM virtual machine, not the OS), so it is often used to handle non-blocking processing of things. In fact, another abstraction is used specifically for non-blocking processing, in the Task module in the Elixir standard library.

In my experience, it’s very common to back a GenServer with a module written for it with a nicer API. That’s because GenServers are super simple, responding to only a few calls, namely GenServer.handle_call/3 and GenServer.handle_cast/2. A “call” is used when you want to send a message to the GenServer and then wait for the response. In contrast, a “cast” is made when you want to send a message, but don’t need a response and the processing can be done asynchronously. A really basic example module looks something like this:

defmodule Queue do
  use GenServer

  def start_link(queue, name) do
    GenServer.start(__MODULE__, queue, name: name)
  end

  # GenServer callbacks

  def handle_call(:get, _from, [item | queue]) do
    {:reply, item, queue}
  end

  def handle_cast({:add, item}, queue) do
    {:noreply, queue ++ [item]}
  end
end

Without writing the nicer API, calls to your imaginary queue GenServer look ugly:

GenServer.cast(:personal_queue, {:add, "check emails"})

GenServer.call(:personal_queue, :get)

Commonly, you’ll define extra functions in your module that handle the ugly parts for you:

defmodule Queue do
  use GenServer

  def start_link(queue, name) do
    GenServer.start(__MODULE__, queue, name: name)
  end

  def add(queue, item) do
    GenServer.cast(queue, {:add, item})
  end

  def get(queue) do
    GenServer.call(queue, :get)
  end

  # other code ...
end

That’s much nicer, because now your calls look like:

Queue.add(:personal_queue, "check emails")

Queue.get(:personal_queue)

It’s less typing and much more intuitive.

Agent

The Agent module lets you easily create a process to hold state without implementing a bunch of stuff for a GenServer. It will usually require writing less code, but it is still common to back an Agent with a module. Building off our Queue example:

defmodule QueueAgent do
  def start_link(queue, name) do
    Agent.start_link(fn -> queue end, name: name)
  end

  def add(queue, item) do
    Agent.cast(queue, fn(state) -> state ++ [item] end)
  end

  def get(queue) do
    Agent.get_and_update(queue, fn([item | state]) -> {item, state} end)
  end
end

We wrote less code and we can still use the same syntax for accessing our queue.

Since we have two tools here at our disposal, what tasks are they better suited to? The examples are available on Github, so you can follow along.

They are both fast, but GenServer is slightly faster

I ran some benchmarks on my poor computer to see about how fast the same, simple functions run in both a GenServer and an Agent:

## CreateBench
benchmark  iterations   average time 
Agent          100000   11.54 µs/op
GenServer      100000   12.03 µs/op
## PlainBench
benchmark  iterations   average time 
add        1000000000   0.01 µs/op
get        1000000000   0.01 µs/op
## QueueAgentBench
benchmark  iterations   average time 
set            500000   6.45 µs/op
add            200000   8.40 µs/op
get            100000   12.84 µs/op
## QueueAsyncBench
benchmark  iterations   average time 
add           1000000   2.01 µs/op
get sync       200000   7.66 µs/op
## QueueBench
benchmark  iterations   average time 
set            500000   6.25 µs/op
add            500000   7.57 µs/op
get            100000   12.57 µs/op

The first set is just to create a process using GenServer.start_link or Agent.start_link and here you see that GenServer is slightly slower.

When it comes to the messages you’ll be passing to the Queue process, look at QueueBench (GenServer version) and QueueAgentBench (Agent version). You’ll see that time difference between the two functions are very slight, but the GenServer version is slightly faster. This makes sense considering that Agent is built on top of GenServer.

Compare these against the PlainBench results, where I had similar basic operations, but not run in a separate process and waiting for a response. With these results, it’s pretty clear that the parts that take the longest are related to message passing and communication between the processes.

I added the QueueAsyncBench just for fun, showing you that an async call to add a item to the queue is blazing fast. After all, it’s only waiting to receive the :ok message, confirming that the Queue process has been sent the message. It’s probably good to note here that the :ok message doesn’t ensure that the Queue process received the message, just that it was sent to the process.

Also, make sure to note that these benchmarks are measured in microseconds, so the one microsecond difference between a GenServer and an Agent is imperceptible and won’t matter except under very heavy load.

Agents are easier to write for simple cases

Since both state management tools are fast, it’s more up to our preference. I tend to optimize for developer ergonomics and code maintainability.

For me, Agents are easier to write for simple things, like maintaining a simple queue or collecting results from multiple, concurrent tasks. In these cases, the Agent is just accepting some data and adding it to a list or iterating over a set of data to return a simple result.

If you’re new to Elixir or functional programming in general, you might find the anonymous-function-as-argument part of calls to Agent actually harder to write. Keep reading and writing them and it’ll come naturally pretty quickly.

GenServer is easier to understand in more complex cases

Part of this comes from simply having to separate functions to have a nice interface and handle callbacks. I have more opportunities for code documentation and the functions are likely to be more well thought out and single-purposed as a result.

Additionally, if the GenServer you’re writing is part of a package you’re building, GenServer makes it harder to crack open, hopefully preventing those bugs that come up from someone doing something their not supposed to. Personally, I’m very much against this philosophy, but not everyone agrees with me. I love that Ruby and its community is very trusting of developers and things are designed so you can do whatever you want with whatever you want. I think it leads to innovative use cases.

It’s really up to you

You’re the boss and it’s also good to note that there isn’t a wrong choice here. If you choose to use Agent now and then, halfway through development, realize that it’s getting clunky and you’d prefer it to be a GenServer, extracting the calls to Agent is pretty straightforward.

It’s great tools like these that makes me agree with James Edward Grey about Erlang (and Elixir) being the most object-oriented language

Related Posts