Skip to content

Commit bd5de88

Browse files
mkleczeksteve-chavez
authored andcommitted
test(io): add test_so_reuseport_zero_downtime_handover
1 parent 8d5b101 commit bd5de88

2 files changed

Lines changed: 71 additions & 3 deletions

File tree

test/io/postgrest.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def run(
8585
stdin=None,
8686
env=None,
8787
port=None,
88+
admin_port=None,
8889
host=None,
8990
wait_for_readiness=True,
9091
wait_max_seconds=1,
@@ -113,7 +114,7 @@ def run(
113114
env["PGRST_SERVER_UNIX_SOCKET"] = str(socketfile)
114115
baseurl = "http+unix://" + urllib.parse.quote_plus(str(socketfile))
115116

116-
adminport = freeport(used_ports=[port])
117+
adminport = freeport(used_ports=[port]) if admin_port is None else admin_port
117118
env["PGRST_ADMIN_SERVER_PORT"] = str(adminport)
118119
adminhost = f"[{host}]" if host and is_ipv6(host) else localhost
119120
adminurl = f"http://{adminhost}:{adminport}"
@@ -176,10 +177,10 @@ def freeport(used_ports=None):
176177
return port
177178

178179

179-
def wait_until_exit(postgrest):
180+
def wait_until_exit(postgrest, timeout=1):
180181
"Wait for PostgREST to exit, or times out"
181182
try:
182-
return postgrest.process.wait(timeout=1)
183+
return postgrest.process.wait(timeout=timeout)
183184
except subprocess.TimeoutExpired:
184185
raise PostgrestTimedOut()
185186

test/io/test_io.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
155222
def 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

Comments
 (0)