use bestool_tamanu::ApiServerKind;
use super::{CheckContext, query_error_check};
use crate::doctor::Stat;
use crate::doctor::check::Check;
const NAME: &str = "sync_snapshot_tables";
pub async fn run(ctx: CheckContext) -> Check {
if ctx.kind != ApiServerKind::Central {
return Check::skip(
NAME,
"not applicable on facility server",
"central-only check",
);
}
let Some(client) = ctx.db.as_ref() else {
return Check::skip(NAME, "no DB connection", "db unavailable");
};
let query = "
SELECT
(SELECT count(*) FROM pg_tables WHERE schemaname = 'sync_snapshots') AS table_count,
(SELECT count(*) FROM sync_sessions WHERE start_time > now() - interval '24 hours') AS sessions_24h,
sizes.p50,
sizes.p99,
sizes.total_bytes
FROM (
SELECT
percentile_cont(0.5) WITHIN GROUP (ORDER BY sz) AS p50,
percentile_cont(0.99) WITHIN GROUP (ORDER BY sz) AS p99,
coalesce(sum(sz), 0)::double precision AS total_bytes
FROM (
SELECT pg_total_relation_size(c.oid)::double precision AS sz
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'sync_snapshots' AND c.relkind = 'r'
) t
) sizes
";
let row = match client.query_one(query, &[]).await {
Ok(r) => r,
Err(err) => {
if let Some(db) = err.as_db_error()
&& db.code() == &tokio_postgres::error::SqlState::UNDEFINED_TABLE
{
return Check::skip(NAME, "sync_sessions table not present", "table absent");
}
return query_error_check(NAME, &err);
}
};
let tables: i64 = row.try_get("table_count").unwrap_or(0);
let sessions: i64 = row.try_get("sessions_24h").unwrap_or(0);
let p50: Option<f64> = row.try_get("p50").unwrap_or(None);
let p99: Option<f64> = row.try_get("p99").unwrap_or(None);
let total_bytes: Option<f64> = row.try_get("total_bytes").unwrap_or(None);
let summary = format!("{tables} snapshot table(s), {sessions} sync session(s)/24h");
let check = match classify(tables, sessions) {
Verdict::Pass => Check::pass(NAME, summary),
Verdict::Warn(reason) => Check::warning(NAME, summary, reason),
Verdict::Fail(reason) => Check::fail(NAME, summary, reason),
};
let mut check = check
.with_detail("table_count", tables)
.with_detail("sessions_24h", sessions)
.with_stat(Stat::gauge("table_count", tables as f64).help("Leftover sync-snapshot tables"));
if let Some(p50) = p50 {
check = check.with_stat(
Stat::gauge("table_size_bytes", p50)
.group("sizes")
.label("quantile", "0.5")
.help("Snapshot-table size percentiles"),
);
}
if let Some(p99) = p99 {
check = check.with_stat(
Stat::gauge("table_size_bytes", p99)
.group("sizes")
.label("quantile", "0.99")
.help("Snapshot-table size percentiles"),
);
}
if let Some(total) = total_bytes {
check = check.with_stat(
Stat::gauge("total_size_bytes", total).help("Total size of all snapshot tables"),
);
}
check
}
enum Verdict {
Pass,
Warn(String),
Fail(String),
}
fn classify(tables: i64, sessions_24h: i64) -> Verdict {
let warn_at = sessions_24h as f64 * 1.1;
let fail_at = sessions_24h as f64 * 2.0;
let t = tables as f64;
if t > fail_at {
Verdict::Fail(format!(
"{tables} snapshot tables is more than double the {sessions_24h} sync session(s) in the last 24h"
))
} else if t > warn_at {
Verdict::Warn(format!(
"{tables} snapshot tables exceeds the {sessions_24h} sync session(s) in the last 24h plus 10%"
))
} else {
Verdict::Pass
}
}
#[cfg(test)]
mod tests {
use super::*;
fn verdict(tables: i64, sessions: i64) -> &'static str {
match classify(tables, sessions) {
Verdict::Pass => "pass",
Verdict::Warn(_) => "warn",
Verdict::Fail(_) => "fail",
}
}
#[test]
fn within_recent_activity_passes() {
assert_eq!(verdict(100, 100), "pass");
assert_eq!(verdict(110, 100), "pass");
}
#[test]
fn modest_excess_warns() {
assert_eq!(verdict(120, 100), "warn");
assert_eq!(verdict(200, 100), "warn");
}
#[test]
fn more_than_double_fails() {
assert_eq!(verdict(201, 100), "fail");
}
#[test]
fn leftover_with_no_recent_sessions_fails() {
assert_eq!(verdict(5, 0), "fail");
}
#[test]
fn empty_schema_always_passes() {
assert_eq!(verdict(0, 0), "pass");
assert_eq!(verdict(0, 50), "pass");
}
}