Coverage for server / utilities / readiness.py: 100%
22 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
1"""
2Dependency readiness checks for the readiness probe.
4Separated from the route so the logic can be unit-tested without a live
5MongoDB, and so the route stays a thin wrapper like every other route here.
7WHY THIS IS NOT `/health` OR `/v1/health`. Both of those answer a static
8literal, which is correct for a LIVENESS probe: "the process is up and
9serving". The deploy workflow points `HEALTH_ENDPOINT` at `/v1/health` and
10rolls back when it fails, so making it depend on MongoDB would mean a transient
11database blip during a deploy could trigger an automatic rollback that would
12not have happened before. Readiness is a different question — "can this process
13actually serve requests" — and it gets its own endpoint.
15WHAT IT MUST NOT SAY. The probe is unauthenticated, so the body names each
16dependency and whether it is up, and nothing else: no connection strings, no
17hostnames, no driver messages, no stack traces. An exception from the driver
18often carries the host and port it failed to reach, which is exactly what an
19anonymous caller must not be handed.
21Developer: Allan Ninal
22Date: 2026-09-21
23"""
25import asyncio
26import logging
28logger = logging.getLogger(__name__)
30# A readiness probe must answer quickly or it is useless to whatever polls it.
31# Without a bound, a wedged connection makes the probe hang instead of
32# reporting "not ready", which is the failure it exists to surface.
33PING_TIMEOUT_SECONDS = 2.0
35UP = "up"
36DOWN = "down"
39async def _ping_mongo(mongo_client) -> str:
40 """Ping MongoDB, returning UP or DOWN. Never raises, never leaks detail."""
41 if mongo_client is None:
42 return DOWN
43 try:
44 await asyncio.wait_for(
45 mongo_client.admin.command("ping"), timeout=PING_TIMEOUT_SECONDS
46 )
47 return UP
48 except asyncio.TimeoutError:
49 # Logged, not returned: the reason belongs in our logs, not in an
50 # anonymous response body.
51 logger.warning(
52 "Readiness: MongoDB ping timed out after %ss", PING_TIMEOUT_SECONDS
53 )
54 return DOWN
55 except Exception:
56 logger.warning("Readiness: MongoDB ping failed", exc_info=True)
57 return DOWN
60async def check_dependencies(mongo_client) -> tuple[bool, dict[str, str]]:
61 """Report whether the service can serve requests.
63 Args:
64 mongo_client: the Motor client to ping, or None if it was never built.
66 Returns:
67 (ready, dependencies) — ``ready`` is True only when every dependency is
68 up; ``dependencies`` maps each name to "up" or "down" and carries
69 nothing else.
70 """
71 dependencies = {"mongodb": await _ping_mongo(mongo_client)}
72 ready = all(state == UP for state in dependencies.values())
73 return ready, dependencies