View Source ProcessTree (ProcessTree v0.1.2)

ProcessTree is a module for avoiding global state in Elixir applications.

Motivation

It's common in Elixir apps to rely on global references or global variables for locating application services or configuration parameters. This presents problems when we test our code. We need to work around our global data, for example by setting/resetting environment variables and using async: false.

ProcessTree solves the problem by localizing data to a particular branch of the Elixir process tree. When testing with ExUnit, each ExUnit test process, and all processes spawned by the test process, can use get/1 or get/2 to see their own private copy of the data of interest.

How to use get()

get/1 and get/2 can be used to make child processes "see" values stored in the process dictionaries of their ancestor processes. This is useful, for example, in ExUnit tests that spawn processes, such as tests that start LiveViews and GenServers.

To make data visible to child processes via get/1 and get/2, we first put the data into the process dictionary of an ancestor process - in this case, an ExUnit test pid:

test "some test that starts a GenServer" do
  # ...
  # add the data of interest to the process dictionary of the test pid
  Process.put(:some_key, some_value)

  # The GenServer process started here can use ProcessTree.get() to see
  # the value we've bound to :some_key
  server = MyGenserver.start_link()
  # ...
end

Example use case

Customizing environment variables in ExUnit tests while preserving async: true.

Smoothing over process ancestry complications

OTP 25 introduced the ability to find the parent of a process via Process.info/2. Prior to OTP 25, it was possible to find the parent of a process only for specific processes such as Task, GenServer, Agent, and Superivsor.

Even under OTP 25+, Process.info/2 is only useful for processes that are still alive, meaning it can't be used, for example, to find the grandparent of a process if the parent of the process has died.

ProcessTree accounts for all complicating factors. Each of the functions exposed by ProcessTree will return the most complete answer possible, regardless of how the processes in the ancestry hierarchy are started or managed, regardless of which OTP version is in use, and so on.

Put another way, ProcessTree is a "no-judgments zone". If you're following recommended guidelines for starting/running Elixir processes, ProcessTree will almost certainly meet your needs. But even in situations typically considered inadvisable or totally crazy, ProcessTree will do its very best to provide meaningful answers.

Summary

Functions

Starting with the calling process, recursively looks for a value for key in the process dictionaries of the calling process and its known ancestors.

Starting with the calling process, recursively looks for a value for key in the process dictionaries of the calling process and its known ancestors.

Returns a list of the known ancestors of pid, from "newest" to "oldest".

Returns the parent of pid, if the parent is known.

Functions

@spec get(term()) :: term()

Starting with the calling process, recursively looks for a value for key in the process dictionaries of the calling process and its known ancestors.

If a particular ancestor process has died, it looks up to the next ancestor, if possible.

If a non-nil value is found in the tree, the value is cached in the dictionary of the calling process.

Returns the first non-nil value found in the tree. If no value is found, returns nil.

@spec get(term(), [{:default, term()}]) :: term()

Starting with the calling process, recursively looks for a value for key in the process dictionaries of the calling process and its known ancestors.

If a particular ancestor process has died, it looks up to the next ancestor, if possible.

If a non-nil value is found in the tree, the value is cached in the dictionary of the calling process.

Returns the first non-nil value found in the tree.

If no value is found, the provided default value is cached in the dictionary of the calling process and then returned.

Default values might typically be read from the Application environment. For example:

default_value = Application.get_env(:my_app, :some_key)
value = ProcessTree.get(:some_key, default: default_value)
@spec known_ancestors(pid()) :: [pid() | atom()]

Returns a list of the known ancestors of pid, from "newest" to "oldest".

The set of ancestors that is "known" depends on factors including:

  • The OTP major version the code is running under. (OTP 25 introduced new functionality for tracking ancestors.)
  • Whether the process and its ancestors are running in a supervision tree
  • Whether ancestor processes are still alive
  • Whether the given process and its ancestors were started via raw spawn or were instead started as Tasks, Agents, GenServers or Supervisors

ProcessTree takes these factors and more into account and produces the most complete list of ancestors that is possible.

In the mainline case - running under a supervision tree, as recommended - known_ancestors/1 will return a list that contains, at minimum, all of the ancestor Supervisors in the tree as well as the ancestor of the initial/topmost Supervisor.

When running under OTP 25 and later, the list will also include all additional ancestors, up to and including the :init process (PID<0.0.0>), provided that the additional ancestor processes are still alive.

List items will be pids in all but the most unusual of circumstances. For example, if a GenServer is spawned by parent/grandparent GenServers that have registered names, and the parent GenServer dies, then the parent & grandparent may be represented in the list of the child's known ancestors using the their registered names - atoms, rather than pids. Precise behavior depends on OTP major version.

@spec parent(pid()) :: pid() | atom()

Returns the parent of pid, if the parent is known.

Returns :unknown if the parent is unknown.

Returns :undefined if pid represents the :init process (PID<0.0.0>).

If pid is part of a supervision tree, the parent will be known regardless of any other factors.

If the parent is known, the return value will be a pid in all but the most unusual of circumstances. See known_ancestors/1 for dicussion.