Alex MartsinovichSoftware Engineer |
||||
Home | Posts | Github |
5-minute guide to Elixir caller tracking
Caller tracking has been part of Elixir since 2018, and yet it remains a relatively obscure mechanism. Let's fix this with a 5-minute guide.
What is caller tracking
Asynchronous tests are the best. They are possible because in many cases we can trace what is happening in the application all the way back to the test. We say that a test process "owns" things, and other processes may be "allowed" to access those things.
Caller tracking is a convention that allows us to know which processes are
related to each other and automatically allow them to access things owned by a
test. Examples of such things are
Ecto.Sandbox
transactions and
Mox
mocks.
In some cases, caller tracking is handled automatically and you don't need to do
anything, such as if you use the Task
module. But in a lot of other cases you need
to take care of caller tracking yourself. The most common example is a
GenServer.
Genserver with caller tracking
Whenever you start a GenServer, equip it with a :"$callers"
value in the
process dictionary. It should include whatever the calling process had in its
process dictionary under the same key and the caller PID. Remember:
start_link
is executed in the caller, while init
runs in the server!
defmodule MyGenserver do
use GenServer
def start_link(args) do
callers = Process.get(:"$callers") || []
new_callers = [self() | callers]
GenServer.start_link(__MODULE__, {new_callers, args})
end
@impl GenServer
def init({callers, _args}) do
Process.put(:"$callers", callers)
{:ok, nil}
end
end
Now whenever you start this GenServer in your test, it will be able to access Ecto transactions without any additional setup.

Further reading
Ancestor and Caller Tracking bit in Task module documentation