I recently upgraded my phoenix project to Ecto 2.0.2. I have some code that is using Task.Supervisor.async_nolink
to make some updates to the db on its own thread. I am getting the following error when my tests run (only occurs on my tests)
[error] Postgrex.Protocol (#PID<0.XXX.0>) disconnected: **
(DBConnection.ConnectionError) owner #PID<0.XXX.0> exited while
client #PID<0.XXX.0> is still running with: shutdown
Now I think I understand whats happening: The Ecto Sandbox connection pool is being checked back in before the db transaction is complete. According to the docs (at least the way I read them) the way to get around that stuff is to use a shared connection pool: Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, {:shared, self()})
which I am doing. Unfortunately this is not working.
How do I set up my tests so that this error does not occur?
If anyone else encounters this, I got an answer back on this directly from the language author Jose Valim:
Yes, your understanding of the issue is correct. It is happening because the test process, the one who owns the connection, has exited but the Task is still using its connection. Using {:shared, self()} does not fix it because the test is still owning the connection, you are just sharing it implicitly.
The way to fix is to guarantee the Task has finished before the test exits. This can be done by calling Process.exit(task_pid, :kill). If you don't know the Task PID, you can call Task.Supervisor.which_children(NameOfYourTaskSupervisor) to return all PIDs which you then traverse and kill them. However, if you do this approach, the test cannot run concurrently (as you may kill tasks started by another test).
I had the same problem today and I think I've found a possible solution with allows the tests to run concurrently.
I'm using the technique described here: http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/ to replace Task.Supervisor while running tests.
Instead of:
Task.Supervisor.async_nolink(TaskSupervisor, fn -> (...) end)
I'm doing:
@task_supervisor Application.get_env(:app, :task_supervisor) || Task.Supervisor
# ...
@task_supervisor.async_nolink(TaskSupervisor, fn -> (...) end)
and then I define TestTaskSupervisor
defmodule TestTaskSupervisor do
def async_nolink(_, fun), do: fun.()
end
and add config :app, :task_supervisor, TestTaskSupervisor
in config/test.exs
.
This way, I'm sure that the task will run synchronously and finish before the test process.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With