david_ex

david_ex

(Dynamic) Supervisor getting restarted by own parent only after getting killed for second time

For context, I’m implementing a basic poolboy clone using Elixir 1.6’s DynamicSupervisor and Registry. (I’ve written about my trials and tribulations on my blog if you’d like more context.)

Using the code here, let’s show an example of my issue in IEx (started with plain iex -S mix):

iex(1)> PoolToy.start_pool(name: :poolio, worker_spec: Doubler, size: 3)
:ok

This will result in the following supervision tree:

Here, we have:

Now, for my mystery: killing pid 171 in the Observer yields the following in the console:

07:39:49.704 [error] GenServer #PID<0.174.0> terminating
** (stop) killed
Last message: {:EXIT, #PID<0.171.0>, :killed}
State: %DynamicSupervisor{args: [], children: %{#PID<0.175.0> => {{Doubler, :start_link, :undefined}, :temporary, 5000, :worker, [Doubler]}, #PID<0.176.0> => {{Doubler, :start_link, :undefined}, :temporary, 5000, :worker, [Doubler]}, #PID<0.177.0> => {{Doubler, :start_link, :undefined}, :temporary, 5000, :worker, [Doubler]}}, extra_arguments: [], max_children: :infinity, max_restarts: 3, max_seconds: 5, mod: PoolToy.WorkerSup, name: {#PID<0.174.0>, PoolToy.WorkerSup}, restarts: [], strategy: :one_for_one}
 
07:39:49.705 [error] GenServer :poolio terminating
** (stop) killed
Last message: {:EXIT, #PID<0.171.0>, :killed}
State: %PoolToy.PoolMan.State{monitors: :monitors_poolio, name: :poolio, pool_sup: #PID<0.171.0>, size: 3, worker_spec: Doubler, worker_sup: #PID<0.174.0>, workers: [#PID<0.175.0>, #PID<0.176.0>, #PID<0.177.0>]}

Of importance, is that the PoolsSup dynamic supervisor doesn’t start a new instance of PoolToy.PoolSup to replace the one that was killed.

Now for chapter 2 of the mystery: after restarting a new pool instance with PoolToy.start_pool(name: :poolio, worker_spec: Doubler, size: 3) (in the same IEx session), if I once again kill the PoolToy.PoolSup instance (which isn’t named: it will be displayed with only a pid in Observer) directly descending from the named PoolsSup process I get the following:

07:46:45.987 [error] GenServer :poolio terminating
** (stop) killed
Last message: {:EXIT, #PID<0.175.0>, :killed}
State: %PoolToy.PoolMan.State{monitors: :monitors_poolio, name: :poolio, pool_sup: #PID<0.175.0>, size: 3, worker_spec: Doubler, worker_sup: #PID<0.177.0>, workers: [#PID<0.178.0>, #PID<0.179.0>, #PID<0.181.0>]}
 
07:46:45.990 [error] GenServer #PID<0.177.0> terminating
** (stop) killed
Last message: {:EXIT, #PID<0.175.0>, :killed}
State: %DynamicSupervisor{args: [], children: %{#PID<0.178.0> => {{Doubler, :start_link, :undefined}, :temporary, 5000, :worker, [Doubler]}, #PID<0.179.0> => {{Doubler, :start_link, :undefined}, :temporary, 5000, :worker, [Doubler]}, #PID<0.181.0> => {{Doubler, :start_link, :undefined}, :temporary, 5000, :worker, [Doubler]}}, extra_arguments: [], max_children: :infinity, max_restarts: 3, max_seconds: 5, mod: PoolToy.WorkerSup, name: {#PID<0.177.0>, PoolToy.WorkerSup}, restarts: [], strategy: :one_for_one}

Where pid 175 is the pool supervisor I killed and and pid 177 is the worker supervisor it had as one of its children.

But this time, a new pool supervisor (usually) gets started by the pools supervisor. (It appears that sometimes a new pool supervisor does NOT get started even the second time around.)

There’s clearly some fundamental understanding about supervision trees that I’m completely missing, as I have no idea what’s going on, besides the fact that it seems to be some sort of race condition. I don’t understand why this is happening, as the worker supervisor is :temporary, therefore only :poolio should get restarted by the parent supervisor (:poolio will then start a new worker supervisor instance). In addition, I’ve tried making the PoolSup one_for_one but that seems to make no difference.

So where’s my mistake? And of course, how can I fix it?

Marked As Solved

josevalim

josevalim

Creator of Elixir

It is a race condition with named processes. I played with the demo and I noticed that, when there is no child after kill, the PID of PoolsSup changed. This means that the PoolsSup is actually crashing and being restarted. But why would PoolsSup crash? Likely because it reached its max restart strategy.

So I started the shell with iex --loger-sasl-reports true -S mix and killed PID again (this time <0.141.0> and I got this:

00:08:31.465 [error] Child :undefined of Supervisor PoolToy.PoolsSup terminated
** (exit) killed
Pid: #PID<0.141.0>
Start Call: PoolToy.PoolSup.start_link([name: :poolio, worker_spec: Doubler, size: 3])
Restart: :permanent
Shutdown: :infinity
Type: :supervisor

00:08:31.465 [error] Child PoolToy.PoolMan of Supervisor #PID<0.294.0> (PoolToy.PoolSup) failed to start
** (exit) already started: #PID<0.142.0>
Start Call: PoolToy.PoolMan.start_link([pool_sup: #PID<0.294.0>, name: :poolio, worker_spec: Doubler, size: 3])
Restart: :permanent
Shutdown: 5000
Type: :worker

00:08:31.465 [error] Child :undefined of Supervisor PoolToy.PoolsSup failed to start
** (exit) shutdown: failed to start child: PoolToy.PoolMan
    ** (EXIT) already started: #PID<0.142.0>
Start Call: PoolToy.PoolSup.start_link([name: :poolio, worker_spec: Doubler, size: 3])
Restart: :permanent
Shutdown: :infinity
Type: :supervisor

00:08:31.465 [error] Child PoolToy.PoolMan of Supervisor #PID<0.295.0> (PoolToy.PoolSup) failed to start
** (exit) already started: #PID<0.142.0>
Start Call: PoolToy.PoolMan.start_link([pool_sup: #PID<0.295.0>, name: :poolio, worker_spec: Doubler, size: 3])
Restart: :permanent
Shutdown: 5000
Type: :worker

00:08:31.465 [error] Child :undefined of Supervisor PoolToy.PoolsSup failed to start
** (exit) shutdown: failed to start child: PoolToy.PoolMan
    ** (EXIT) already started: #PID<0.142.0>
Start Call: PoolToy.PoolSup.start_link([name: :poolio, worker_spec: Doubler, size: 3])
Restart: :permanent
Shutdown: :infinity
Type: :supervisor

00:08:31.466 [error] Child PoolToy.PoolMan of Supervisor #PID<0.296.0> (PoolToy.PoolSup) failed to start
** (exit) already started: #PID<0.142.0>
Start Call: PoolToy.PoolMan.start_link([pool_sup: #PID<0.296.0>, name: :poolio, worker_spec: Doubler, size: 3])
Restart: :permanent
Shutdown: 5000
Type: :worker

00:08:31.466 [error] Child :undefined of Supervisor PoolToy.PoolsSup failed to start
** (exit) shutdown: failed to start child: PoolToy.PoolMan
    ** (EXIT) already started: #PID<0.142.0>
Start Call: PoolToy.PoolSup.start_link([name: :poolio, worker_spec: Doubler, size: 3])
Restart: :permanent
Shutdown: :infinity
Type: :supervisor

00:08:31.466 [error] Child :undefined of Supervisor PoolToy.PoolsSup caused shutdown
** (exit) :reached_max_restart_intensity
Start Call: PoolToy.PoolSup.start_link([name: :poolio, worker_spec: Doubler, size: 3])
Restart: :permanent
Shutdown: :infinity
Type: :supervisor

00:08:31.466 [error] Child PoolToy.PoolsSup of Supervisor #PID<0.134.0> (Supervisor.Default) terminated
** (exit) shutdown
Pid: #PID<0.137.0>
Start Call: PoolToy.PoolsSup.start_link([])
Restart: :permanent
Shutdown: :infinity
Type: :supervisor

00:08:31.466 [info]  Child PoolToy.PoolsSup of Supervisor #PID<0.134.0> (Supervisor.Default) started
Pid: #PID<0.297.0>
Start Call: PoolToy.PoolsSup.start_link([])
Restart: :permanent
Shutdown: :infinity
Type: :supervisor

00:08:31.470 [error] GenServer :poolio terminating
** (stop) killed
Last message: {:EXIT, #PID<0.141.0>, :killed}
State: %PoolToy.PoolMan.State{monitors: :monitors_poolio, name: :poolio, pool_sup: #PID<0.141.0>, size: 3, worker_spec: Doubler, worker_sup: #PID<0.144.0>, workers: [#PID<0.145.0>, #PID<0.146.0>, #PID<0.147.0>]}

00:08:31.470 [error] GenServer #PID<0.144.0> terminating
** (stop) killed
Last message: {:EXIT, #PID<0.141.0>, :killed}
State: %DynamicSupervisor{args: [], children: %{#PID<0.145.0> => {{Doubler, :start_link, :undefined}, :temporary, 5000, :worker, [Doubler]}, #PID<0.146.0> => {{Doubler, :start_link, :undefined}, :temporary, 5000, :worker, [Doubler]}, #PID<0.147.0> => {{Doubler, :start_link, :undefined}, :temporary, 5000, :worker, [Doubler]}}, extra_arguments: [], max_children: :infinity, max_restarts: 3, max_seconds: 5, mod: PoolToy.WorkerSup, name: {#PID<0.144.0>, PoolToy.WorkerSup}, restarts: [], strategy: :one_for_one}

00:08:31.472 [error] Process :poolio (#PID<0.142.0>) terminating
** (exit) killed
    (stdlib) gen_server.erl:432: :gen_server.decode_msg/9
    (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Initial Call: PoolToy.PoolMan.init/1
Ancestors: [#PID<0.141.0>, PoolToy.PoolsSup, #PID<0.134.0>, #PID<0.133.0>]
Message Queue Length: 3
Messages: [{:EXIT, #PID<0.145.0>, :shutdown}, {:EXIT, #PID<0.146.0>, :shutdown}, {:EXIT, #PID<0.147.0>, :shutdown}]
Links: [#PID<0.144.0>]
Dictionary: []
Trapping Exits: true
Status: :running
Heap Size: 1598
Stack Size: 27
Reductions: 2425

00:08:31.472 [error] Process #PID<0.144.0> terminating
** (exit) killed
    (stdlib) gen_server.erl:432: :gen_server.decode_msg/9
    (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Initial Call: PoolToy.WorkerSup.init/1
Ancestors: [#PID<0.141.0>, PoolToy.PoolsSup, #PID<0.134.0>, #PID<0.133.0>]
Message Queue Length: 0
Messages: []
Links: [#PID<0.142.0>]
Dictionary: []
Trapping Exits: true
Status: :running
Heap Size: 2586
Stack Size: 27
Reductions: 4995

You can see that after 0.141.0 dies, it tries to start another process named poolio. However, because the currently running poolie process still exists, as it did not receive the linked exit failure from its parent, it fails with already started message. If we attempt to start 3 times before the original poolie process dies, then the supervisor crashes too.

Note this error only happens because the supervisor terminated before it was able to terminate its children. In practice, this error is unlikely to happen, since termination is guaranteed. Except if you kill the supervisor, as we have seen here.

Also Liked

josevalim

josevalim

Creator of Elixir

Everything is perfect, except for this sentence:

This would not happen in real-life because in real-life shut down of your application happens by sending a :shutdown message to the supervisor. Opposite to :kill, :shutdown is trappable, which means the supervisor can terminate its children properly before exiting.

In other words, the error you are seeing would only happen if you send an :kill signal to the supervisor, but nothing in a system will actually do this. I guess it would also happen if the Supervisor implementation has a bug, but that is even more unlikely.

david_ex

david_ex

I was indeed only checking via the observer, but it seems that it isn’t an observer bug: calling DynamicSupervisor.which_children(PoolToy.PoolsSup) returns [] when the child isn’t restarted.

david_ex

david_ex

Thank you so much for taking the time to look into this. From your reply, can I conclude that:

  • there is nothing inherently wrong with the app’s supervision tree
  • the race condition takes place because I killed a supervisor directly (which wouldn’t happen “in real life”)

Therefore, the main problem is that by killing the supervisor directly I was creating a situation (that wouldn’t happen in practice) where the supervisor was dead but its children were still (briefly) alive.

In real life, this situation wouldn’t have happened because the supervisor would usually die due to its children (e.g. max restart intensity hit). In this case, the supervisor would have properly terminated all of the children before restarting, preventing the problematic race condition.

Please correct me if my understanding of your reply is incorrect, and thanks again for shedding light on the problem, as well as bringing the logger-sasl-reports option to my attention.

Where Next?

Popular in Questions Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
gonzofish
I’m currently trying to understand how to join three tables using Ecto. All the examples I’ve seen use 2, so maybe I’m just missing somet...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
Exadra37
Sometimes I want to check if the input into a function is not a blank string. My first approach: defmodule Example do def do_stuff(s...
New
sabri
Can someone explain the settings of pool_size of Ecto in config file? and what is the recommend size? Thanks
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

Other popular topics Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New

We're in Beta

About us Mission Statement