From 8b18afe0ffb1fa37c3f50eaff76180b239a06d60 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Thu, 15 Oct 2026 11:55:04 +0200 Subject: [PATCH] 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. --- lib/galaxy_test/driver/driver_util.py | 9 ++++++--- test/unit/test_driver/test_driver_util.py | 11 +++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/galaxy_test/driver/driver_util.py b/lib/galaxy_test/driver/driver_util.py index 9f96167c2bb..8d96b44266d 100644 --- a/lib/galaxy_test/driver/driver_util.py +++ b/lib/galaxy_test/driver/driver_util.py @@ -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 diff --git a/test/unit/test_driver/test_driver_util.py b/test/unit/test_driver/test_driver_util.py index e8b87f6e7bc..b2823f156a0 100644 --- a/test/unit/test_driver/test_driver_util.py +++ b/test/unit/test_driver/test_driver_util.py @@ -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.