Supervisors and Workers

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:

otp-supervisor/1/sequence/lib/sequence/application.ex
 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:

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.