litellm/tests/integration/_support/process.py

92 lines
3.5 KiB
Python

import os
import socket
import signal
import subprocess
import sys
import time
import uuid
from collections.abc import Iterator, Mapping
from contextlib import contextmanager
from pathlib import Path
from typing import Final
import httpx
import psutil
from integration._support.client import Gateway
def in_group(process: psutil.Process, group: int) -> bool:
try:
return os.getpgid(process.pid) == group
except ProcessLookupError:
return False
def group_members(group: int) -> tuple[psutil.Process, ...]:
return tuple(process for process in psutil.process_iter() if in_group(process, group))
def signal_group(group: int, action: int) -> None:
try:
os.killpg(group, action)
except ProcessLookupError:
pass
@contextmanager
def owned_proxy(gateway: Gateway, directory: Path, overrides: Mapping[str, str]) -> Iterator[Gateway]:
with socket.socket() as reserve:
reserve.bind(("127.0.0.1", 0))
port: Final = reserve.getsockname()[1]
root: Final = Path(__file__).resolve().parents[3]
environment: Final = {
**os.environ,
"LITELLM_MASTER_KEY": gateway.key,
"LITELLM_SALT_KEY": os.environ.get("LITELLM_SALT_KEY", "sk-integration-salt"),
"STORE_MODEL_IN_DB": "True",
**overrides,
}
output: Final = Path(os.environ.get("INTEGRATION_RESULTS_DIR", str(directory)))
output.mkdir(parents=True, exist_ok=True)
with (output / f"owned-proxy-{uuid.uuid4().hex}.log").open("w") as log:
process: Final = subprocess.Popen(
[sys.executable, "-m", "integration._support.proxy", "--config", "tests/integration/proxy_config.yaml",
"--host", "127.0.0.1", "--port", str(port), "--num_workers", "1", "--telemetry", "False",
"--use_prisma_db_push", "--enforce_prisma_migration_check"],
cwd=root, env=environment, stdout=log, stderr=subprocess.STDOUT, start_new_session=True,
)
try:
with httpx.Client(base_url=f"http://127.0.0.1:{port}", timeout=15, trust_env=False) as client:
deadline: Final = time.monotonic() + 70
while True:
assert process.poll() is None, "Owned proxy exited before readiness"
try:
if client.get("/health/readiness", timeout=2).status_code == 200:
break
except httpx.TransportError:
pass
assert time.monotonic() < deadline, "Owned proxy readiness deadline exceeded"
time.sleep(0.1)
yield Gateway(client, gateway.key, gateway.upstream_url)
finally:
forced = False
if process.poll() is None:
process.terminate()
try:
process.wait(timeout=30)
except subprocess.TimeoutExpired:
forced = True
residual: Final = group_members(process.pid)
if residual:
signal_group(process.pid, signal.SIGTERM)
psutil.wait_procs(residual, timeout=5)
remaining: Final = group_members(process.pid)
if remaining:
forced = True
signal_group(process.pid, signal.SIGKILL)
psutil.wait_procs(remaining, timeout=3)
process.wait(timeout=3)
survivors: Final = group_members(process.pid)
assert not survivors, "Owned proxy child survived cleanup"
assert not forced, "Owned proxy required forced cleanup"