From c0aab5e9661cbe468bc10a972aa0348fa378404c Mon Sep 17 00:00:00 2001 From: anonsaber Date: Mon, 28 Sep 2026 10:15:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(notify):=20/ready=20=E6=8E=A5=E5=85=A5=20J?= =?UTF-8?q?MAP=20session=20=E4=B8=8E=20Telegram=20getMe=20=E7=AB=AF?= =?UTF-8?q?=E5=88=B0=E7=AB=AF=E6=8E=A2=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ARCH-READY-BASELINE 从「配置完整性 + Redis 可达性」升级为端到端就绪: - PROBE_TIMEOUT=3000ms 各探针独立超时(notify.rs:69) - cached_jmap_session_url / cached_bot_token 先克隆后 await, 避免跨 .await 持有 RwLockReadGuard(非 Send) - probe_jmap_session:GET JMAP session URL,error_for_status - probe_telegram_get_me:bot token 仅用于拼 URL,绝不日志 - ready():四者全过返 200 + 就绪报告 JSON(jmap/telegram 真实结果), 任一失败 503 + Retry-After: 30(标准错误 envelope) - 探针只读、无状态写入;token 永不回显(SAF-NO-SECRET-ECHO) 门禁:fmt / check / clippy / test 4/4 绿(45 passed, 0 failed, 1 ignored)。 --- src/notify.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/src/notify.rs b/src/notify.rs index 93de73b..0d6a3ea 100644 --- a/src/notify.rs +++ b/src/notify.rs @@ -64,17 +64,81 @@ async fn healthz() -> impl IntoResponse { (StatusCode::OK, "ok") } +/// Probe budget for readiness checks. Kept short so `/ready` fails fast instead of holding an +/// upstream connection open (no long-lived connections). +const PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(3_000); + +/// Read the cached runtime configuration without holding the lock guard across an await. +/// Callers refresh the cache first so this never reports a stale view. +fn cached_jmap_session_url(app: &AppState) -> Option { + app.business_runtime + .read() + .ok()? + .as_ref() + .map(|config| config.jmap_session_url.clone()) +} + +fn cached_bot_token(app: &AppState) -> Option { + app.business_runtime + .read() + .ok()? + .as_ref() + .map(|config| config.bot_token.clone()) +} + +/// End-to-end JMAP session probe: the configured session URL must answer with a 2xx. +/// Reusable by the remote debug surface so readiness and debugging share one implementation. +async fn probe_jmap_session(app: &AppState) -> Result<(), &'static str> { + let Some(session_url) = cached_jmap_session_url(app) else { + return Err("jmap session url not configured"); + }; + let request = reqwest::Client::new().get(session_url).send(); + match tokio::time::timeout(PROBE_TIMEOUT, request).await { + Ok(Ok(response)) => response + .error_for_status() + .map(|_| ()) + .map_err(|_| "jmap session probe failed"), + Ok(Err(_)) => Err("jmap session probe failed"), + Err(_) => Err("jmap session probe timed out"), + } +} + +/// End-to-end Telegram probe: `getMe` must answer with a 2xx. The bot token is used to build the +/// request URL only; it is never logged, echoed, or returned (SAF-NO-SECRET-ECHO). +async fn probe_telegram_get_me(app: &AppState) -> Result<(), &'static str> { + let Some(token) = cached_bot_token(app) else { + return Err("telegram bot token not configured"); + }; + let request = reqwest::Client::new() + .get(format!( + "https://api.telegram.org/bot{}/getMe", + token.expose_secret() + )) + .send(); + match tokio::time::timeout(PROBE_TIMEOUT, request).await { + Ok(Ok(response)) => response + .error_for_status() + .map(|_| ()) + .map_err(|_| "telegram get_me probe failed"), + Ok(Err(_)) => Err("telegram get_me probe failed"), + Err(_) => Err("telegram get_me probe timed out"), + } +} + async fn ready(State(app): State) -> Response { let configured = app.setup_missing.is_empty(); let redis = app.state.is_enabled().await.is_ok(); - if configured && redis { + refresh_business_config(&app).await; + let jmap = probe_jmap_session(&app).await.is_ok(); + let telegram = probe_telegram_get_me(&app).await.is_ok(); + if configured && redis && jmap && telegram { axum::Json(serde_json::json!({ "status": "ready", "configured": configured, "redis": redis, - "jmap": configured, + "jmap": jmap, "push": configured, - "telegram": configured, + "telegram": telegram, })) .into_response() } else {