An Elixir supervisor has just one purpose—it manages one or more processes. (As we’ll discuss later, these processes can be workers or other supervisors.)
At its simplest, a supervisor is a process that uses the OTP supervisor behavior. It is given a list of processes to monitor and is told what to do if a process dies, and how to prevent restart loops (when a process is restarted, dies, gets restarted, dies, and so on).
To do this, the supervisor uses the Erlang VM’s process-linking and -monitoring facilities. We talked about these when we covered spawn.
You can write supervisors as separate modules, but the Elixir style is to include them inline. The easiest way to get started is to create your project with the --sup flag. Let’s do this for our sequence server.
» | $ mix new --sup sequence |
| * creating README.md |
| * creating .formatter.exs |
| * creating .gitignore |
| * creating mix.exs |
| * creating config |
| * creating config/config.exs |
| * creating lib |
| * creating lib/sequence.ex |
| * creating lib/sequence/application.ex |
| * creating test |
| * creating test/test_helper.exs |
| * creating test/sequence_test.exs |
The only apparent difference is the appearance of the file lib/sequence/application. Let’s have a look inside (I stripped out some comments…):
| defmodule Sequence.Application do |
| @moduledoc false |
| |
| use Application |
| |
| def start(_type, _args) do |
| children = [ |
| # {Sequence.Worker, arg}, |
| ] |
| |
| opts = [strategy: :one_for_one, name: Sequence.Supervisor] |
| Supervisor.start_link(children, opts) |
| end |
| end |
Our start function now creates a supervisor for our application. All we need to do is tell it what we want supervised. Copy the second version of the Sequence.Server module[35] from the last chapter into the lib/sequence folder. Then uncomment and change the line in the child_list to reference this module:
| def start(_type, _args) do |
| children = [ |
» | { Sequence.Server, 123}, |
| ] |
| |
| opts = [strategy: :one_for_one, name: Sequence.Supervisor] |
| Supervisor.start_link(children, opts) |
| end |
Let’s look at what’s going to happen:
When our application starts, the start function is called.
It creates a list of child server modules. In our case, there’s just one, the Sequence.Server. Along with the module name, we specify an argument to be passed to the server when we start it.
We call Supervisor.start_link, passing it the list of child specifications and a set of options. This creates a supervisor process.
Now our supervisor process calls the start_link function for each of its managed children. In our case, this is the function in Sequence.Server. This code is unchanged—it calls GenServer.start_link to create a GenServer process.
Now we’re up and running. Let’s try it:
| $ iex -S mix |
| Compiling 2 files (.ex) |
| Generated sequence app |
| iex> Sequence.Server.increment_number 3 |
| :ok |
| iex> Sequence.Server.next_number |
| 126 |
So far, so good. But the key thing with a supervisor is that it is supposed to manage our worker process. If it dies, for example, we want it to be restarted. Let’s try that. If we pass something that isn’t a number to increment_number, the process should die trying to add it to the current number.
| iex(3)> Sequence.Server.increment_number "cat" |
| :ok |
| iex(4)> 14:22:06.269 [error] GenServer Sequence.Server terminating |
| Last message: {:"$gen_cast", {:increment_number, "cat"}} |
| State: [data: [{'State', "My current state is '127', and I'm happy"}]] |
| ** (exit) an exception was raised: |
| ** (ArithmeticError) bad argument in arithmetic expression |
| (sequence) lib/sequence/server.ex:27: Sequence.Server.handle_cast/2 |
| (stdlib) gen_server.erl:599: :gen_server.handle_msg/5 |
| (stdlib) proc_lib.erl:239: :proc_lib.init_p_do_apply/3 |
| iex(4)> Sequence.Server.next_number |
| 123 |
| iex(5)> Sequence.Server.next_number |
| 124 |
We get a wonderful error report that shows us the exception, along with a stack trace from the process. We can also see the message we sent that triggered the problem.
But when we then ask our server for a number, it responds as if nothing had happened. The supervisor restarted our process for us.
This is excellent, but there’s a problem. The supervisor restarted our sequence process with the initial parameters we passed in, and the numbers started again from 123. A reincarnated process has no memory of its past lives, and no state is retained across a crash.