Skip to content

Commit f6c638e

Browse files
committed
test(db): observe advisory lock waiters
Add a Postgrex observer connection to assert the second test runner is actually waiting on the configured advisory lock. Replace the fixed no-message timing assertion with bounded polling of pg_locks.
1 parent 0dbd885 commit f6c638e

1 file changed

Lines changed: 92 additions & 3 deletions

File tree

test/mix/tasks/test_database_lock_test.exs

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,32 @@ defmodule CodexPooler.MixTasks.TestDatabaseLockTest do
44
alias CodexPooler.MixTasks.TestDatabaseLock
55
alias CodexPooler.Repo
66

7+
@lock_namespace "codex_pooler_test_runner"
8+
@lock_database "postgres"
9+
@lock_wait_attempts 50
10+
@connection_keys [
11+
:after_connect,
12+
:connect_timeout,
13+
:hostname,
14+
:password,
15+
:parameters,
16+
:port,
17+
:socket_dir,
18+
:socket_options,
19+
:ssl,
20+
:ssl_opts,
21+
:timeout,
22+
:types,
23+
:url,
24+
:username
25+
]
26+
727
test "serializes concurrent callers for the configured test database" do
828
parent = self()
929
repo_config = Keyword.put(Repo.config(), :database, "codex_pooler_test_lock_regression")
30+
observer = start_lock_observer!(repo_config)
31+
32+
on_exit(fn -> stop_lock_observer(observer) end)
1033

1134
first =
1235
Task.async(fn ->
@@ -25,21 +48,87 @@ defmodule CodexPooler.MixTasks.TestDatabaseLockTest do
2548

2649
second =
2750
Task.async(fn ->
28-
send(parent, :second_waiting)
51+
send(parent, :second_entering_lock)
2952

3053
TestDatabaseLock.with_lock!(repo_config, fn ->
3154
send(parent, :second_locked)
3255
:second_released
3356
end)
3457
end)
3558

36-
assert_receive :second_waiting
37-
refute_receive :second_locked, 100
59+
assert_receive :second_entering_lock, 5_000
60+
assert_advisory_lock_waiter!(observer, repo_config)
3861

3962
send(first.pid, :release_first)
4063

4164
assert Task.await(first) == :first_released
4265
assert Task.await(second) == :second_released
4366
assert_receive :second_locked
4467
end
68+
69+
defp assert_advisory_lock_waiter!(conn, repo_config, attempts \\ @lock_wait_attempts)
70+
71+
defp assert_advisory_lock_waiter!(conn, repo_config, attempts) when attempts > 0 do
72+
if advisory_lock_waiter?(conn, repo_config) do
73+
:ok
74+
else
75+
receive do
76+
after
77+
20 -> assert_advisory_lock_waiter!(conn, repo_config, attempts - 1)
78+
end
79+
end
80+
end
81+
82+
defp assert_advisory_lock_waiter!(_conn, repo_config, 0) do
83+
flunk(
84+
"expected a PostgreSQL advisory lock waiter for #{Keyword.fetch!(repo_config, :database)}"
85+
)
86+
end
87+
88+
defp advisory_lock_waiter?(conn, repo_config) do
89+
%{rows: [[waiting?]]} =
90+
Postgrex.query!(
91+
conn,
92+
"""
93+
SELECT EXISTS (
94+
SELECT 1
95+
FROM pg_locks
96+
WHERE locktype = 'advisory'
97+
AND classid = hashtext($1)::oid
98+
AND objid = hashtext($2)::oid
99+
AND NOT granted
100+
)
101+
""",
102+
[@lock_namespace, Keyword.fetch!(repo_config, :database)]
103+
)
104+
105+
waiting?
106+
end
107+
108+
defp start_lock_observer!(repo_config) do
109+
{:ok, _started} = Application.ensure_all_started(:postgrex)
110+
111+
repo_config
112+
|> Keyword.take(@connection_keys)
113+
|> Keyword.put(:database, lock_database(repo_config))
114+
|> Postgrex.start_link()
115+
|> case do
116+
{:ok, conn} -> conn
117+
{:error, _reason} -> raise "failed to start test database lock observer"
118+
end
119+
end
120+
121+
defp lock_database(repo_config) do
122+
Keyword.get(repo_config, :maintenance_database) || @lock_database
123+
end
124+
125+
defp stop_lock_observer(conn) do
126+
if Process.alive?(conn) do
127+
GenServer.stop(conn)
128+
end
129+
130+
:ok
131+
catch
132+
:exit, _reason -> :ok
133+
end
45134
end

0 commit comments

Comments
 (0)