diff --git a/src/main.py b/src/main.py index 2c0e75c..7f169f9 100644 --- a/src/main.py +++ b/src/main.py @@ -15,6 +15,7 @@ from src.views import router as frontend_router app = FastAPI(title="Stackchain Dashboard") CONTEXT_TIMEOUT_SECONDS = 5.0 EVENT_STREAM_TIMEOUT_SECONDS = 5.0 +READINESS_TIMEOUT_SECONDS = 5.0 FRONTEND_DIR = Path(__file__).resolve().parent.parent / "frontend" app.add_middleware( @@ -39,15 +40,26 @@ def health() -> dict[str, str]: async def readiness(): """Return readiness after verifying the configured Gitea connection.""" try: - user = await current_user() + user = await asyncio.wait_for( + current_user(), timeout=READINESS_TIMEOUT_SECONDS + ) except Exception as exc: + timed_out = isinstance(exc, TimeoutError) + error_message = ( + f"Gitea readiness check timed out after {READINESS_TIMEOUT_SECONDS:g}s" + if timed_out + else str(exc) + ) return JSONResponse( { "status": "not_ready", "service": "stackchain-dashboard", - "error": str(exc), + "error": error_message, }, status_code=503, + headers={ + "Retry-After": str(max(1, math.ceil(READINESS_TIMEOUT_SECONDS))) + } if timed_out else None, ) return { "status": "ready", diff --git a/tests/test_health.py b/tests/test_health.py index 7e36988..0ac3564 100644 --- a/tests/test_health.py +++ b/tests/test_health.py @@ -1,3 +1,6 @@ +import asyncio +import json + import pytest from src import main @@ -37,3 +40,29 @@ async def test_readiness_endpoint_returns_503_when_gitea_is_unavailable(monkeypa assert response.status_code == 503 assert b'"status":"not_ready"' in response.body assert b'"error":"connection refused"' in response.body + + +@pytest.mark.anyio +async def test_readiness_endpoint_times_out_and_cancels_stalled_gitea_check(monkeypatch): + cancelled = asyncio.Event() + + async def hanging_user(): + try: + await asyncio.sleep(0.05) + return {"login": "too-late"} + finally: + cancelled.set() + + monkeypatch.setattr(main, "READINESS_TIMEOUT_SECONDS", 0.01, raising=False) + monkeypatch.setattr(main, "current_user", hanging_user) + + response = await main.readiness() + + assert response.status_code == 503 + assert response.headers["retry-after"] == "1" + assert json.loads(response.body) == { + "status": "not_ready", + "service": "stackchain-dashboard", + "error": "Gitea readiness check timed out after 0.01s", + } + assert cancelled.is_set()