1

I have a test in my Phoenix app that is testing a Phoenix.PubSub subscriber that uses Genserver. The subscriber does some database work as part of its handle_info/2.

test "sending creating a referral code upon user registration" do
  start_supervised(MyApp.Accounts.Subscriber)
  user = insert(:user)

  Phoenix.PubSub.broadcast(MappApp.PubSub, "accounts", {:register, user})

  assert_eventually(Repo.exists?(ReferralCode))

  stop_supervised(MyApp.Accounts.Subscriber)
end

Running this test module by itself is fine. However when I run my entire test suite I get an error like so (the test still passes):

[error] GenServer MyApp.Accounts.Subscriber terminating
** (stop) exited in: DBConnection.Holder.checkout(#PID<0.970.0>, [log: #Function<9.124843621/1 in Ecto.Adapters.SQL.with_log/3>, cache_statement: "ecto_insert_referral_codes", timeout: 15000, pool_size: 10, pool: DBConnection.Ownership])
    ** (EXIT) shutdown: "owner #PID<0.969.0> exited"
    <stacktrace...>

This looks like it's an issue with the database connection still being open when the process is terminated so it doesn't die gracefully. But I'm not sure how to deal with this.

Any advice on how to prevent this error?

harryg
  • 23,311
  • 45
  • 125
  • 198
  • Do you by any chance run this test module with `async: true`? If so, that could be the culprit. – zwippie Nov 25 '20 at 16:10
  • I'm `use`ing the `ModelCase` module. Is this the default behaviour? – harryg Nov 25 '20 at 16:12
  • If you just have `use ModelCase` (and not `use ModelCase, async: true`) then the tests in the module will not run concurrently with tests in other modules, which is probably what you want. (Although I don't know what your ModelCase looks like) – zwippie Nov 25 '20 at 16:40
  • It's basically stock (now called `DataCase` in current Phoenix: https://github.com/phoenixframework/phoenix/blob/master/installer/templates/phx_ecto/data_case.ex). – harryg Nov 25 '20 at 16:45
  • I tried with and without `async: false` and the same error occurs. I think the `ModelCase` runs the test non-async – harryg Nov 25 '20 at 16:45

1 Answers1

1

I ran into this today. Any time you are doing database operations in separate child processes (e.g. database operations triggered inside a GenStage or GenServer et al), then you need to read the Sandbox adapter documentation carefully. There is an FAQ that deals with this error specifically, and the solution can be either to explicitly grant the Sandbox adapter access to the child process like this (from the docs):

test "gets results from GenServer" do
  {:ok, pid} = MyAppServer.start_link()
  Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), pid)
  assert MyAppServer.get_my_data_fast(timeout: 1000) == [...]
end

or you can enabled "shared" mode by altering your test setup to set the Sandbox mode:

  setup do
    :ok = Sandbox.checkout(Repo)
    Sandbox.mode(Repo, {:shared, self()})
  end

I had more luck with the latter, but be aware that if you are using some other adapter anywhere explicitly (e.g. to make raw database calls), then it might cause tests to fail (because it can no longer get a connection).

Everett
  • 8,746
  • 5
  • 35
  • 49
  • Hmmm, that's strange as the latter solution you gave is actually the default behaviour of my `ModelCase`, and it doesn't seem to solve the error I'm getting. I also discovered that my test still passes even when `start_supervised` is omitted, meaning that somehow the subscriber process is running. How can this be possible? – harryg Nov 26 '20 at 09:15
  • Are you running tests `async: false` ? – Everett Nov 26 '20 at 14:12
  • I've tried with and without. It has the same result – harryg Nov 26 '20 at 14:17
  • The thing I don't really understand is how the `Subscriber` Genserver seems to be started in the test run, even if I remove the `start_supervised` call. How is this possible? This is a pretty standard Phoenix app so the only other place it's started is in the Application file where I call `Supervisor.start_link` on the `children` – harryg Nov 26 '20 at 14:44
  • It seems like the Genserver is started by the test runner as a result of being in the `children` list in application.ex. I'm not sure or the right way to deal with this – harryg Nov 26 '20 at 16:23
  • Yeah: everything in application.ex is started during a test run — it makes sense if you think about it: the app has to start if you want to test it. – Everett Nov 27 '20 at 15:13
  • ah yeah, I guess that makes sense. But it does mean that all the processes declared in `children` are running and supervised for the run. For some tests I'm not testing the whole app, rather just some specific functionality. It's likely that a different test is causing the error in my question then – harryg Nov 27 '20 at 15:25
  • Yes, this is something I've often encountered as well. I've had to make sure I can override the `:name` parameter for each process so I can bring up another instance of the process for tests. – Everett Nov 28 '20 at 02:05