litellm/tests/integration/_support/redis_process.py
yucheng-berri 55726b91c8 perf(router): read cooldown state only for the request's candidate deployments
Routing used to read the cooldown key of every deployment on the router on
each request, so the Redis MGET and the in-memory scan grew with the size of
the whole model list. The cooldown reads on the routing path now take the ids
of the request's own deployments, and the proxy's routing prefetch arms the
cooldown keys of the route's team-aware candidates instead of every id.

The no-deployments error paths keep reading every deployment, so the
cooldown_list in the 429 message is unchanged.

Resolves LIT-9861
2026-10-20 10:12:27 -07:00

245 lines
9.4 KiB
Python

import os
import shutil
import signal
import subprocess
import threading
import time
import uuid
from collections.abc import Callable, Iterator, Mapping
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from queue import SimpleQueue
from typing import Final, TextIO
from pydantic import JsonValue, TypeAdapter
from redis import Redis
from redis.exceptions import ConnectionError as RedisConnectionError
from tests.integration._support.ports import allocate_port
_MONITOR_RECORD: Final = TypeAdapter(dict[str, JsonValue])
@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 owned_pid(self) -> int | None:
assert self.process is not None
if self.command[0] != "docker":
return self.process.pid
result: Final = subprocess.run(
["docker", "exec", "redis-cache", "cat", self.pid_file],
capture_output=True,
text=True,
timeout=2,
)
if result.returncode != 0:
return None
return int(result.stdout)
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"
process: Final = subprocess.Popen(
self.command, stdout=self.log, stderr=subprocess.STDOUT, start_new_session=True
)
self.process = process
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:
code: Final = process.poll()
if code is not None:
self.process = None
raise AssertionError(
f"Owned Redis exited with code {code} before readiness on {self.host}:{self.port}"
)
try:
if client.ping():
actual: Final = int(client.info("server")["process_id"])
expected: Final = self.owned_pid()
if actual == expected:
self.server_pid = actual
return
try:
process.wait(timeout=max(deadline - time.monotonic(), 0))
except subprocess.TimeoutExpired as error:
raise AssertionError("Redis readiness reached a different process") from error
self.process = None
raise AssertionError(f"A different Redis answered on {self.host}:{self.port} during startup")
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 paused(self) -> Iterator[None]:
self.signal(signal.SIGSTOP)
try:
yield
finally:
self.signal(signal.SIGCONT)
@contextmanager
def owned_redis(
directory: Path,
choose_port: Callable[[], int] = allocate_port,
inherited: Mapping[str, str] = os.environ,
) -> Iterator[OwnedRedis]:
binary: Final = shutil.which("redis-server")
if binary:
host: Final = "127.0.0.1"
prefix: Final = (binary,)
else:
host: Final = 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: Final = ("docker", "exec", "redis-cache", "redis-server")
output: Final = Path(inherited.get("INTEGRATION_RESULTS_DIR", str(directory)))
output.mkdir(parents=True, exist_ok=True)
directory.mkdir(parents=True, exist_ok=True)
with (output / f"owned-redis-{uuid.uuid4().hex}.log").open("w") as log:
port: Final = choose_port()
pid_file: Final = (
str(directory / f"owned-redis-{port}.pid") if binary else f"/tmp/integration-redis-{uuid.uuid4().hex}.pid"
)
command: Final = (
*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",
)
server: Final = OwnedRedis(host, port, command, log, pid_file)
try:
server.start()
yield server
finally:
if server.process is not None:
server.stop()
@dataclass(frozen=True, slots=True)
class RedisCommands:
queue: SimpleQueue[str]
def drain(self) -> tuple[tuple[str, ...], ...]:
"""Every command MONITOR saw since the last drain, split on spaces into its name and arguments"""
lines: Final = tuple(self.queue.get_nowait() for _ in range(self.queue.qsize()))
return tuple(tuple(line.split(" ")) for line in lines)
@contextmanager
def redis_commands(host: str, port: int) -> Iterator[RedisCommands]:
"""Records every command the Redis server at `host:port` executes while the block runs"""
commands: Final = RedisCommands(SimpleQueue())
started: Final = threading.Event()
armed: Final = threading.Event()
ready_marker: Final = f"monitor-ready-{uuid.uuid4().hex}"
stop_marker: Final = f"monitor-stop-{uuid.uuid4().hex}"
def capture() -> None:
# No socket timeout: a timeout raised inside `listen()` ends the generator, so capture would stop at the
# first quiet second. The stop marker sent on exit is what wakes and ends this read.
with Redis(host=host, port=port, decode_responses=True) as client:
with client.monitor() as monitor:
started.set()
for raw in monitor.listen():
record: Final = _MONITOR_RECORD.validate_python(raw)
command: Final = record.get("command")
if not isinstance(command, str):
continue
if stop_marker in command:
return
if ready_marker in command:
armed.set()
continue
commands.queue.put(command)
thread: Final = threading.Thread(target=capture, daemon=True)
thread.start()
try:
assert started.wait(timeout=5), "Redis MONITOR did not start"
with Redis(host=host, port=port, socket_timeout=1, decode_responses=True) as client:
client.set(ready_marker, "ready", ex=1)
assert armed.wait(timeout=5), "Redis MONITOR did not capture its readiness command"
yield commands
finally:
with Redis(host=host, port=port, socket_timeout=1, decode_responses=True) as client:
client.set(stop_marker, "stop", ex=1)
thread.join(timeout=5)
assert not thread.is_alive(), "Redis MONITOR did not reach its stop command"