Prevent random port collisions between controllers (#191)

This happens from time to time on the CI and is pretty annoying
This commit is contained in:
2023-04-04 22:01:20 +02:00
committed by GitHub
parent 136a7923c0
commit 418b526033
6 changed files with 59 additions and 16 deletions

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import dataclasses
import multiprocessing
import os
from pathlib import Path
import shutil
@ -9,7 +10,18 @@ import subprocess
import tempfile
import textwrap
import time
from typing import IO, Any, Callable, Dict, List, Optional, Set, Tuple, Type
from typing import (
IO,
Any,
Callable,
Dict,
List,
MutableMapping,
Optional,
Set,
Tuple,
Type,
)
import irctest
@ -58,9 +70,43 @@ class _BaseController:
supported_sasl_mechanisms: Set[str]
proc: Optional[subprocess.Popen]
_used_ports: Set[Tuple[str, int]]
"""``(hostname, port))`` used by this controller."""
# the following need to be shared between processes in case we are running in
# parallel (with pytest-xdist)
# The dicts are used as a set of (hostname, port), because _manager.set() doesn't
# exist.
_manager = multiprocessing.Manager()
_port_lock = _manager.Lock()
"""Lock for access to ``_all_used_ports`` and ``_available_ports``."""
_all_used_ports: MutableMapping[Tuple[str, int], None] = _manager.dict()
"""``(hostname, port)`` used by all controllers."""
_available_ports: MutableMapping[Tuple[str, int], None] = _manager.dict()
"""``(hostname, port)`` available to any controller."""
def __init__(self, test_config: TestCaseControllerConfig):
self.test_config = test_config
self.proc = None
self._used_ports = set()
def get_hostname_and_port(self) -> Tuple[str, int]:
with self._port_lock:
try:
# try to get a known available port
((hostname, port), _) = self._available_ports.popitem()
except KeyError:
# if there aren't any, iterate while we get a fresh one.
while True:
(hostname, port) = find_hostname_and_port()
if (hostname, port) not in self._all_used_ports:
# double-checking in self._used_ports to prevent collisions
# between controllers starting at the same time.
break
# Make this port unavailable to other processes
self._all_used_ports[(hostname, port)] = None
return (hostname, port)
def check_is_alive(self) -> None:
assert self.proc
@ -84,6 +130,11 @@ class _BaseController:
if self.proc:
self.kill_proc()
# move this controller's ports from _all_used_ports to _available_ports
for hostname, port in self._used_ports:
del self._all_used_ports[(hostname, port)]
self._available_ports[(hostname, port)] = None
class DirectoryBasedController(_BaseController):
"""Helper for controllers whose software configuration is based on an
@ -202,9 +253,6 @@ class BaseServerController(_BaseController):
super().__init__(*args, **kwargs)
self.faketime_enabled = False
def get_hostname_and_port(self) -> Tuple[str, int]:
return find_hostname_and_port()
def run(
self,
hostname: str,

View File

@ -8,7 +8,6 @@ from irctest.basecontrollers import (
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """
global {{
@ -125,8 +124,8 @@ class BahamutController(BaseServerController, DirectoryBasedController):
self.port = port
self.hostname = hostname
self.create_config()
(unused_hostname, unused_port) = find_hostname_and_port()
(services_hostname, services_port) = find_hostname_and_port()
(unused_hostname, unused_port) = self.get_hostname_and_port()
(services_hostname, services_port) = self.get_hostname_and_port()
password_field = "passwd {};".format(password) if password else ""

View File

@ -7,7 +7,6 @@ from irctest.basecontrollers import (
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_SSL_CONFIG = """
ssl_private_key = "{key_path}";
@ -53,7 +52,7 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
self.port = port
self.hostname = hostname
self.create_config()
(services_hostname, services_port) = find_hostname_and_port()
(services_hostname, services_port) = self.get_hostname_and_port()
password_field = 'password = "{}";'.format(password) if password else ""
if ssl:
self.gen_ssl()

View File

@ -8,7 +8,6 @@ from irctest.basecontrollers import (
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """
# Clients:
@ -138,7 +137,7 @@ class InspircdController(BaseServerController, DirectoryBasedController):
self.port = port
self.hostname = hostname
self.create_config()
(services_hostname, services_port) = find_hostname_and_port()
(services_hostname, services_port) = self.get_hostname_and_port()
password_field = 'password="{}"'.format(password) if password else ""

View File

@ -7,7 +7,6 @@ from irctest.basecontrollers import (
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """
[Global]
@ -66,7 +65,7 @@ class NgircdController(BaseServerController, DirectoryBasedController):
self.port = port
self.hostname = hostname
self.create_config()
(unused_hostname, unused_port) = find_hostname_and_port()
(unused_hostname, unused_port) = self.get_hostname_and_port()
password_field = "Password = {}".format(password) if password else ""

View File

@ -12,7 +12,6 @@ from irctest.basecontrollers import (
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """
include "modules.default.conf";
@ -228,8 +227,8 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
password_field = 'password "{}";'.format(password) if password else ""
with _STARTSTOP_LOCK():
(services_hostname, services_port) = find_hostname_and_port()
(unused_hostname, unused_port) = find_hostname_and_port()
(services_hostname, services_port) = self.get_hostname_and_port()
(unused_hostname, unused_port) = self.get_hostname_and_port()
self.gen_ssl()
if ssl: