"""健康检查 — 存活探针与就绪探针分离 为什么必须拆开: - 存活探针(liveness)只回答「进程还活着吗」,绝不能探测外部依赖。 否则数据库抖一下,编排系统会判定进程已死并反复重启容器, 把一次依赖故障放大成全站雪崩。 - 就绪探针(readiness)回答「现在能对外服务吗」。依赖不可用时返回 503, 由负载均衡把该实例摘掉,依赖恢复后自动回来。 """ from __future__ import annotations import logging import anyio from fastapi import APIRouter from fastapi.responses import JSONResponse from sqlalchemy import text from app.core.config import settings from app.core.database import AsyncSessionLocal from app.core.mom_database import mom_engine logger = logging.getLogger("track.health") router = APIRouter(tags=["健康检查"]) async def _probe_primary_db() -> bool: """主库探活 — 业务强依赖,失败即不就绪""" try: async with AsyncSessionLocal() as session: await session.execute(text("SELECT 1")) return True except Exception: logger.exception("主库探活失败") return False def _probe_mom_db_sync() -> bool: try: with mom_engine.connect() as conn: conn.execute(text("SELECT 1")) return True except Exception: logger.exception("MOM 库探活失败") return False async def _probe_mom_db() -> bool: # MOM 用的是同步引擎,放线程池执行,避免阻塞事件循环 return await anyio.to_thread.run_sync(_probe_mom_db_sync) async def _collect() -> tuple[bool, dict[str, str]]: primary_ok = await _probe_primary_db() mom_ok = await _probe_mom_db() checks = { "database": "ok" if primary_ok else "fail", # MOM 是外部只读依赖:挂掉时登录/选料降级,但扫码、流转、看板仍可用。 # 因此只标记 degraded、不摘流量 —— 否则 MOM 一抖就让在产车间全线停摆。 "mom_database": "ok" if mom_ok else "degraded", } return primary_ok, checks @router.get("/health/live", include_in_schema=False) async def liveness() -> dict: """存活探针:不触碰任何依赖,恒定快速返回""" return {"status": "ok"} @router.get("/health/ready", include_in_schema=False) async def readiness() -> JSONResponse: """就绪探针:主库不可用时返回 503,让负载均衡摘流量""" ready, checks = await _collect() return JSONResponse( {"status": "ready" if ready else "not_ready", "checks": checks}, status_code=200 if ready else 503, ) @router.get("/health", include_in_schema=False) async def health() -> JSONResponse: """兼容旧监控脚本:语义等同就绪探针,并附带版本号""" ready, checks = await _collect() return JSONResponse( { "status": "ok" if ready else "unavailable", "version": settings.APP_VERSION, "checks": checks, }, status_code=200 if ready else 503, )