Skip to content

Commit f909095

Browse files
fix(health): add health checkup for external check of osmsg
1 parent 0202e68 commit f909095

4 files changed

Lines changed: 49 additions & 0 deletions

File tree

api/app.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
from contextlib import asynccontextmanager
3+
from datetime import UTC, datetime, timedelta
34
from pathlib import Path
45

56
import asyncpg
@@ -19,6 +20,7 @@
1920
from .schemas import HealthResponse
2021

2122
FRONTEND_DIST = os.getenv("FRONTEND_DIST")
23+
_MAX_STALENESS = timedelta(seconds=int(os.getenv("OSMSG_HEALTH_MAX_STALENESS_SECONDS", "1800")))
2224
DEFAULT_CORS_ORIGINS = (
2325
"http://localhost:5173",
2426
"http://127.0.0.1:5173",
@@ -91,6 +93,13 @@ async def health() -> HealthResponse:
9193
state = await fetch_state()
9294
except (OSError, asyncpg.PostgresError) as exc:
9395
raise HTTPException(status_code=503, detail="database unavailable") from exc
96+
if state and state["last_ts"] is not None:
97+
last_ts = state["last_ts"]
98+
if last_ts.tzinfo is None:
99+
last_ts = last_ts.replace(tzinfo=UTC)
100+
age = datetime.now(UTC) - last_ts
101+
if age > _MAX_STALENESS:
102+
raise HTTPException(status_code=503, detail=f"stale: newest data is {age} old")
94103
return HealthResponse(
95104
status="ok",
96105
last_seq=state["last_seq"] if state else None,

infra/.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ GA_MEASUREMENT_ID=
1717
# ticks, each committing its slice, instead of accumulating the whole gap in one pass. Unset = off.
1818
OSMSG_MAX_UPDATE_WINDOW_HOURS=24
1919

20+
# Kill a tick that overruns this many seconds so a hung run cannot wedge the scheduler (default 1200).
21+
# OSMSG_TICK_TIMEOUT_SECONDS=1200
22+
# /health returns 503 when the newest data is older than this many seconds, so an HTTP monitor alerts on a
23+
# stalled worker (default 1800).
24+
# OSMSG_HEALTH_MAX_STALENESS_SECONDS=1800
25+
2026
# Postgres DSN for the monthly prune (drops rows now covered by published history). Same db as the
2127
# worker; the prune runs inside the compose network.
2228
# OSMSG_PSQL_DSN=postgresql://osmsg:osmsg@db:5432/osmsg

infra/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ services:
6060
OSMSG_BOOTSTRAP: ${OSMSG_BOOTSTRAP:-hour}
6161
OSMSG_BOOTSTRAP_DAYS: ${OSMSG_BOOTSTRAP_DAYS:-}
6262
OSMSG_MAX_UPDATE_WINDOW_HOURS: ${OSMSG_MAX_UPDATE_WINDOW_HOURS:-}
63+
OSMSG_TICK_TIMEOUT_SECONDS: ${OSMSG_TICK_TIMEOUT_SECONDS:-}
6364
OSM_USERNAME: ${OSM_USERNAME:-}
6465
OSM_PASSWORD: ${OSM_PASSWORD:-}
6566
OSMSG_EXTRA_ARGS: ${OSMSG_EXTRA_ARGS:-}

tests/test_api.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,39 @@ async def failing_state():
142142
assert response.status_code == 503
143143

144144

145+
def test_health_reports_503_when_data_is_stale(monkeypatch):
146+
from datetime import UTC, datetime, timedelta
147+
148+
app_module = import_module("api.app")
149+
150+
async def old_state():
151+
now = datetime.now(UTC)
152+
return {"last_seq": 1, "last_ts": now - timedelta(hours=5), "updated_at": now}
153+
154+
monkeypatch.setattr(app_module, "fetch_state", old_state)
155+
with TestClient(Litestar(route_handlers=[health])) as client:
156+
response = client.get("/health")
157+
158+
assert response.status_code == 503
159+
160+
161+
def test_health_ok_when_data_is_fresh(monkeypatch):
162+
from datetime import UTC, datetime
163+
164+
app_module = import_module("api.app")
165+
166+
async def fresh_state():
167+
now = datetime.now(UTC)
168+
return {"last_seq": 1, "last_ts": now, "updated_at": now}
169+
170+
monkeypatch.setattr(app_module, "fetch_state", fresh_state)
171+
with TestClient(Litestar(route_handlers=[health])) as client:
172+
response = client.get("/health")
173+
174+
assert response.status_code == 200
175+
assert response.json()["status"] == "ok"
176+
177+
145178
def test_window_normalizes_naive_datetime_to_utc():
146179
from datetime import UTC, datetime
147180

0 commit comments

Comments
 (0)