Note on process exits in Elixir
2023-08-14
I learned Elixir, the syntax, a while ago, now I’m in the process of learning Elixir, the semantics.
I was initially confused by the functionality around process exits. This note is my attempt of wrapping my head around the semantics and implications of process deaths, exit signals, exit trapping, and process monitoring.
Hopefully there are no glaring errors here, but I’m still learning, so all bets are off. First off, processes always exit with a reason, a value that influences how the exit plays out. A process can exit because of (I think?) three causes. The cause of death doesn’t directly influence the exit scenario. It does influence it indirectly, through the exit reason. The causes are the following: Sending an exit signal can be performed by the following invocation. What happens when a process receives an exit signal can be represented by the following bullet list. The list is evaluated until the first condition is true. When a process exits, regardless of the cause, the processes linked to that process also receive an exit signal. If the reason for exit was Trapping exits means that an exit signal—with reason other than To enable trapping exits for given process, the process needs to call By having trapping exits enabled for some process The above observations can be summarized by the following snippet. This is not the actual definition of Hopefully, I’ll learn enough Erlang to be dangerous soon enough, and see how that’s really implemented. One process—say, When Monitoring is unidirectional: when Now, a little Q&A with myself to summarize all this information. Q: When to link two processes without trapping exits? A: When you want an exit with non- Q: When to make process A: Monitoring is unidirectional, so when you want Q: Do supervisors use linking or monitoring then? A: Linking. A dying supervisor also brings down all of its children. Q: So when is monitoring useful? A: I think when the monitored process can carry on even when the monitoring process dies. Q: When to link processes with trapping exits? A: When you want two processes to react in a custom way to each other’s exits. Q: Do I ever want to trap exits without linking to anything? A: I don’t know if it’s actually done in the wild, but this allows other processes to call Q: Why not send normal messages then? A: You could do that, but Q: Do I ever want two processes to monitor each other? A: Probably not. This is very similar to linking two processes with trapping exits; there is a subtle difference though. The processes monitoring each other handle their own exit signals in the default way.Exit reason and exit signals
:normal
;Process.exit(pid, reason)
:kill
, the process unconditionally dies.:normal
, the process dies.Links
:kill
, the reason sent in the signal to the linked processes is :killed
. This is to avoid recursively and unconditionally killing everything linked to the process. If the reason was different from :kill
, the original reason is used.Trapping exits
:kill
—is delivered to a process inbox as a normal message. The shape of the message is as follows; from
being a pid:{:EXIT, from, reason}
Process.flag(:trap_exit, true)
pid
, these things change::normal
reason still don’t kill pid
, but are not entirely ignored either;:normal
or :kill
no longer kill pid
and can be reacted to;pid
no longer spells doom for pid
.exit
snippetexit
, it’s just a simple rendering of the rules stated above. In the snippet, die
is a hypothetical function cleaning up after the process.def exit(target, reason) do
target_info = Process.info(target)
trapping? = Keyword.get(target_info, :trap_exit)
linked = Keyword.get(target_info, :links)
cond do
reason == :kill ->
for p <- linked, do: exit(p, :killed)
die(target)
trapping? ->
send(target, {:EXIT, self(), reason})
reason != :normal ->
for p <- linked, do: exit(p, reason)
die(target)
self() == target ->
for p <- linked, do: exit(p, :normal)
die(target)
true ->
nil
end
Monitoring
foo
—can also monitor another process—bar
. This is done by running this line in foo
:ref = Process.monitor(bar)
bar
exits, foo
receives the message of the following shape:{:DOWN, ^ref, :process, ^bar, reason}
foo
exits, bar
is not notified.Q&A
:normal
reason in one process to make the other process exit as well.foo
monitor another process bar
?foo
to be notified that bar
exited, but not the other way around. Additionally, the monitoring process handles exits in the default way.Process.exit
on foo
, and have foo
implement some custom logic for handling that.Process.exit
is there, and maybe it fits that particular use case. I guess.