** (DBConnection.OwnershipError) cannot find ownership process for #PID<0.XXX.0>

In order to update a last_seen field when a User leaves their UserChannel, I am starting a GenServer, on join, that monitors the User's UserChannel process, and makes the db call on reception of :DOWN signal.

I’m trying to test this.

test "joins succesfully", %{user_a: user} do
  assert {:ok, _, _socket} =
            UserSocket
            |> socket("user_socket:#{user.id}", %{user_id: user.id})
            |> subscribe_and_join(UserChannel, "user:#{user.id}")
end

Currently getting this error

[error] GenServer #PID<0.629.0> terminating
** (DBConnection.OwnershipError) cannot find ownership process for #PID<0.629.0>.

The module that tracks the user channel

defmodule ChataWeb.UserTracker.Process do
  use GenServer, restart: :transient

  alias Chata.Accounts

  def start_link(%{user_id: user_id, user_channel_pid: user_channel_pid}, opts \\ []) do
    init_args = %{user_id: user_id, user_channel_pid: user_channel_pid}
    GenServer.start_link(__MODULE__, init_args, opts)
  end

  def init(init_args) do
    Process.monitor(init_args.user_channel_pid)
    {:ok, init_args}
  end

  def handle_info({:DOWN, _ref, :process, _pid, _}, state) do
    %{user_id: user_id} = state
    Accounts.update_last_seen(user_id)
    {:stop, :normal, state}
  end
end

I suppose this is happening because the test process completes before the db call does.

Any ideas how to get this to work?

I haven’t tried it but feels like this doc page speaks about your issue: Ecto.Adapters.SQL.Sandbox — Ecto SQL v3.6.2

4 Likes

Two quick things to address this:

  1. Make sure that you are starting the sandbox with the new start_owner! function. Here is how it is used in Phoenix: Use Ecto.Adapters.SQL.Sandbox.start_owner!/2 in generators by wojtekmach · Pull Request #3856 · phoenixframework/phoenix · GitHub

  2. Use start_supervised! to start your GenServer

This will make it so ExUnit first terminates your GenServer before it terminates the process owning the DB connection.

If you can’t control how the channel monitor process is started, you can alternatively start them under a supervisor and use a code like this to wait until they are done: Use Ecto.Adapters.SQL.Sandbox.start_owner!/2 in generators by wojtekmach · Pull Request #3856 · phoenixframework/phoenix · GitHub

5 Likes

Thanks for the response

I couldn’t find any mention of a supervisor in the last link you sent. (dunno if it was a mispaste, but its the same as the link above)

So I came back to this few days ago and have been at it since, and still can’t figure it out.

I now put @chrismccord’s method into practice, which consists of a global GenServer that monitors channel processes when they join, and persists last_seen for that user when the channel goes {:DOWN.

This LastSeenTracker GenServer is started in Application.ex children on boot.

Here is the test

  describe "user joins own channel" do
    test "successfully", %{user_1: me, device_info: device_info} do
      token =
        ChataWeb.Token.sign(%{user_id: me.id, device_unique_id: device_info.device_unique_id})

      assert {:ok, _reply, socket} =
               UserSocket
               |> socket("user_socket:#{me.id}", %{
                 user: me,
                 phone_number: me.phone_number,
                 user_id: me.id
               })
               |> subscribe_and_join(UserChannel, "user:#{me.id}")
    end
  end

and here is the output

Finished in 1.9 seconds (1.7s async, 0.2s sync)
23 tests, 0 failures

Randomized with seed 260232
18:26:09.586 [error] GenServer ChataWeb.LastSeenTracker terminating
** (stop) exited in: DBConnection.Holder.checkout(#PID<0.601.0>, [log: #Function<14.16893214/1 in Ecto.Adapters.SQL.with_log/3>, cache_statement: "ecto_update_users", timeout: 15000, pool_size: 10, pool: DBConnection.Ownership])
    ** (EXIT) shutdown: "owner #PID<0.600.0> exited"
    (db_connection 2.4.0) lib/db_connection/holder.ex:95: DBConnection.Holder.checkout/3
    (db_connection 2.4.0) lib/db_connection/holder.ex:76: DBConnection.Holder.checkout/3
    (db_connection 2.4.0) lib/db_connection.ex:1082: DBConnection.checkout/3
    (db_connection 2.4.0) lib/db_connection.ex:1407: DBConnection.run/6

I have tried various permutations of manually a) leaving the channel b) closing the socket c) monitoring the channel + assert_receive its down signal, and a few other solutions but each one of these ended up making the test actually fail.

Any ideas on how to test a db write made asynchronously by another process?

You might need to trap exits in the test processes to prevent the test process from dying when the channel goes down. From there you can assert that the channel has indeed died and then you can check in the DB for the update.

1 Like