mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-17 04:34:25 +00:00
About 45 places in the integration harness picked a port by binding port 0, reading the number back, closing the socket and starting a server on that number later. Another process could take the port in between, which surfaced as "Address already in use" flakes. Earlier fixes retried around the race in single call sites without removing it. tests/integration/_support/ports.py now owns every port the harness picks ahead of a server. Each pytest process claims a 200-port block between 10000 and 32767 by taking an exclusive flock on a per-block lock file in the system temp directory, so parallel runs on one machine never share a block and the lock goes away with the process. The range is above the fixed service ports and below the Linux and macOS ephemeral ranges. Ports go out in order, are never reused in a run, skip anything an outside process already holds, and a process that exhausts its block claims the next free one. The three _free_port copies, worker_endpoints' port-0 binds, free_loopback_port, the owned proxy's lost-port-race retry and the owned Redis retry loop are gone. Tests that restart a server on the same port now allocate that port up front instead of reading it back from the first server. TQ012 in scripts/check_test_quality.py rejects a port-0 bind read back with getsockname anywhere under tests/integration.
61 lines
1.7 KiB
Python
61 lines
1.7 KiB
Python
import fcntl
|
|
import os
|
|
import socket
|
|
import tempfile
|
|
import threading
|
|
from collections.abc import Iterator
|
|
from pathlib import Path
|
|
from typing import Final
|
|
|
|
FIRST_PORT: Final = 10_000
|
|
LAST_PORT: Final = 32_767
|
|
BLOCK_SIZE: Final = 200
|
|
LOCK_DIRECTORY: Final = Path(tempfile.gettempdir()) / "litellm-integration-ports"
|
|
|
|
|
|
def _blocks() -> Iterator[range]:
|
|
return (range(start, start + BLOCK_SIZE) for start in range(FIRST_PORT, LAST_PORT + 2 - BLOCK_SIZE, BLOCK_SIZE))
|
|
|
|
|
|
def _claim_until_exit(block: range, directory: Path) -> bool:
|
|
descriptor: Final = os.open(directory / f"{block.start}.lock", os.O_RDWR | os.O_CREAT, 0o600)
|
|
try:
|
|
fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
except BlockingIOError:
|
|
os.close(descriptor)
|
|
return False
|
|
return True
|
|
|
|
|
|
def _bindable(port: int) -> bool:
|
|
with socket.socket() as probe:
|
|
try:
|
|
probe.bind(("127.0.0.1", port))
|
|
except OSError:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _free_ports(directory: Path) -> Iterator[int]:
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
for block in _blocks():
|
|
if _claim_until_exit(block, directory):
|
|
yield from filter(_bindable, block)
|
|
|
|
|
|
class PortAllocator:
|
|
def __init__(self, directory: Path) -> None:
|
|
self._directory: Final = directory
|
|
self._guard: Final = threading.Lock()
|
|
self._ports: Final = _free_ports(directory)
|
|
|
|
def allocate(self) -> int:
|
|
with self._guard:
|
|
port: Final = next(self._ports, None)
|
|
assert port is not None, (
|
|
f"Every integration port block in {FIRST_PORT}-{LAST_PORT} is claimed (locks in {self._directory})"
|
|
)
|
|
return port
|
|
|
|
|
|
allocate_port: Final = PortAllocator(LOCK_DIRECTORY).allocate
|