Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/adapters/detector_readiness.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ def http_probe(client: HttpClientLike, url: str) -> Callable[[], Awaitable[bool]
"""

async def probe() -> bool:
return (await client.get(url)).status_code == 200
status = (await client.get(url)).status_code
if status != 200:
# A redirect, a rejection and a service still starting are all "not
# ready" but call for different responses, so the status is kept.
logger.info("detector_probe_status", extra={"context": {"status": status}})
return status == 200

return probe

Expand Down
6 changes: 5 additions & 1 deletion app/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,11 @@ async def main() -> None:

# The first request also activates the detector, since it scales to zero,
# so probing both starts it and establishes when it is usable.
http = httpx.AsyncClient(timeout=DETECTOR_PROBE_TIMEOUT_S)
# follow_redirects, because the detector's ingress refuses plain HTTP and
# answers with a redirect to HTTPS. A client that does not follow it sees a
# non-200 forever, and the redirect alone does not start an app scaled to
# zero, so the endpoint would never become reachable.
http = httpx.AsyncClient(timeout=DETECTOR_PROBE_TIMEOUT_S, follow_redirects=True)
probe = http_probe(http, f"{settings.llm_guardrail_base_url.rstrip('/')}/models")

async def detector_ready() -> bool:
Expand Down
36 changes: 36 additions & 0 deletions tests/unit/test_detector_readiness.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,39 @@ async def probe() -> bool:

assert "detector_not_ready_yet" in events
assert "detector_ready" in events


@pytest.mark.asyncio
async def test_a_probe_reports_the_status_it_received():
""" "Not ready" covers a redirect, a rejection and a service still starting,
which need different responses. The status is recorded so the logs say
which one it was."""
import logging

from app.adapters.detector_readiness import http_probe

events: list[dict] = []

class _Collect(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
events.append(getattr(record, "context", {}))

class _Response:
status_code = 307

class _Client:
async def get(self, url: str):
return _Response()

logger = logging.getLogger("screen")
handler = _Collect()
logger.addHandler(handler)
previous = logger.level
logger.setLevel(logging.INFO)
try:
assert not await http_probe(_Client(), "http://detector/v1/models")()
finally:
logger.removeHandler(handler)
logger.setLevel(previous)

assert any(c.get("status") == 307 for c in events)
Loading