litellm/tests/integration/_support/redis_process.py
yuneng-jiang 9b53ee878f
perf(ci): isolate and shard integration suites (#44146)
* perf(ci): distribute unit cases and reuse security sweep connections

* revert(ci): reject first speed trial after new failures

* perf(ci): avoid repeated test discovery and parsing

* perf(ci): isolate parallel security workers

* revert(ci): restore measured security execution isolation

* perf(tests): reuse source scans and deduplicate cache cleanup

* perf(ci): restore parallel security with safe log batching

* test(ci): isolate fixture state and schema comparisons

* test(ci): make schema and scheduler fixtures deterministic

* test(ci): drain queued logging before fixture teardown

* test(ci): preserve logging delivery without idle worker restarts

* test(ci): keep guardrail sinks available during parallel bursts

* test(ci): isolate full proxy lifecycle from gateway imports

* test(ci): preserve first failure responses and sweep evidence

* test(ci): restore documentation URL settings between cases

* test(ci): isolate open MCP tools in private gateways

* test(ci): group private MCP cases and handle exited workers

* test(ci): retain proxy exits and MCP failure details

* test(ci): repair merge interactions with main

Key the schema shard's full clone on artifact-name, since main removed the
unit-flag input. Give each owned Redis its own log file again. Hand the
realtime transcription tests main added a logging object whose dispatch
handlers are awaitable, so the per-test logging drain no longer waits on a
MagicMock task forever

* perf(ci): shard owned integration cases and close lifecycle leaks

* fix(ci): qualify shard execution and harden interrupted cleanup

* fix(ci): bound worker supervision and own fault traffic

* perf(ci): reuse unparametrized fixtures across bounded case groups

* fix(ci): preserve boot fault allowances and complete exit records

* test(ci): tolerate startup retries and retain readiness details

* perf(ci): balance integration shards by recorded duration

* fix(ci): load migration database helpers only for workers
2026-10-10 11:15:19 -07:00

116 lines
5.2 KiB
Python

import os
import shutil
import signal
import socket
import subprocess
import time
import uuid
from collections.abc import Iterator
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import Final, TextIO
from redis import Redis
from redis.exceptions import ConnectionError as RedisConnectionError
@dataclass
class OwnedRedis:
host: str
port: int
command: tuple[str, ...]
log: TextIO
pid_file: str
process: subprocess.Popen | None = None
server_pid: int | None = None
def serves(self) -> bool:
with Redis(host=self.host, port=self.port, socket_connect_timeout=0.2, socket_timeout=0.2) as client:
try:
return bool(client.ping())
except RedisConnectionError:
return False
def start(self) -> None:
assert self.process is None
assert not self.serves(), f"Another Redis already serves {self.host}:{self.port} before the owned one starts"
self.process = subprocess.Popen(self.command, stdout=self.log, stderr=subprocess.STDOUT, start_new_session=True)
deadline: Final = time.monotonic() + 8
with Redis(host=self.host, port=self.port, socket_connect_timeout=0.2, socket_timeout=0.2) as client:
while True:
assert self.process.poll() is None, "Owned Redis exited before readiness"
try:
if client.ping():
actual: Final = int(client.info("server")["process_id"])
expected: Final = self.process.pid if self.command[0] != "docker" else int(subprocess.check_output(["docker", "exec", "redis-cache", "cat", self.pid_file], timeout=2))
assert actual == expected, "Redis readiness reached a different process"
self.server_pid = actual
return
except RedisConnectionError:
pass
assert time.monotonic() < deadline, "Owned Redis readiness deadline exceeded"
time.sleep(0.05)
def stop(self) -> None:
assert self.process is not None
failure = None
forced = False
try:
if self.process.poll() is None:
with Redis(host=self.host, port=self.port, socket_connect_timeout=1, socket_timeout=1) as client:
assert int(client.info("server")["process_id"]) == self.server_pid, "Redis ownership changed before shutdown"
client.shutdown(nosave=True)
except Exception as error:
failure = error
finally:
try:
self.process.wait(timeout=5)
except subprocess.TimeoutExpired:
forced = True
self.signal(signal.SIGTERM)
try:
self.process.wait(timeout=5)
except subprocess.TimeoutExpired:
self.signal(signal.SIGKILL)
self.process.wait(timeout=3)
self.process = None
self.server_pid = None
assert not self.serves(), "Owned Redis still serves after shutdown"
assert failure is None and not forced, f"Owned Redis required shutdown recovery: {failure!r}"
def signal(self, action: signal.Signals) -> None:
assert self.process is not None
if self.command[0] != "docker":
self.process.send_signal(action)
return
pid: Final = int(subprocess.check_output(["docker", "exec", "redis-cache", "cat", self.pid_file], timeout=2))
command: Final = subprocess.check_output(["docker", "exec", "redis-cache", "cat", f"/proc/{pid}/cmdline"], timeout=2)
assert self.pid_file.encode() in command, "Redis process ownership changed"
subprocess.run(["docker", "exec", "redis-cache", "kill", f"-{int(action)}", str(pid)], check=True, timeout=2)
@contextmanager
def owned_redis(directory: Path) -> Iterator[OwnedRedis]:
binary: Final = shutil.which("redis-server")
with socket.socket() as reservation:
reservation.bind(("127.0.0.1", 0))
port: Final = reservation.getsockname()[1]
if binary:
host = "127.0.0.1"
prefix = (binary,)
else:
host = subprocess.check_output(["docker", "inspect", "--format", "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}", "redis-cache"], text=True).strip()
assert host, "CircleCI owned Redis container has no address"
prefix = ("docker", "exec", "redis-cache", "redis-server")
output: Final = Path(os.environ.get("INTEGRATION_RESULTS_DIR", str(directory)))
output.mkdir(parents=True, exist_ok=True)
with (output / f"owned-redis-{uuid.uuid4().hex}.log").open("w") as log:
pid_file: Final = str(directory / "owned-redis.pid") if binary else f"/tmp/integration-redis-{uuid.uuid4().hex}.pid"
server: Final = OwnedRedis(host, port, (*prefix, "--port", str(port), "--set-proc-title", "no", "--pidfile", pid_file, "--bind", "0.0.0.0" if not binary else "127.0.0.1", "--protected-mode", "no", "--save", "", "--appendonly", "no"), log, pid_file)
try:
server.start()
yield server
finally:
if server.process is not None:
server.stop()