How To Get the Parent of an Elixir Process

JB Steadman

This used to be a question with a somewhat unsatisfying answer. No longer. As of OTP 25.0, via this pull request, the parent of every child process is stored in the process info table of the child, and retrievable via process_info/2 (Erlang) and Process.info/2 (Elixir).

For example:

iex(1)> self()
#PID<0.105.0>
iex(2)> pid = spawn(fn -> receive do :stop -> :ok end end)
#PID<0.106.0>
iex(3)> Process.info(pid, :parent)
{:parent, #PID<0.105.0>}
iex(4)> :erlang.process_info(pid, :parent)
{:parent, #PID<0.105.0>}

The ultimate ancestor of all processes is the :init process. Its parent is :undefined, as seen here:

iex(1)> init = Process.whereis(:init)
#PID<0.0.0>
iex(2)> Process.info(init, :parent)
{:parent, :undefined}

The way things used to be

Prior to OTP 25.0, ancestry information for Erlang processes was tracked only for processes spawned by the OTP standard behaviours: gen_server, gen_statem, gen_event and supervisor. Rather than directly using Erlang’s native spawn() functions, the OTP behaviours spawn processes via proc_lib, a module “used to start processes adhering to the OTP design principles.”

proc_lib uses the process dictionary to track ancestry information. When it spawns a process, proc_lib stores the full list of available ancestor pids in the new process’ dictionary, under the $ancestors key.

Elixir’s Agent, GenServer and Supervisor are built on top of Erlang’s OTP modules. When we start any of these processes, proc_lib spawns the process and tracks ancestry information in the process dictionary. Here’s an example:

iex(2)> {:ok, pid} = Supervisor.start_link(MySupervisor, [])
{:ok, #PID<0.111.0>}
iex(3)> Process.info(pid, :dictionary)
{:dictionary,
 [
   "$ancestors": [#PID<0.105.0>, #PID<0.79.0>],
   "$initial_call": {:supervisor, MySupervisor, 1}
 ]}

We see that two ancestors are listed for the supervisor: #PID<0.105.0> and #PID<0.79.0>. Notably, the :init process (#PID<0.0.0>) is not listed as an ancestor. What’s going on?

The IEx shell used proc_lib to spawn #PID<0.105.0>, therefore we have its available ancestry information. However, #PID<0.79.0> was not started by proc_lib, therefore we have no ancestry information. This highlights a key limitation of obtaining ancestor info via $ancestors: it only works under certain circumstances, typically involving OTP.

This is almost certainly fine for most use cases. For any processes we truly care about, Elixir and Erlang encourage us to put them in a supervision tree. We will know the parent of any process in the tree.

Although, as of Elixir 1.15, it is not started via proc_lib, Elixir’s Task also tracks ancestry info using the $ancestors key in the process dictionary.

$ancestors is still useful

If we’re running a pre-25.0 version of OTP, $ancestors is the only sometimes-available mechanism to find the parent of a process. If we don’t know what version of OTP our code might run under, $ancestors can be a fallback in case Process.info(pid, :parent) is not supported.

Additionally, Process.info(pid, :parent) returns nil for any process that is no longer alive. If, for example, we want to know the grandparent of a process, but the parent process has died, using $ancestors of the grandchild process is our only hope.

$ancestors is longer shrouded in uncertainty

$ancestors has historically existed in a gray area. The proc_lib module documentation states that proc_lib tracks ancestor info, but it doesn’t say how, and the $ in $ancestors indicates it’s for internal use. Discussions about using it have featured gentle cautions and direct warnings, sometimes accompanied by skeptical queries asking “why would you want to know the parent of a process?”.

Now that parent info is available for all Erlang processes, the “why would you want to know the parent of a process?” pushback should hopefully die. By including the ability to find the parent of a process, the language itself has recognized that we might have legitimate reasons for doing so. Like any language feature, it can be abused. We can all judge for ourselves based on our own circumstances.

Additionally, OTP’s inclusion of process_info(pid, :parent) has lifted the cloud around $ancestors. Warnings about $ancestors have centered on the risk that it’s an undocumented feature subject to change. Now it doesn’t matter.

$ancestors is not disappearing from older versions of OTP. process_info(pid, :parent) is not disappearing from future versions of OTP. No matter what version of OTP we might be using, there is a reliable way to get the parent of a process in most/all situations we care about. Even if we don’t know what version of OTP our code might run under, we can develop our code to gracefully handle any circumstance.

There’s a hex package for that

The process_tree package includes a single module called ProcessTree that handles all the complexities related to finding the parent of a process. It works for older and newer versions of OTP, using Process.info(pid, :parent) when available, and falling back to $ancestors when necessary.