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.
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.
1 2 3 |
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,
1 2 3 4 5 |
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.
When a child process fails, something like this happens,
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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,
1 |
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,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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.
- The ABCs of OTP — Great video by Jesse J. Anderson
- The Zen of Erlang — I highly recommend this one
- ElixirShool — One of the greatest websites to learn elixir
- Programming Elixir Book — A really great book written by Jose Valim and Dave Thomas
You can find me in twitter as @jorgechavz if you have any question or suggestion.