@@ -152,6 +152,73 @@ def test_random_port_bound(defaultenv):
152152 assert True # liveness check is done by run(), so we just need to check that it doesn't fail
153153
154154
155+ @pytest .mark .xfail (reason = "PostgREST should not start on a used port" , strict = True )
156+ def test_so_reuseport_zero_downtime_handover (defaultenv ):
157+ "A second PostgREST instance should take over on the same main/admin ports without request failures."
158+
159+ # set host to _all_ addresses to force port conflict without SO_REUSEPORT
160+ # setting to localhost (which is the default)
161+ # might allow running multiple instances on the same port
162+ # as the name might be resolved to many IP addresses
163+ host = "0.0.0.0"
164+ port = freeport ()
165+ admin_port = freeport (used_ports = [port ])
166+ failures = []
167+ # mutable location shared between threads
168+ keep_running = {"value" : True }
169+
170+ # 1. Start first PostgREST instance
171+ # 2. Start a "client" thread issuing requests in a loop
172+ # remembering all received errors
173+ # 3. Start second PostgREST instance on the same port as the first one
174+ # 4. Wait a little and terminate the first instance
175+ #
176+ # We expect the client does not get any errors after stopping the first instance
177+ # and seamlessly migrate to the second instance.
178+ #
179+ # 5. Stop client thread
180+ # 6. Stop second PostgREST instance
181+ # 7. Verify client did not get any errors
182+ with run (
183+ env = {** defaultenv },
184+ port = port ,
185+ host = host ,
186+ admin_port = admin_port ,
187+ ) as first :
188+
189+ def continuously_request ():
190+ while keep_running ["value" ]:
191+ try :
192+ response = first .session .get ("/projects" , timeout = 1 )
193+ assert response .status_code == 200
194+ except Exception as exc :
195+ failures .append (exc )
196+ break
197+ time .sleep (0.2 )
198+
199+ requester = Thread (target = continuously_request )
200+ requester .start ()
201+
202+ try :
203+ time .sleep (1 )
204+ with run (
205+ env = {** defaultenv },
206+ port = port ,
207+ host = host ,
208+ admin_port = admin_port ,
209+ ):
210+ time .sleep (1 )
211+ first .process .terminate ()
212+ wait_until_exit (first , 2 )
213+
214+ time .sleep (1 )
215+ finally :
216+ keep_running ["value" ] = False
217+ requester .join ()
218+
219+ assert failures == []
220+
221+
155222def test_app_settings_reload (tmp_path , defaultenv ):
156223 "App settings should be reloaded from file when PostgREST is sent SIGUSR2."
157224 config = (CONFIGSDIR / "sigusr2-settings.config" ).read_text ()
0 commit comments