feat(stability): stage 6 — lightweight /ping healthcheck that never touches Telegram (#6)
The container healthcheck hit /rss/...?limit=1 (5s timeout): on a cold cache RSS generation exceeds 5s, or a hung TG RPC makes it hang, so docker/autoheal restarts the container mid-download and corrupts temp files. Replace it with /ping, which reflects process/loop liveness (answers instantly, always) plus TG liveness read from the watchdog's last-probe data — issuing ZERO Telegram RPC. - telegram_client: public watchdog_last_ok_age() — seconds since the last successful watchdog probe (None if never). Pure read of the Stage-1 _wd_last_ok_monotonic field; no RPC. - api_server: /ping route (no token, no TG RPC, no SQLite, no fs scan). healthy = connected and (age is None or age < threshold). age is None right after boot => healthy (don't kill before the first probe). connected coerced to bool so the JSON "connected" field is always a bool (pre-start reports false, never null). - config: TG_PING_UNHEALTHY_AFTER knob, default interval*(failures+1)+timeout = 250s (how long until the watchdog itself gives up), env-overridable. - dockercompose.yml: healthcheck -> curl -sf http://127.0.0.1:80/ping, interval 5m, timeout 5s, retries 3, start_period 30s. Old /rss check removed, not left behind. - tests: 10 (healthy/stale/disconnected/fresh-boot/pre-start-null/no-token + the anti-regression zero-TG-RPC spy across all branches + the accessor). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+33
-1
@@ -33,7 +33,7 @@ from pyrogram import errors
|
||||
from pyrogram.types import Message
|
||||
from pyrogram.enums import MessageMediaType
|
||||
from fastapi import FastAPI, HTTPException, Response, Request
|
||||
from fastapi.responses import HTMLResponse, FileResponse
|
||||
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
|
||||
from telegram_client import TelegramClient
|
||||
from config import get_settings, setup_logging
|
||||
from rss_generator import generate_channel_rss, generate_channel_html
|
||||
@@ -1065,6 +1065,38 @@ async def get_raw_post_json(channel: str, post_id: int, request: Request, token:
|
||||
raise HTTPException(status_code=500, detail=error_message) from e
|
||||
|
||||
|
||||
@app.get("/ping")
|
||||
async def ping() -> JSONResponse:
|
||||
"""Lightweight liveness probe for the container healthcheck.
|
||||
|
||||
Reflects process/event-loop liveness (always answers in microseconds) and TG liveness
|
||||
from the watchdog's last-probe data. It MUST NOT issue any Telegram RPC (no get_me,
|
||||
no safe_get_messages), touch SQLite, or walk the filesystem — that is the whole point:
|
||||
it stays instant and truthful even while a real TG RPC is hung. It only reads the
|
||||
already-recorded watchdog timestamp and the is_connected bool.
|
||||
"""
|
||||
age = client.watchdog_last_ok_age() # seconds since last OK probe, None if never
|
||||
# is_connected is None before client.start() and a bool afterwards; coerce so the JSON
|
||||
# "connected" field is always a bool (never null) and the pre-start window reports false.
|
||||
connected = bool(client.client.is_connected)
|
||||
threshold = Config["tg_ping_unhealthy_after"]
|
||||
# age is None right after boot: the watchdog hasn't run its first probe yet. Treat that
|
||||
# as healthy (gate on connected only) so a freshly-started container is not killed before
|
||||
# its first probe cycle — otherwise start_period would have to cover a full watchdog interval.
|
||||
# Note: when the watchdog is DISABLED (TG_WATCHDOG_ENABLED=false) age stays None forever,
|
||||
# so /ping degenerates to a pure connectivity check and cannot detect a stale-but-connected
|
||||
# ("zombie") session — that TG-liveness signal only exists while the watchdog runs.
|
||||
healthy = connected and (age is None or age < threshold)
|
||||
return JSONResponse(
|
||||
{
|
||||
"status": "ok" if healthy else "degraded",
|
||||
"connected": connected,
|
||||
"last_probe_age_s": round(age, 1) if age is not None else None,
|
||||
"threshold_s": threshold,
|
||||
},
|
||||
status_code=200 if healthy else 503,
|
||||
)
|
||||
|
||||
@app.get("/health")
|
||||
@app.get("/health/{token}")
|
||||
async def health_check(request: Request, token: str | None = None) -> Response:
|
||||
|
||||
Reference in New Issue
Block a user