there is a simple GenServer
which performs a simple Ecto query within async call:
defmodule App.Notifications.Manager do
def send(user, event) do
IO.write "manager pid "
IO.inspect self()
GenServer.cast(__MODULE__, {:email, user, event})
end
def handle_cast({:email, user, event}, state) do
IO.write "manager server pid "
IO.inspect self()
App.Repo.all(App.User)
{:noreply, state}
end
end
and relevant test that looks like that:
defmodule App.Notifications.EventManagerTest do
use App.ModelCase
test "send a message", context do
IO.puts "start test"
IO.inspect self()
App.Notifications.Manager.send(context.user, context.event)
IO.puts "finish test"
end
end
the tests itself are performed in shared mode
defmodule App.ModelCase do
#...
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(App.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(App.Repo, {:shared, self()})
end
:ok
end
end
Now, mix test
is leading to the race condition:
..................start test
#PID<0.499.0>
manager pid #PID<0.499.0>
finish test
.manager server pid #PID<0.283.0>
12:31:04.787 [error] GenServer App.Notifications.Manager terminating
** (stop) exited in: GenServer.call(#PID<0.500.0>, {:checkout, #Reference<0.0.1.1627>, true, 15000}, 5000)
** (EXIT) shutdown: "owner #PID<0.499.0> exited with: shutdown"
(db_connection) lib/db_connection/ownership/proxy.ex:32: DBConnection.Ownership.Proxy.checkout/2
(db_connection) lib/db_connection.ex:701: DBConnection.checkout/2
(db_connection) lib/db_connection.ex:608: DBConnection.run/3
(db_connection) lib/db_connection.ex:449: DBConnection.prepare_execute/4
(ecto) lib/ecto/adapters/sql.ex:224: Ecto.Adapters.SQL.sql_call/6
(ecto) lib/ecto/adapters/sql.ex:396: Ecto.Adapters.SQL.execute_and_cache/7
(ecto) lib/ecto/repo/queryable.ex:127: Ecto.Repo.Queryable.execute/5
(ecto) lib/ecto/repo/queryable.ex:40: Ecto.Repo.Queryable.all/4
(ave88) lib/ave88/notifications/manager.ex:46: Ave88.Notifications.Manager.handle_cast/2
(stdlib) gen_server.erl:615: :gen_server.try_dispatch/4
(stdlib) gen_server.erl:681: :gen_server.handle_msg/5
(stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
..............................................
Finished in 2.9 seconds (1.1s on load, 1.7s on tests)
65 tests, 0 failures
As you can see handle_cast
occur after test has finished. App uses latest versions of db_connection
- 1.0.0-rc.1
and ecto
- 2.0.0-rc.6
.
One way to do this would be to add a handle_call
that would return just a dummy value, and then invoke it after the cast
from your tests to make sure all the queued up casts are executed by the GenServer
. This works because a GenServer
handles all the casts/calls in the order they're received in. If you make 10 long running cast
s and then 1 call
, the call
would return after the 10 cast
s are finished executing one by one.
In App.Notifications.Manager
, add:
def handle_call(:ping, _from, state) do
{:reply, :pong, state}
end
Then, in your test, after
App.Notifications.Manager.send(context.user, context.event)
add
GenServer.call(App.Notifications.Manager, :ping)