@@ -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
45134end
0 commit comments