use std::sync::atomic::{AtomicU64, Ordering};
use serde_json::Value;
use super::{CheckContext, util::fetch_rows};
use crate::doctor::Stat;
use crate::doctor::check::Check;
use bestool_tamanu::ApiServerKind;
const NAME: &str = "sync_session_errors";
const FAIL_ERRORS: usize = 10;
const MOBILE_SQL: &str = "SELECT id, errors::text, \
parameters->'facilityIds' AS facility_ids, \
created_at::text AS created, (completed_at - created_at)::text AS duration \
FROM sync_sessions \
WHERE updated_at > now() - interval '1 minute' \
AND parameters->>'isMobile' = 'true' \
AND errors IS NOT NULL \
AND errors <> ARRAY['Session marked as completed due to its device reconnecting'] \
AND errors <> ARRAY['could not serialize access due to concurrent update'] \
ORDER BY created_at DESC";
const SERVER_SQL: &str = "SELECT id, errors::text, \
parameters->'facilityIds' AS facility_ids, \
created_at::text AS created, (completed_at - created_at)::text AS duration \
FROM sync_sessions \
WHERE updated_at > now() - interval '1 minute' \
AND parameters->>'isMobile' IS DISTINCT FROM 'true' \
AND errors IS NOT NULL \
AND errors <> ARRAY['could not serialize access due to concurrent update'] \
AND NOT (cardinality(errors) = 1 AND errors[1] LIKE '%snapshot-for-pushing%') \
ORDER BY created_at DESC";
static MOBILE_SEEN: AtomicU64 = AtomicU64::new(0);
static SERVER_SEEN: AtomicU64 = AtomicU64::new(0);
fn accumulate(seen: &AtomicU64, found: u64) -> u64 {
seen.fetch_add(found, Ordering::Relaxed) + found
}
fn with_error_counters(check: Check, mobile_seen: u64, server_seen: u64) -> Check {
check
.with_stat(
Stat::counter("errors_total", mobile_seen as f64)
.label("stream", "mobile")
.group("errors")
.help("Sync-session errors seen"),
)
.with_stat(
Stat::counter("errors_total", server_seen as f64)
.label("stream", "server")
.group("errors")
.help("Sync-session errors seen"),
)
}
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 mobile = match fetch_rows(client, MOBILE_SQL, &[]).await {
Ok(set) => set,
Err(err) => return super::query_error_check(NAME, &err),
};
let server = match fetch_rows(client, SERVER_SQL, &[]).await {
Ok(set) => set,
Err(err) => return super::query_error_check(NAME, &err),
};
let mobile_seen = accumulate(&MOBILE_SEEN, mobile.total);
let server_seen = accumulate(&SERVER_SEEN, server.total);
if mobile.is_empty() && server.is_empty() {
return with_error_counters(
Check::pass(NAME, "no recent sync session errors"),
mobile_seen,
server_seen,
);
}
let (mobile_count, mobile_truncated) = (mobile.count(), mobile.truncated);
let (server_count, server_truncated) = (server.count(), server.truncated);
let total = (mobile.total + server.total) as usize;
let summary = format!("sync session errors: {mobile_count} mobile, {server_count} server");
let reason = "recent sync session error(s)";
let check = if total >= FAIL_ERRORS {
Check::fail(NAME, summary, reason)
} else {
Check::warning(NAME, summary, reason)
};
with_error_counters(
check
.with_detail("mobile", Value::Array(mobile.rows))
.with_detail("mobile_count", mobile_count)
.with_detail("mobile_truncated", mobile_truncated)
.with_detail("server", Value::Array(server.rows))
.with_detail("server_count", server_count)
.with_detail("server_truncated", server_truncated),
mobile_seen,
server_seen,
)
}
#[cfg(test)]
mod tests {
use std::sync::atomic::AtomicU64;
use super::{MOBILE_SQL, SERVER_SQL, accumulate};
use crate::doctor::checks::test_support::{central_ctx, facility_ctx};
#[test]
fn queries_return_one_row_per_session() {
for sql in [MOBILE_SQL, SERVER_SQL] {
assert!(!sql.contains("jsonb_array_elements"));
assert!(sql.contains("parameters->'facilityIds' AS facility_ids"));
}
}
#[test]
fn accumulate_sums_successive_windows() {
let seen = AtomicU64::new(0);
assert_eq!(accumulate(&seen, 3), 3);
assert_eq!(accumulate(&seen, 4), 7);
assert_eq!(accumulate(&seen, 0), 7);
}
#[tokio::test]
async fn runs_against_central() {
let Some(ctx) = central_ctx().await else {
return;
};
let check = super::run(ctx).await;
assert_eq!(check.name, "sync_session_errors");
}
#[tokio::test]
async fn skips_on_facility() {
let check = super::run(facility_ctx()).await;
assert!(check.status.is_skip());
}
}