wowlab_sentinel/cron/
integrity.rs1use async_trait::async_trait;
2
3use super::CronJob;
4use crate::state::ServerState;
5
6const RANKING_STALE_THRESHOLD_HOURS: f64 = 24.0;
7
8#[derive(Debug)]
9pub(crate) struct IntegrityCheckJob {
10 schedule: String,
11}
12
13impl IntegrityCheckJob {
14 pub(crate) fn new(schedule: &str) -> Self {
15 Self {
16 schedule: schedule.to_owned(),
17 }
18 }
19}
20
21#[async_trait]
22impl CronJob for IntegrityCheckJob {
23 fn name(&self) -> &'static str {
24 "integrity_check"
25 }
26
27 fn schedule(&self) -> &str {
28 &self.schedule
29 }
30
31 async fn run(&self, state: &ServerState) {
32 if let Err(e) = run_integrity_check(state).await {
33 tracing::error!(error = %e, "Integrity check failed");
34 }
35 }
36}
37
38async fn run_integrity_check(
39 state: &ServerState,
40) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
41 let active_count: i64 = sqlx::query_file_scalar!("queries/integrity_active_seasons.sql")
42 .fetch_one(state.dbs.get::<crate::cron::CronDb>())
43 .await?;
44
45 match active_count {
46 1 => tracing::debug!("Integrity: active season count OK"),
47 0 => tracing::warn!("Integrity: no active season found"),
48 n => tracing::error!(count = n, "Integrity: multiple active seasons"),
49 }
50
51 let orphaned: i64 = sqlx::query_file_scalar!("queries/integrity_orphaned_jobs.sql")
52 .fetch_one(state.dbs.get::<crate::cron::CronDb>())
53 .await?;
54
55 if orphaned > 0 {
56 tracing::warn!(
57 count = orphaned,
58 "Integrity: completed jobs without season_id"
59 );
60 }
61
62 let stale_hours: Option<f64> =
63 sqlx::query_file_scalar!("queries/integrity_ranking_freshness.sql")
64 .fetch_one(state.dbs.get::<crate::cron::CronDb>())
65 .await?;
66
67 if let Some(hours) = stale_hours {
68 if hours > RANKING_STALE_THRESHOLD_HOURS {
69 tracing::warn!(hours_stale = hours, "Integrity: rankings are stale");
70 }
71 }
72
73 tracing::info!(
74 active_seasons = active_count,
75 orphaned_jobs = orphaned,
76 "Integrity check completed"
77 );
78
79 Ok(())
80}