Listen on the reserved test server port right away

Linux lets several sockets with SO_REUSEADDR bind the same port as long
as none of them listens, so two xdist workers that drew the same random
port could both reserve it while their apps were building. Listening
right after the bind makes the reservation exclusive.
This commit is contained in:
mvdbeek
2026-10-15 11:55:04 +02:00
parent bf86788349
commit 8b18afe0ff
2 changed files with 17 additions and 3 deletions
+6 -3
View File
@@ -568,10 +568,12 @@ def attempt_ports(port=None, set_galaxy_web_port=True) -> str:
def reserve_port(host: str, port: str | None = None) -> socket.socket:
"""Bind the server socket up front and keep it until the server serves on it.
"""Bind and listen on the server socket now, and keep it until the server serves on it.
Releasing a probed port while the app builds would let another server (say, another
pytest-xdist worker's) take it, and the readiness check would then reach that server.
A port released while the app builds can be taken by another test server, whose
responses would then satisfy this server's readiness check. Listening makes the
reservation exclusive: Linux lets several SO_REUSEADDR sockets bind one port while
none of them listens.
"""
candidates = [int(port)] if port is not None else [random.randint(8000, 10000) for _ in range(9)]
for candidate in candidates:
@@ -579,6 +581,7 @@ def reserve_port(host: str, port: str | None = None) -> socket.socket:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sock.bind((host, candidate))
sock.listen()
except OSError:
sock.close()
continue
+11
View File
@@ -1,5 +1,6 @@
from unittest.mock import Mock
import pytest
from fastapi import FastAPI
from starlette.routing import Mount
@@ -27,6 +28,16 @@ def test_reserved_port_stays_taken_until_closed():
assert attempt_port(port) == port
def test_reserved_port_cannot_be_reserved_twice():
sock = reserve_port("127.0.0.1")
port = sock.getsockname()[1]
try:
with pytest.raises(Exception, match="bound to specified test server port"):
reserve_port("127.0.0.1", str(port))
finally:
sock.close()
def _gx_app(new_file_path: str) -> Mock:
# Spell out every setting caching_fast_app_factory reads: an unset Mock attribute is
# a truthy Mock, which sends the factory down the uncached path.