How to send Sandbox.allow for each dynamic supervisor (testing)

To share a connection between a test and a GenServer you need to ensure three things:

  1. somebody calls allow
  2. the GenServer shuts down if the test shuts down
  3. the GenServer shuts down if it’s trying to start up when the test has already shut down (this can happen when the DynamicSupervisor restarts the GenServer)

To handle all three, I’ve used code like this in the application’s Repo:

  def allow_if_sandbox(parent_pid, orphan_msg \\ :stop) do
    if sandbox_pool?() do
      monitor_parent(parent_pid, orphan_msg)

      # this addresses #1
      Ecto.Adapters.SQL.Sandbox.allow(__MODULE__, parent_pid, self())
    end
  end

  def sandbox_pool?() do
    config = Application.fetch_env!(:core, __MODULE__)
    Keyword.get(config, :pool) == Ecto.Adapters.SQL.Sandbox
  end

  defp monitor_parent(parent_pid, orphan_msg) do
    # this is part of addressing #2
    Process.monitor(parent_pid)

    if Process.alive?(parent_pid) do
      :ok
    else
      Logger.error("#{inspect(parent_pid)} down when booting #{inspect(self())}")
      # this addresses #3
      # the "throw" will work like an early "return"; see the GenServer docs
      throw(orphan_msg)
    end
  end

The Process.alive? check is not strictly required - monitoring an already-down process will put a :DOWN message into the queue - but typically the code that calls Repo.allow_if_sandbox will then immediately make a Repo.one call instead of processing messages.

Then in your GenServer, there are two changes needed:

  • receive parent_pid in the arguments to init/1 and call Repo.allow_if_sandbox(parent_pid)
  • add a handler for the message Process.monitor can trigger, like:
def handle_info({:DOWN, _ref, :process, pid, _reason}, state)
  # log that this happened, etc. Don't use Repo!
  :stop
end

Finally, you’ll need to actually pass the parent_pid - usually by adding it to the code that’s starting processes. Based on your example, something like:

Enum.map(@depends, fn item ->
  Map.merge(@new_soft_plugin, %{
    parent_pid: self(),
    name: item,
    depend_type: :hard,
    depends: List.delete(@depends, item)
  })
  |> MishkaInstaller.PluginState.push_call()
end)
2 Likes