litellm/tests/integration/_support/asgi.py
yuneng-jiang 229f5a9745
test(integration): fix four flaky integration tests (#49554)
Scope the Prisma migration lock test's advisory lock check to its own database, so a lock another xdist worker holds in its database no longer trips it

Keep in-process MCP peers' idle connections open for 30s instead of 1s, so the gateway's pooled HTTP client doesn't reuse a connection the peer is closing and hang the tool call

Delete database guardrails in the logging-only scope tests through DELETE /guardrails/{id} on the shared gateway instead of raw SQL, so a default-on guardrail stops applying to the next test on the same worker right away

Bound the wedged coordination Redis /customer/update check by the shortest Redis wait the proxy uses, like the burst check, so a slow runner can't fail it while a handler that waits on Redis still does
2026-11-02 09:23:17 -08:00

85 lines
3.3 KiB
Python

import asyncio
import logging
import queue
import socket
import threading
import time
from collections.abc import Callable, Iterator
from concurrent.futures import Future
from contextlib import contextmanager
from typing import Final
import uvicorn
from starlette.types import ASGIApp
@contextmanager
def asgi_server(
app: ASGIApp, *, before_stop: Callable[[], None] | None = None, port: int = 0, keepalive_seconds: int = 30
) -> Iterator[str]:
with socket.socket() as listener:
listener.bind(("127.0.0.1", port))
bound_port: Final = listener.getsockname()[1]
server: Final = uvicorn.Server(
uvicorn.Config(
app,
host="127.0.0.1",
port=bound_port,
lifespan="on",
log_level="warning",
timeout_keep_alive=keepalive_seconds,
timeout_graceful_shutdown=5,
)
)
errors: Final[queue.SimpleQueue[str]] = queue.SimpleQueue()
loop_ready: Final[Future[asyncio.AbstractEventLoop]] = Future()
def serve() -> None:
with asyncio.Runner() as runner:
loop_ready.set_result(runner.get_loop())
try:
runner.run(server.serve(sockets=[listener]))
except BaseException as error:
errors.put(type(error).__name__ + ": " + str(error))
if asyncio.all_tasks(runner.get_loop()):
errors.put("Owned ASGI loop retained unfinished tasks")
worker: Final = threading.Thread(target=serve)
class Capture(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
if record.thread == worker.ident and record.levelno >= logging.ERROR:
errors.put(self.format(record))
handler: Final = Capture()
logger: Final = logging.getLogger("uvicorn.error")
logger.addHandler(handler)
worker.start()
try:
deadline: Final = time.monotonic() + 8
while not server.started:
assert worker.is_alive() and time.monotonic() < deadline, "Owned ASGI peer failed readiness"
time.sleep(0.01)
yield f"http://127.0.0.1:{bound_port}"
finally:
if before_stop is not None:
before_stop()
server.should_exit = True
worker.join(timeout=8)
forced: Final = worker.is_alive()
if forced:
server.force_exit = True
loop: Final = loop_ready.result(timeout=1)
def cancel_owned() -> None:
for task in asyncio.all_tasks(loop):
task.cancel()
loop.call_soon_threadsafe(cancel_owned)
worker.join(timeout=3)
logger.removeHandler(handler)
assert not worker.is_alive(), "Owned ASGI peer survived forced cleanup"
assert not forced, "Owned ASGI peer required forced cleanup"
assert not server.server_state.tasks, "Owned ASGI peer retained request tasks"
assert not server.lifespan.error_occurred and not server.lifespan.shutdown_failed
assert errors.empty(), tuple(errors.get_nowait() for _ in range(errors.qsize()))