Supervisors and Workers
Understand how the supervisor supervises and manages the processes' states.
We'll cover the following
Elixir doesn’t worry much about code that crashes. Instead, it makes sure the overall application keeps running. This might sound contradictory, but it isn’t. Think of a typical application. If an unhandled error causes an exception to be raised, the application stops. Nothing else gets done until it’s restarted. If it’s a server handling multiple requests, they all might be lost.
The issue here is that one error takes the whole application down. Now, imagine that our application consists of hundreds or thousands of processes, each handling just a small part of a request. If one of those crashes, everything else carries on. We might lose the work it’s doing, but we can design our applications to minimize even that risk. And when that process gets restarted, we’re back running at 100%.
In the Elixir and OTP worlds, supervisors perform all of this process monitoring and restarting.
Supervisor
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’s 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 process-monitoring facilities. We can write supervisors as separate modules, but the Elixir style is to include them inline. The easiest way to get started is to create our project with the --sup
flag. Let’s do this for our sequence server.
$ mix new --sup sequence
The only apparent difference is the appearance of the file lib/sequence/application
. Let’s have a look inside (Note that we 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 to be supervised. We copy the second version of the Sequence.Server
module from the last chapter into the lib/sequence
folder. Then, we uncomment and change the line in the child_list
to reference this module:
defmodule Sequence.Application do
@moduledoc false
use Application
def start(_type, _args) do
children = [
{ Sequence.Server, 123},
]
opts = [strategy: :one_for_one, name: Sequence.Supervisor]
Supervisor.start_link(children, opts)
end
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 inSequence.Server
. This code is unchanged. It callsGenServer.start_link
to create a GenServer process.
Now, we’re up and running. Let’s try it:
iex> Sequence.Server.increment_number 3
:ok
iex> Sequence.Server.next_number
126
So far, so good. But the key with a supervisor is that it’s 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> Sequence.Server.increment_number "cat"
:ok
iex>
10:04:05.805 [error] GenServer Sequence.Server terminating
** (ArithmeticError) bad argument in arithmetic expression
:erlang.+(123, "cat")
(sequence 0.1.0) lib/sequence/server.ex:31: Sequence.Server.handle_cast/2
(stdlib 3.15.2) gen_server.erl:695: :gen_server.try_dispatch/4
(stdlib 3.15.2) gen_server.erl:771: :gen_server.handle_msg/6
(stdlib 3.15.2) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
Last message: {:"$gen_cast", {:increment_number, "cat"}}
State: 126
Run commands given in the above two snippets to execute the code below:
Get hands-on with 1200+ tech skills courses.