Ecto 2.0 SQL Sandbox Error on tests

2020-02-11 02:50发布

问题:

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?

回答1:

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).



回答2:

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.



回答3:

This problem is finally solved by Elixir v1.8.0 and db_connection v2.0.4. See https://twitter.com/plataformatec/status/1091300824251285504 and https://elixirforum.com/t/problem-asynchronizing-ecto-calls/19796/8

If you use versions of Elixir and DBConnection newer than those mentioned above, the test should work out of the box without any errors.