Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ecto 2.0 SQL Sandbox Error on tests

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?

like image 274
jeffskelton3 Avatar asked Jul 12 '16 17:07

jeffskelton3


2 Answers

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

like image 179
jeffskelton3 Avatar answered Nov 15 '22 11:11

jeffskelton3


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.

like image 42
Maciej Kaszubowski Avatar answered Nov 15 '22 11:11

Maciej Kaszubowski