如何在使用数据库查询测试 handle_cast 期间避免竞争条件?
How to avoid race condition during testing handle_cast with database query?
有一个简单的 GenServer
在异步调用中执行简单的 Ecto 查询:
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
和类似的相关测试:
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
测试本身在共享模式下执行
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
现在,mix test
导致竞争条件:
..................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
如您所见,handle_cast
在测试完成后发生。应用程序使用最新版本的 db_connection
- 1.0.0-rc.1
和 ecto
- 2.0.0-rc.6
.
一种方法是添加一个 handle_call
,它 return 只是一个虚拟值,然后在测试 cast
之后调用它,以确保所有排队的演员表由 GenServer
执行。这是有效的,因为 GenServer
按照收到的顺序处理所有 casts/calls。如果你制作 10 长 运行 cast
s 然后 1 call
,call
会在10个cast
一个一个执行完后return
在App.Notifications.Manager
中添加:
def handle_call(:ping, _from, state) do
{:reply, :pong, state}
end
然后,在你的测试中,
App.Notifications.Manager.send(context.user, context.event)
添加
GenServer.call(App.Notifications.Manager, :ping)
有一个简单的 GenServer
在异步调用中执行简单的 Ecto 查询:
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
和类似的相关测试:
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
测试本身在共享模式下执行
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
现在,mix test
导致竞争条件:
..................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
如您所见,handle_cast
在测试完成后发生。应用程序使用最新版本的 db_connection
- 1.0.0-rc.1
和 ecto
- 2.0.0-rc.6
.
一种方法是添加一个 handle_call
,它 return 只是一个虚拟值,然后在测试 cast
之后调用它,以确保所有排队的演员表由 GenServer
执行。这是有效的,因为 GenServer
按照收到的顺序处理所有 casts/calls。如果你制作 10 长 运行 cast
s 然后 1 call
,call
会在10个cast
一个一个执行完后return
在App.Notifications.Manager
中添加:
def handle_call(:ping, _from, state) do
{:reply, :pong, state}
end
然后,在你的测试中,
App.Notifications.Manager.send(context.user, context.event)
添加
GenServer.call(App.Notifications.Manager, :ping)