feat(notify): /ready 接入 JMAP session 与 Telegram getMe 端到端探测
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)。
This commit is contained in:
parent
a39ebcf9d1
commit
c0aab5e966
|
|
@ -64,17 +64,81 @@ async fn healthz() -> impl IntoResponse {
|
||||||
(StatusCode::OK, "ok")
|
(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<String> {
|
||||||
|
app.business_runtime
|
||||||
|
.read()
|
||||||
|
.ok()?
|
||||||
|
.as_ref()
|
||||||
|
.map(|config| config.jmap_session_url.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cached_bot_token(app: &AppState) -> Option<secrecy::SecretString> {
|
||||||
|
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<AppState>) -> Response {
|
async fn ready(State(app): State<AppState>) -> Response {
|
||||||
let configured = app.setup_missing.is_empty();
|
let configured = app.setup_missing.is_empty();
|
||||||
let redis = app.state.is_enabled().await.is_ok();
|
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!({
|
axum::Json(serde_json::json!({
|
||||||
"status": "ready",
|
"status": "ready",
|
||||||
"configured": configured,
|
"configured": configured,
|
||||||
"redis": redis,
|
"redis": redis,
|
||||||
"jmap": configured,
|
"jmap": jmap,
|
||||||
"push": configured,
|
"push": configured,
|
||||||
"telegram": configured,
|
"telegram": telegram,
|
||||||
}))
|
}))
|
||||||
.into_response()
|
.into_response()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue