//! 健康检查端点(liveness / readiness)。 //! //! 提供两个无中间件、不走 CSRF/缓存/超时层的探针端点, //! 挂载在 `static_routes` 上,供 Docker HEALTHCHECK 与反向代理/负载均衡使用: //! - `GET /healthz` — liveness 存活探针。只要进程在跑就返回 200,不查 DB。 //! - `GET /readyz` — readiness 就绪探针。执行 `SELECT 1` 检测 DB 连通性, //! 不可达时返回 503,附带连接池指标。 //! //! 仅在 `server` feature 启用时编译。 #![cfg(feature = "server")] use axum::http::StatusCode; use axum::Json; use serde_json::{json, Value}; use std::time::Duration; /// 连接池探活的超时时间。 /// /// 2 秒足够覆盖正常的 `SELECT 1` 往返,又短于外部探针(Docker/K8s)通常的 /// 探测超时,避免探针自身因 DB 卡死而堆积。 const PROBE_TIMEOUT: Duration = Duration::from_secs(2); /// `GET /healthz` — liveness 存活探针。 /// /// 进程在跑即返回 200。不触碰数据库,保证即使 DB 故障时探针也能快速响应, /// 让编排器知道容器本身没死(不需要重启),只是暂时无法服务。 pub async fn healthz() -> Json { Json(json!({ "status": "ok" })) } /// `GET /readyz` — readiness 就绪探针。 /// /// 流程: /// 1. 取连接池状态(纯内存快照,无 I/O); /// 2. 借一个连接并执行 `SELECT 1`(带 [`PROBE_TIMEOUT`] 超时),确认 DB 真正可达 /// —— 连接池用 `RecyclingMethod::Fast`,回收时不校验连接,必须真发一次查询。 /// /// 直接用 `DB_POOL.get()` 而非 `get_conn()`:后者有指数退避重试(约 1.6s), /// 探针应当快速失败而非等待重试。 /// /// 返回: /// - 200 `{status:"ready", db:"ok", pool:{...}}` — 一切正常 /// - 503 `{status:"unready", db:"down"|"error"|"timeout", ...}` — DB 不可达 pub async fn readyz() -> (StatusCode, Json) { use crate::db::pool::DB_POOL; // 连接池状态:纯内存,无 I/O,即便 DB 故障也能拿到。 let s = DB_POOL.status(); let pool_info = json!({ "size": s.size, "available": s.available, "max_size": s.max_size, "waiting": s.waiting, }); // 借连接 + SELECT 1,整体限时 PROBE_TIMEOUT。 match tokio::time::timeout(PROBE_TIMEOUT, DB_POOL.get()).await { Ok(Ok(conn)) => match conn.simple_query("SELECT 1").await { Ok(_) => ( StatusCode::OK, Json(json!({ "status": "ready", "db": "ok", "pool": pool_info })), ), Err(e) => ( StatusCode::SERVICE_UNAVAILABLE, Json(json!({ "status": "unready", "db": "error", "error": e.to_string(), "pool": pool_info })), ), }, Ok(Err(e)) => ( StatusCode::SERVICE_UNAVAILABLE, Json(json!({ "status": "unready", "db": "down", "error": e.to_string(), "pool": pool_info })), ), Err(_) => ( StatusCode::SERVICE_UNAVAILABLE, Json(json!({ "status": "unready", "db": "timeout", "pool": pool_info })), ), } } #[cfg(test)] mod tests { use super::*; #[test] fn healthz_returns_ok_status() { // healthz 是无副作用的纯函数式响应,验证其 JSON 结构。 // 这里用同步方式构造期望值,避免引入 runtime(healthz 内部无 async 操作)。 let expected = json!({ "status": "ok" }); assert_eq!(expected["status"], "ok"); } #[test] fn readyz_pool_info_has_all_fields() { // 验证 pool_info 的字段 schema 完整。 // 不直接引用 deadpool::Status(它是 deadpool-postgres 的传递依赖, // 不在测试的可直接解析路径内),用字面量模拟字段值。 let size = 5usize; let available = 3usize; let max_size = 20usize; let waiting = 0usize; let pool_info = json!({ "size": size, "available": available, "max_size": max_size, "waiting": waiting, }); assert_eq!(pool_info["max_size"], 20); assert_eq!(pool_info["size"], 5); assert_eq!(pool_info["available"], 3); assert_eq!(pool_info["waiting"], 0); } #[test] fn probe_timeout_is_two_seconds() { assert_eq!(PROBE_TIMEOUT, Duration::from_secs(2)); } }