Dynamic Supervisors in Elixir

Declan Kennedy
Expert360 Engineering
3 min readJun 14, 2018

--

In Erlang and Elixir, an application is a process which supervises a tree of child processes. This tree is, unsurprisingly, referred to as “the supervision tree”.

Traditionally, when an application is started, it will also start a supervisor:

This is fine in cases where you have a static number of processes you want to add to your supervision tree. For example, the application we’re looking at here will always require a single process for connecting to Redis (we’re using Redix, which is great). Many applications will be using Ecto, so they’ll need to add their Ecto.Repo to the supervisor when their app starts.

What isn’t as straightforward, is what to do when your application requires processes be spun-up on demand, while still being properly supervised under the supervision tree. There are a lot of uses for dynamic worker processes, such as performing background work in a web application out of band so the user doesn’t have to wait for it to complete before being given feedback.

Consider this “worker”:

We can start an interactive shell and see our worker doing some work:

/usr/src/blogs/dynamic_supervision # iex -S mix
Erlang/OTP 20 [erts-9.3.1] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.6.5) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, wid} = DynamicSupervision.Worker.start_link self(), :redis
{:ok, #PID<0.160.0>}
iex(2)> send wid, {:store, "foo", "bar"}
{:store, "foo", "bar"}
iex(3)> flush
{:hello_friend}
:ok
iex(4)> send wid, {:shutdown}
{:shutdown}
iex(5)> flush
{:goodbye}
:ok
iex(6)>

As you can see, our worker can be started and sent messages successfully. What if something goes wrong, though?

Now we’ve added a message handler which will raise an error, because sometimes good things happen to bad processes.

iex(1)>  {:ok, wid} = DynamicSupervision.Worker.start_link self(), :redis
{:ok, #PID<0.200.0>}
iex(2)> send wid, {:raise, "heck"}
{:raise, "heck"}
iex(3)>
07:23:18.213 [error] GenServer #PID<0.200.0> terminating
** (RuntimeError) heck
(dynamic_supervision) lib/dynamic_supervision/worker.ex:25: DynamicSupervision.Worker.handle_info/2
(stdlib) gen_server.erl:616: :gen_server.try_dispatch/4
(stdlib) gen_server.erl:686: :gen_server.handle_msg/6
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: {:raise, "heck"}
State: {#PID<0.197.0>, :redis}
** (EXIT from #PID<0.197.0>) shell process exited with reason: an exception was raised:
** (RuntimeError) heck
(dynamic_supervision) lib/dynamic_supervision/worker.ex:25: DynamicSupervision.Worker.handle_info/2
(stdlib) gen_server.erl:616: :gen_server.try_dispatch/4
(stdlib) gen_server.erl:686: :gen_server.handle_msg/6
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3

Interactive Elixir (1.6.5) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>

Well that wasn’t so bad, right?

Actually, if you look at the output above, when our worker crashed it took the whole IEx process down with it. This is because, when started, the worker was linked to the process which spawned it.

This is where the DynamicSupervisor becomes your best friend.

Using this simple supervisor we can now start our workers as fully supervised processes which will be restarted safely in the event of any errors, using &DynamicSupervision.DynamicSupervisor.start_child/2. We’ll need to add it to the main application supervision tree, so our application module now looks like this:

Let’s see what happens when we use it in an interactive shell:

Interactive Elixir (1.6.5) - press Ctrl+C to exit (type h() ENTER for help)iex(1)> self()
#PID<0.157.0>
iex(2)> {:ok, wid} = DynamicSupervision.DynamicSupervisor.start_child self(), :redis
{:ok, #PID<0.162.0>}
iex(3)> DynamicSupervisor.which_children DynamicSupervision.DynamicSupervisor
[{:undefined, #PID<0.162.0>, :worker, [DynamicSupervision.Worker]}]
iex(4)> send wid, {:raise, "heck"}
{:raise, "heck"}
iex(5)>
07:52:06.124 [error] GenServer #PID<0.162.0> terminating
** (RuntimeError) heck
(dynamic_supervision) lib/dynamic_supervision/worker.ex:25: DynamicSupervision.Worker.handle_info/2
(stdlib) gen_server.erl:616: :gen_server.try_dispatch/4
(stdlib) gen_server.erl:686: :gen_server.handle_msg/6
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: {:raise, "heck"}
State: {#PID<0.157.0>, :redis}
iex(6)> self()
#PID<0.157.0>
iex(7)> DynamicSupervisor.which_children DynamicSupervision.DynamicSupervisor
[{:undefined, #PID<0.165.0>, :worker, [DynamicSupervision.Worker]}]

You can see from the calls to self() before and after we crashed our worker process that the process ID for the shell remains the same. It lives! And equally importantly, when calling DynamicSupervisor.which_children again after the crash we can see that a new child process was started to replace the crashed one.

And that’s pretty much all we need to do to take advantage of dynamic supervision. Simple, but very powerful.

--

--