diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -9,13 +9,14 @@ import logging import os import re +import socket import time import unittest from base64 import b64encode from decimal import ROUND_DOWN, Decimal from io import BytesIO from subprocess import CalledProcessError -from typing import Callable, Optional +from typing import Callable, Dict, Optional from . import coverage from .authproxy import AuthServiceProxy, JSONRPCException @@ -329,15 +330,70 @@ return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile) -def p2p_port(n): - assert n <= MAX_NODES - return PORT_MIN + n + \ - (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) - - -def rpc_port(n): - return PORT_MIN + PORT_RANGE + n + \ - (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) +def is_port_available(port: int) -> bool: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + return sock.connect_ex(('127.0.0.1', port)) != 0 + + +MAX_PORT_RETRY = 5 +P2P_PORTS_CACHE: Dict[int, int] = {} +LAST_P2P_PORT_USED = None + + +def p2p_port(n: int) -> int: + global P2P_PORTS_CACHE + global LAST_P2P_PORT_USED + if LAST_P2P_PORT_USED is None: + # We initialize this here rather than the module level because + # PortSeed.n is not defined at import time, but it is defined when this + # function is first called. + assert PortSeed.n is not None + LAST_P2P_PORT_USED = PORT_MIN + ( + MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) + + if n in P2P_PORTS_CACHE: + return P2P_PORTS_CACHE[n] + + ntries = 0 + try_port = LAST_P2P_PORT_USED + 1 + while not is_port_available(try_port): + if ntries >= MAX_PORT_RETRY: + raise RuntimeError( + f"Could not find available port after {MAX_PORT_RETRY} attempts.") + ntries += 1 + try_port += 1 + LAST_P2P_PORT_USED = try_port + P2P_PORTS_CACHE[n] = try_port + return try_port + + +RPC_PORTS_CACHE: Dict[int, int] = {} +LAST_RPC_PORT_USED = None + + +def rpc_port(n: int) -> int: + global RPC_PORTS_CACHE + global LAST_RPC_PORT_USED + if LAST_RPC_PORT_USED is None: + assert PortSeed.n is not None + LAST_RPC_PORT_USED = PORT_MIN + PORT_RANGE + ( + MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) + + if n in RPC_PORTS_CACHE: + return RPC_PORTS_CACHE[n] + + ntries = 0 + try_port = LAST_RPC_PORT_USED + 1 + while not is_port_available(try_port): + if ntries >= MAX_PORT_RETRY: + raise RuntimeError( + f"Could not find available port after {MAX_PORT_RETRY} attempts.") + ntries += 1 + try_port += 1 + + LAST_RPC_PORT_USED = try_port + RPC_PORTS_CACHE[n] = try_port + return try_port def rpc_url(datadir, chain, host, port):