//! Background backup freshness check task. use tokio::task::JoinHandle; use tracing::info; use pom::alerts::Alerter; use pom::checks::backup; use pom::config::Config; use pom::db; use super::{CheckInterval, configured_targets}; pub(crate) fn spawn_backup_tasks( config: &Config, pool: &sqlx::SqlitePool, cancel: &tokio_util::sync::CancellationToken, alerter: Option<&Alerter>, ) -> Vec> { let mut handles = Vec::new(); for (name, target_config) in configured_targets(config) { if let Some(backup_config) = target_config.backups { let pool = pool.clone(); let name = name.clone(); let label = target_config.label.clone(); let alerter = alerter.cloned(); let cancel = cancel.clone(); let interval_secs = backup_config.interval_secs; info!( "{name}: backup check every {interval_secs}s (dir={}, databases={:?})", backup_config.directory, backup_config.databases ); handles.push(tokio::spawn(async move { let mut ticks = CheckInterval::new(interval_secs, cancel); while ticks.next().await { for database in &backup_config.databases { let result = backup::check_backup( &name, &backup_config.directory, database, backup_config.max_age_hours, ); info!( "{}: backup {}, {} (age: {}h)", name, database, result.status, result.age_hours.unwrap_or(-1), ); // Check for status transitions before storing let previous = db::get_latest_backup_check(&pool, &name, database) .await .ok() .flatten(); if let Err(e) = db::insert_backup_check(&pool, &result).await { tracing::error!( "{name}: failed to store backup check for {database}: {e}" ); } // Fire alerts on status transitions if let Some(ref alerter) = alerter { let prev_status = previous.as_ref().map(|p| p.status.as_str()); let was_ok = prev_status.is_none_or(|s| s == "ok"); let now_ok = result.status == "ok"; if was_ok && !now_ok { // Transitioned to a bad state alerter .send_backup_stale_alert( &name, &label, database, &result.status, result.age_hours, ) .await; } else if !was_ok && now_ok { // Recovered alerter.send_backup_recovery(&name, &label, database).await; } } } } })); } } handles }