A Brief Guide to OTP in Elixir

Article by Gints Dreimanis
September 30th, 2020

In this article, I will introduce you to OTP, look at basic process loops, the GenServer and Supervisor behaviours, and see how they can be used to implement an elementary process that stores funds.

(This article assumes that you are already familiar with the basics of Elixir. If you’re not, you can check out the Getting Started guide on Elixir’s website or use one of the other resources listed in our Elixir guide.)

What is OTP?

OTP is an awesome set of tools and libraries that Elixir inherits from Erlang, a programming language on whose VM it runs.

OTP contains a lot of stuff, such as the Erlang compiler, databases, test framework, profiler, debugging tools. But, when we talk about OTP in the context of Elixir, we usually mean the Erlang actor model that is based on lightweight processes and is the basis of what makes Elixir so efficient.

Processes

At the foundation of OTP, there are tiny things called processes.

Unlike OS processes, they are really, really lightweight. Creating them takes microseconds, and a single machine can easily run multiple thousands of them, simultaneously.

Processes loosely follow the actor model. Every process is basically a mailbox that can receive messages, and in response to those messages it can:

• Create new processes.
• Send messages to other processes.
• Modify its private state.

Spawning processes

The most basic way to spawn a process is with the spawn command. Let’s open IEx and launch one.

iex(1)> process = spawn(fn -> IO.puts("hey there!") end)


The above function will return:

hey there!
#PID<0.104.0>


First is the result of the function, second is the output of spawn – PID, a unique process identification number.

Meanwhile, we have a problem with our process. While it did the task we asked it to do, it seems like it is now… dead? 😱

Let’s use its PID (stored in the variable process) to query for life signs.

iex(2)> Process.alive?(process)
false


If you think about it, it makes sense. The process did what we asked it to do, fulfilled its reason for existence, and closed itself. But there is a way to extend the life of the process to make it more worthwhile for us.

Turns out, we can extend the process function to a loop that can hold state and modify it.

For example, let’s imagine that we need to create a process that mimics the funds in a palace treasury. We’ll create a simple process to which you can store or withdraw funds, and ask for the current balance.

We’ll do that by creating a loop function that responds to certain messages while keeping the state in its argument.

defmodule Palace.SimpleTreasury do

def loop(balance) do
{:store, amount} ->
loop(balance + amount)
{:withdraw, amount} ->
loop(balance - amount)
{:balance, pid} ->
send(pid, balance)
loop(balance)
end
end
end


In the body of the function, we put the receive statement and pattern match all the messages we want our process to respond to. Every time the loop runs, it will check from the bottom of the mailbox (in order they were received) for messages that match what we need and process them.

If the process sees any messages with atoms store, withdraw, balance, those will trigger certain actions.

To make it a bit nicer, we can add an open function and also dump all the messages we don’t need to not pollute the mailbox.

defmodule Palace.SimpleTreasury do

def open() do
loop(0)
end

def loop(balance) do
{:store, amount} ->
loop(balance + amount)
{:withdraw, amount} ->
loop(balance - amount)
{:balance, pid} ->
send(pid, balance)
loop(balance)
_ ->
loop(balance)
end
end
end
end


While this seems quite concise, there’s already some boilerplate lurking, and we haven’t even covered corner cases, tracing, and reporting that would be necessary for production-level code.

In real life, we don’t need to write code with receive do loops. Instead, we use one of the behaviours created by people much smarter than us.

GenServer and Supervisor behaviours

Many processes follow certain similar patterns. To abstract over these patterns, we use behaviours. Behaviours have two parts: abstract code that we don’t have to implement and a callback module that is implementation-specific.

In this article, I will introduce you to GenServer, short for generic server, and Supervisor. Those are not the only behaviours out there, but they certainly are one of the most common ones.

GenServer

To start off, let’s create a module called Treasury, and add the GenServer behaviour to it.

defmodule Palace.Treasury do
use GenServer
end


This will pull in the necessary boilerplate for the behaviour. After that, we need to implement the callbacks for our specific use case.

Here’s what we will use for our basic implementation.

 Callback What it does What it usually returns init(state) Initializes the server. {:ok, state} handle_cast(pid, message) An async call that doesn’t demand an answer from the server. {:noreply, state} handle_call(pid, from, message) A synchronous call that demands an answer from the server. {:reply, reply, state}

Let’s start with the easy one – init. It takes a state and starts a process with that state.

 def init(balance) do
{:ok, balance}
end


Now, if you look at the simple code we wrote with receive, there are two types of triggers. The first one (store and withdraw) just asks for the treasury to update its state asynchronously, while the second one (get_balance) waits for an answer. handle_cast can handle the async ones, while handle_call  can handle the synchronous one.

To handle adding and subtracting, we will need two casts. These take a message with the command and the transaction amount and update the state.

def handle_cast({:store, amount}, balance) do
end

def handle_cast({:withdraw, amount}, balance) do
end


Finally, handle_call  takes the balance call, the caller, and state, and uses all that to reply to the caller and return the same state.

def handle_call(:balance, _from, balance) do
end


These are all the callbacks we have:

defmodule Palace.Treasury do
use GenServer

def init(balance) do
{:ok, balance}
end

def handle_cast({:store, amount}, balance) do
end

def handle_cast({:withdraw, amount}, balance) do
end

def handle_call(:balance, _from, balance) do
end
end


To hide the implementation details, we can add client commands in the same module. Since this will be the only treasury of the palace, let’s also give a name to the process equal to its module name when spawning it with start_link. This will make it easier to refer to it.

defmodule Palace.Treasury do
use GenServer

# Client

def open() do
end

def store(amount) do
GenServer.cast(__MODULE__, {:store, amount})
end

def withdraw(amount) do
GenServer.cast(__MODULE__, {:withdraw, amount})
end

def get_balance() do
GenServer.call(__MODULE__, :balance)
end

# Callbacks

def init(balance) do
{:ok, balance}
end

def handle_cast({:store, amount}, balance) do
end

def handle_cast({:withdraw, amount}, balance) do
end

def handle_call(:balance, _from, balance) do
end
end


Let’s try it out:

iex(1)> Palace.Treasury.open()
{:ok, #PID<0.138.0>}
iex(2)> Palace.Treasury.store(400)
:ok
iex(3)> Palace.Treasury.withdraw(100)
:ok
iex(4)> Palace.Treasury.get_balance()
300


It works. 🥳

Supervisor

However, just letting a treasury run without supervision is a bit irresponsible, and a good way to lose your funds or your head. 😅

Thankfully, OTP provides us with the supervisor behaviour. Supervisors can:

• start and shutdown applications,
• provide fault tolerance by restarting crashed processes,
• be used to make a hierarchical supervision structure, called a supervision tree.

Let’s equip our treasury with a simple supervisor.

defmodule Palace.Treasury.Supervisor do
use Supervisor

end

def init(_init_arg) do
children = [
%{
id: Palace.Treasury,
start: {Palace.Treasury, :open, []}
}
]

Supervisor.init(children, strategy: :one_for_one)
end
end


In its most basic, a supervisor has two functions: start_link(), which runs the supervisor as a process, and init, which provides the arguments necessary for the supervisor to initialize.

Things we need to pay attention to are:

• The list of children. Here, we list all the processes that we want the supervisor to start, together with their init functions and starting arguments. Each of the processes is a map, with at least the id and start keys in it.
• Supervisor’s init function. To it, we supply the list of children processes and a supervision strategy. Here, we use :one_for_one – if a child process will crash, only that process will be restarted. There are a few more.

Running the Palace.Treasury.Supervisor.start_link() function will open a treasury, which will be supervised by the process. If the treasury crashes, it will get restarted with the initial state – 0.

If we wanted, we could add several other processes to this supervisor that are relevant to the treasury function, such as a process that can exchange looted items for their monetary value.

Additionally, we could also duplicate or persist the state of the treasury process to make sure that our funds are not lost when the treasury process crashes.

Since this is a basic guide, I will let you investigate the possibilities by yourself.