"""就绪探针:MySQL / Redis / Milvus 必须是真实探测结果。 B3 修复:原先 ``checks["milvus"] = True`` 是硬编码,健康检查永远报告 Milvus 正常, 属于误导性探针。现在改为真实探测(连接 + ``get_server_version``),并满足: - 探测有超时(默认 2s),超时返回 ``False`` + ``timeout`` 状态; - 外部依赖不可用时不抛异常,只返回 ``False`` + 明确状态,整体降级为 ``degraded``; - pymilvus 是可选依赖(且无类型存根),缺失时返回 ``False`` + ``client_missing``。 """ import asyncio from typing import Any from sqlalchemy import text from app.core.config import get_settings from app.infrastructure.db import SessionFactory MILVUS_PROBE_TIMEOUT_SECONDS = 2.0 def _create_milvus_client(uri: str, token: str) -> Any: """延迟导入并建连:pymilvus 为可选依赖,缺失时由调用方转为明确状态。""" from pymilvus import AsyncMilvusClient # type: ignore[import-untyped] return AsyncMilvusClient(uri=uri, token=token or None) class HealthService: def __init__(self, *, milvus_timeout_seconds: float = MILVUS_PROBE_TIMEOUT_SECONDS) -> None: self._milvus_timeout_seconds = milvus_timeout_seconds async def ready(self) -> dict[str, Any]: checks: dict[str, bool] = {} probes: dict[str, str] = {} try: async with SessionFactory() as session: await session.execute(text("SELECT 1")) checks["mysql"] = True except Exception: checks["mysql"] = False probes["mysql"] = "unavailable" checks["redis"] = await self._redis_ready() milvus_ready, milvus_state = await self._milvus_ready() checks["milvus"] = milvus_ready probes["milvus"] = milvus_state return { "status": "ready" if all(checks.values()) else "degraded", "checks": checks, "probes": probes, } async def _redis_ready(self) -> bool: client = None try: from redis.asyncio import Redis settings = get_settings() client = Redis.from_url( settings.redis_url, socket_connect_timeout=settings.redis_connect_timeout_seconds, socket_timeout=settings.redis_connect_timeout_seconds, decode_responses=True, ) return bool(await client.ping()) except Exception: return False finally: if client is not None: try: await client.aclose() except Exception: pass async def _milvus_ready(self) -> tuple[bool, str]: """真实探测 Milvus:状态取值 ok / unavailable / client_missing / timeout。""" settings = get_settings() client: Any = None try: async with asyncio.timeout(self._milvus_timeout_seconds): client = await asyncio.to_thread( _create_milvus_client, settings.milvus_uri, settings.milvus_token ) await client.get_server_version() return True, "ok" except TimeoutError: return False, "timeout" except ImportError: return False, "client_missing" except Exception: return False, "unavailable" finally: await self._close_milvus_client(client) @staticmethod async def _close_milvus_client(client: Any) -> None: if client is None: return try: await client.close() except Exception: pass