How are DB connections passed from the test process to the liveview process when using the ecto sandbox?


I noticed that if I have a LV test that uses the Ecto sandbox in manual mode, the LV process in the test has “magically” access to the sandboxed DB connection created by the test process. This is really amazing but I don’t understand how it works :slight_smile:

Simplified example:


defmodule MyAppWeb.LiveView do
  @moduledoc false

  import Ecto.Query

  use MyAppWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    connect_params = get_connect_params(socket)

    IO.inspect(connect_params, label: "connect_params")
    IO.inspect(self(), label: "I'm the LV")
    Repo.all(MyApp.User) |> length() |> IO.inspect(label: "users")

    {:ok, socket}

Test setup:

defmodule MyAppWeb.ConnCase do

  setup tags do
    Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, :manual)

    :ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)

    {:ok, conn: Phoenix.ConnTest.build_conn()}

The actual test:

defmodule MyAppWeb.LiveViewTest do 
    use MyAppWeb.ConnCase

    test "just to prove a point", %{conn: conn} do 

      IO.inspect(self(), label: "I'm the test process")

      {:ok, view, html} = live(conn, "/live_view")


1 I'm the test process: #PID<0.869.0>
2 connect_params: nil
3 I'm the LV: #PID<0.869.0>
4 entries: 20
5 connect_params: %{"_mounts" => 0}
6 I'm the LV: #PID<0.873.0>
7 entries: 20

Lines 2-4 come from the static LV mount, and lines 5-7 from the connected one. As you can see, the static mount happens in the same process as the test’s, while the connected mount happens in a different process. So far nothing strange. What I don’t understand is how the connected LV can see the test records inserted by the test process, even though I didn’t change the mode of the sandbox to shared or call Ecto.Adapters.SQL.Sandbox.allow. I would have expected the DB access from the connected LV process to fail with the error:
** (DBConnection.OwnershipError) cannot find ownership process for #PID<0.873.0>

The only explanation I can think of is that the sandboxed DB connection is somehow passed over to the LV process when this latter is spawned, by calling Ecto.Adapters.SQL.Sandbox.allow somewhere. But I couldn’t find where this happens in the code.

Can someone please clarify this for me?

The sandbox uses the mechanic described here to track the caller (aka the test process) for a LV: Task — Elixir v1.12.3

With that pid as key it can fetch the connection checked out by that process.

1 Like

Thanks @LostKobrakai! Very interesting. I guess it must be using the mechanism you describe, it’s just that I can’t find where this happens in the code.

It’s passed into the clientproxy phoenix_live_view/client_proxy.ex at master · phoenixframework/phoenix_live_view · GitHub and used here: phoenix_live_view/channel.ex at 112b1446b7962285d699bce6d187139de71b9ffa · phoenixframework/phoenix_live_view · GitHub

1 Like

Thanks. This explains it. So this is a generic mechanism, not specific to LV. Whenever the sandbox can track the caller using the $callers key in the process dictionary, it will try to use a DB connection from the caller, and no explicit allowances are needed. So this works whenever I start a new process using Task, Task.Supervisor, or Supervisor (I just tested with Task.async and it works).

I have to say I was completely in the dark about this one. The sandbox docs never mention it (Ecto.Adapters.SQL.Sandbox — Ecto SQL v3.8.1). I think they should. Reading the docs I would think that you need allowances or shared DB connections whenever you’re dealing with subprocesses, but clearly this is not necessary if you are using supervised tasks and that’s very relevant information.

I mean, how did you find about this mechanism @LostKobrakai ? Just poking around in the code, or did you read about it somewhere?

There we‘re some blogposts around the introduction to the functionality for Tasks, which is by now a lot of elixir versions back. While ecto sandbox piggibacks on this, it‘s afaik meant to be a much more general feature (hence it‘s inclusion in elixir core). But it should probably still be mentioned in the docs that processes supporting $caller keys might get automatic concurrent support with the sandbox.

1 Like