You Don’t Know OTP

This should be named “Introduction to OTP” but I always wanted to be like Kyle.

When I started learning Elixir, the acronym OTP didn’t mean anything to me. But this is one of the main features of Elixir.

If you are new with Elixir, read on.

First Things First

OTP stands for Open Telecom Platform.

It is a collections of libraries and design-principles written mostly in Erlang to make concurrent, distributed and fault-tolerant applications.

Since Elixir uses BEAM, the same Virtual Machine as Erlang, you have access to all these tools for building powerful software.

As Jesse J. Anderson mentions in his talk, “The ABCs of OTP,”

  • Collection of libraries
  • Hierarchy of supervisors & workers

Heading

People normally see an App as a unit. If something breaks, then the App is ruined. However, with OTP we can see an app as several components working together and watching each other’s status. This assures that our app will be fine is something weird happens.

app different pieces

Thanks to OTP, we can separate all its pieces and make an app as a group of components that don’t depend directly on each. This is the architecture we need to make distributed, concurrent and fault-tolerant applications.

Basics of OTP

Supervisor, GenServer, Agent, and Task are the basic concept behind OTP. If we want to understand what they are, we need to start from the most basic: processes.

Processes

Processes in Elixir are not like those in a operating system. In Elixir and Erlang they are super-lightweight. We can spin up a thousand of them like nothing.

iex(1)> spawn(fn -> IO.puts("Hello OTP") end)
Hello OTP
#PID<0.86.0>

With this, we create a brand new process and we evaluate a function asynchronously. Every process has its own pid (i.e., process ID). It identifies each process we create.

A given pid dies after the function is evaluated. For example,

iex(1)> pid = spawn(fn -> IO.puts("Hello OTP") end)
Hello OTP
#PID<0.86.0>
iex(2)> Process.alive?(pid)
false

As soon as the process runs, it dies automatically.

Handling Errors

“Let it crash!”

You’ll hear this expression often in the Elixir/Erlang world. This is counterintuitive because that’s exactly what you want to prevent when you create an application.

But in Elixir you don’t have to be afraid of errors. We need to embrace them so we can control them.

Elixir processes are isolated. If one of them fails, we can fix it without impacting others.

In OTP if some process fails, another process called supervisor will restart it.

Supervisors

Supervisors are also processes. Their main purpose is to monitoring other processes. If a child process fails, then the supervisor restarts it with a fresh state.

Strategies

OTP allow us to configure how supervisors will handle failures. For example,

  • Whether or not to restart the process
  • Restart only the process that failed or all processes linked to the supervisor
  • Restarting just when the child process terminates abnormally

These configurations are called Strategies.

Supervision Tree

Supervisors also have supervisors organized into what’s called a Supervision Tree.

Supervision tree

When a child process fails, something like this happens,

supervision tree gif

Messages

Communication among processes is pretty easy,

  • send/2 if you want to send a message to an specific process
  • receive/1 if you want to receive a message from other processes

Whenever we send a message to another process, it will be stored in a virtual mail box inside the receiving process.

The receive/1 function will be checking all the time until it matches a specific value. It doesn’t matter if you handle the message or not, the message will be stored in the receiving process’ virtual mailbox.

Listening Example

This is an example of a process that’s constantly listening for messages.
You can see a start function and a loop function. Note that the latter is called recursively.

defmodule SpeakersProcess do

  def start do
    spawn(__MODULE__, :loop, [%{}])
  end

  def loop(state) do
    receive do
      {:add, name} ->
        speaker = SpeakerRepo.find(name)
        new_state = Map.put(state, name, speaker)

        loop(new_state)
        
      {:remove, name} ->
        new_state = Map.delete(state, name)

        loop(new_state) 
        
      {:team, pid} ->
        send(pid, {:ok, state})

        loop
    end
  end
  
end

This is what’s happening in the example above,

We execute a function in a separate process

  • We execute a function in a separate process spawn(__MODULE__, :loop, [%{}])
  • __MODULE__ refers to the SpeakersProcess module.
  • :loop the function that we will execute within this __MODULE__
  • [%{}] The initial state

Basically, this is equivalent to,

SpeakerProcess.loop([%{}])

But in a different process.

An in our case we have the function loop waiting with for new messages with receive

GenServer

This can get pretty complicated. Fortunately, it is made simple by using GenServer.

GenServer is an implementation for starting and handling all in and out messages in your process.

With GenServer, the example above would look like this,

defmodule GenServerExample do
  use GenServer

  def start do
    GenServer.start(__MODULE__, :ok, [])
  end

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

  def handle_cast({:add, name}, state) do
    speaker = SpeakerRepo.find(name)
    new_state = Map.put(state, name, speaker)
    {:noreply, new_state}
  end

  def handle_cast({:remover, name}, state) do
    new_state = Map.delete(state, name)
    {:noreply, new_state} 
  end

  def handle_call(:team, _from, state) do
    {:reply, state, state}
  end


end

GenServer.start(__MODULE__, :ok, [])

This is pretty much the same as our previous implementation of spawn but in this case we will trigger an init function in the module. This function let’s you set the initial process state.

handle_cast

We use this function in order to cast any message that we receive. The message will pattern matched with any handle_cast function declared in our module. At the end of this function we return a two-element tuple {:noreply, new_state} to give a new state to our server.

handle_call

We use this function in order to send any message back the the process that pinged us. At the end, we reply with a three-element tuple {:reply, state_to_reply, new_state} where the second element is actually the message that we will send back, and the third one is the new state for our process.

What’s Next?

Thanks to you for making it to the end.

I think this article will help you to understand many tricky things about OTP. However, this topic goes deeper (and darker) as you dig more and more.

Here are some of the sources I found really helpful.

You can find me in twitter as @jorgechavz if you have any question or suggestion.

Focus Mode

Contact Request

Close

We will call you right away. All information is kept private