home

Elixir: Which Modules Are Using My Module

24 Jun 2018

In Elixir, getting a list of modules that use a specific module is trickier than you'd think. It's caused me, and others, some grief.

The naive approach is to mark the module (say, with a special function):

defmacro __using__(_opts) do
  quote location: :keep do
    def __my_module(), do: :ok
  end
end

Then iterate through the modules with something like :code.all_loaded/0 and look for our special function. But in Elixir, modules are lazily loaded and, there's a good chance that at the time you call :code.all_loaded/0 the modules that you're looking for haven't been loaded it.

Idiomatically, I haven't found a great solution to this problem. The best seems to be to explicit pass the list of modules, either via configuration or as an argument to to a process. But this highlights a major pain point in Elixir: requiring a deep hierarchy of dependencies to be configured at the root app. Dave Thomas spoke about this problem, as well as others, at EMPEX.

At one point, we got pretty desperate to make this type of auto discovery work, and had our __using__ write the caller's name to a DETS file, which could then be read at runtime. It worked, but it was ugly.

More recently, I came up with a <airquote>better</airquote> solution.

First, we'll use :application.loaded_applications/0 to get all the applications, then :application.get_key/2 to get all the application's modules (which gives us all the names, even if they aren't loaded):

applications = :application.loaded_applications()
modules = Enum.reduce(applications, [], fn {app, _desc, _version}, acc ->
  {:ok, modules} = :application.get_key(app, :modules)
  # TODO
end)

Now, if we wanted to, we could just iterate through modules and call Code.ensure_loaded and look for our special function. This essentially circumvents Elixir's lazily loading:

Enum.reduce(modules, acc, fn module, acc ->
  Code.ensure_loaded(module)
  # Does this module export our special __prepared_statements/0 function?
  case Keyword.get(module.__info__(:functions), :__prepared_statements) do
    nil -> acc
    0 -> [module | acc] # yes, it does, add it to the list
  end
end)

It would be nice if we could make this more surgical and minimize the amount of modules we have to force load. This part is a little ugly, but it isn't too bad. What I do is add a special module to the app/libary.. Only if this module is present are the modules loaded and probed.

applications = :application.loaded_applications()
state = Enum.reduce(applications, [], fn {app, _desc, _version}, acc ->
  {:ok, modules} = :application.get_key(app, :modules)

  auto_registry? = Enum.any?(modules, fn m ->
    String.starts_with?(to_string(m), "Elixir.Special.Registry.Auto.")
  end)

  case auto_registry? do
    false -> acc
    true -> scan_app(modules, acc) # same code as above
  end
end)

To make this work, you just need to drop an empty module in the app/library you want scanned. I use a macro to create this:

defmacro auto_discover() do
  module = Module.concat(Special.Registry.Auto, __CALLER__.module)
  quote do
    defmodule unquote(module) do
    end
  end
end

Since I've seen this question asked a number of times, I hope this post will prove helpful. I also hope that future versions of Elixir address this problem.