use std::time::Instant;
use super::{CheckContext, fmt_db_error};
use crate::doctor::check::Check;
const WARN_LATENCY_MS: u64 = 1000;
pub async fn run(ctx: CheckContext) -> Check {
let host = ctx
.config
.db
.host
.clone()
.unwrap_or_else(|| "localhost".into());
let name = ctx.config.db.name.clone();
let start = Instant::now();
let connect_result = tokio_postgres::connect(&ctx.database_url, tokio_postgres::NoTls).await;
let latency_ms = start.elapsed().as_millis() as u64;
let check = match connect_result {
Ok((_, conn)) => {
tokio::spawn(async move {
let _ = conn.await;
});
let summary = format!("postgres at {host}/{name} ({latency_ms}ms)");
if latency_ms > WARN_LATENCY_MS {
Check::warning(
"db_connect",
summary,
format!("connect latency {latency_ms}ms over {WARN_LATENCY_MS}ms"),
)
} else {
Check::pass("db_connect", summary)
}
}
Err(err) => Check::fail(
"db_connect",
format!("failed to connect to {host}/{name}"),
fmt_db_error(&err),
),
};
check
.with_detail("db_host", host)
.with_detail("db_name", name)
.with_detail("latency_ms", latency_ms)
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use node_semver::Version;
use bestool_tamanu::{ApiServerKind, config::TamanuConfig};
use super::*;
use crate::doctor::check::CheckStatus;
#[tokio::test]
async fn unreachable_postgres_alerts() {
let config: TamanuConfig = serde_json::from_value(serde_json::json!({
"db": { "name": "tamanu-central", "username": "u", "password": "p" }
}))
.unwrap();
let ctx = CheckContext {
tamanu_version: Version::parse("0.0.0").unwrap(),
tamanu_root: std::path::PathBuf::from("/nonexistent"),
config: Arc::new(config),
kind: ApiServerKind::Central,
database_url: "postgresql://127.0.0.1:1/tamanu-central".into(),
db: None,
http_client: reqwest::Client::new(),
has_install: true,
};
let check = run(ctx).await;
assert!(
matches!(check.status, CheckStatus::Fail(_)),
"expected FAIL on unreachable postgres, got {:?}",
check.status
);
}
#[tokio::test]
async fn rejected_connection_surfaces_real_reason() {
if crate::doctor::checks::test_support::central_ctx()
.await
.is_none()
{
return;
}
let config: TamanuConfig = serde_json::from_value(serde_json::json!({
"db": { "name": "bestool-test-nonexistent-db", "username": "u", "password": "p" }
}))
.unwrap();
let ctx = CheckContext {
tamanu_version: Version::parse("0.0.0").unwrap(),
tamanu_root: std::path::PathBuf::from("/nonexistent"),
config: Arc::new(config),
kind: ApiServerKind::Central,
database_url: "postgresql://localhost/bestool-test-nonexistent-db".into(),
db: None,
http_client: reqwest::Client::new(),
has_install: true,
};
let check = run(ctx).await;
match check.status {
CheckStatus::Fail(reason) => {
assert!(
reason.contains("does not exist"),
"expected postgres's real 'database does not exist' rejection, got {reason:?}"
);
assert!(
reason.contains("bestool-test-nonexistent-db"),
"expected the message to name the missing database, got {reason:?}"
);
}
other => panic!("expected FAIL on a rejected connection, got {other:?}"),
}
}
}