use std::time::Duration;
use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use serde::Serialize;
use crate::state::AppState;
const DB_CHECK_TIMEOUT: Duration = Duration::from_secs(2);
#[derive(Serialize)]
pub struct HealthResponse {
pub status: &'static str,
}
pub async fn up_check() -> (StatusCode, Json<HealthResponse>) {
(StatusCode::OK, Json(HealthResponse { status: "ok" }))
}
pub async fn health_check(State(state): State<AppState>) -> (StatusCode, Json<HealthResponse>) {
match check_db(&state).await {
Ok(()) => (StatusCode::OK, Json(HealthResponse { status: "ok" })),
Err(err) => {
tracing::warn!(error = %err, "health check: database unreachable");
(
StatusCode::SERVICE_UNAVAILABLE,
Json(HealthResponse {
status: "unavailable",
}),
)
}
}
}
async fn check_db(state: &AppState) -> Result<(), sqlx::Error> {
let pool = state.tenant_db.pool().clone();
let probe = async move {
let mut conn = pool.acquire().await?;
sqlx::query("SELECT 1").execute(conn.as_mut()).await?;
Ok::<(), sqlx::Error>(())
};
match tokio::time::timeout(DB_CHECK_TIMEOUT, probe).await {
Ok(result) => result,
Err(_) => Err(sqlx::Error::PoolTimedOut),
}
}
#[cfg(test)]
mod tests {
use async_trait::async_trait;
use axum::body::Body;
use axum::http::Request;
use sqlx::PgPool;
use tower::ServiceExt;
use yorishiro_core::YorishiroError;
use yorishiro_core::db::TenantDb;
use yorishiro_core::services::embedding::EmbeddingProvider;
use super::*;
use crate::build_app;
struct UnreachableEmbeddingProvider;
#[async_trait]
impl EmbeddingProvider for UnreachableEmbeddingProvider {
fn dimensions(&self) -> usize {
768
}
async fn embed_batch(&self, _texts: &[&str]) -> Result<Vec<Vec<f32>>, YorishiroError> {
Err(YorishiroError::Internal(anyhow::anyhow!(
"embedding provider should not be called in this test"
)))
}
}
async fn get_response(pool: PgPool, uri: &str) -> axum::response::Response {
let state = AppState::new(
TenantDb::new(pool.clone()),
pool,
std::sync::Arc::new(UnreachableEmbeddingProvider),
);
let app = build_app(state, None);
app.oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap())
.await
.unwrap()
}
async fn health_response(pool: PgPool) -> axum::response::Response {
get_response(pool, "/health").await
}
#[sqlx::test(migrations = "../../migrations")]
async fn up_returns_ok_even_when_db_is_unreachable(pool: PgPool) {
pool.close().await;
let response = get_response(pool, "/up").await;
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["status"], "ok");
}
#[sqlx::test(migrations = "../../migrations")]
async fn health_returns_ok_when_db_is_reachable(pool: PgPool) {
let response = health_response(pool).await;
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["status"], "ok");
}
#[sqlx::test(migrations = "../../migrations")]
async fn health_returns_service_unavailable_when_db_is_unreachable(pool: PgPool) {
pool.close().await;
let response = health_response(pool).await;
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["status"], "unavailable");
}
}