use tokio::task::JoinHandle; use tracing::info; use pom::alerts::Alerter; use pom::config::Config; use pom::db; use pom::types::HealthStatus; pub(crate) fn spawn_meta_alert_task( config: &Config, pool: &sqlx::SqlitePool, default_interval: u64, cancel: &tokio_util::sync::CancellationToken, alerter: &Option, ) -> Option> { let health_target_names: Vec = config.target_names() .into_iter() .filter(|n| config.get_target(n).is_some_and(|t| t.health.is_some())) .collect(); if health_target_names.len() < 2 { return None; } let alerter = alerter.clone()?; let pool = pool.clone(); let cancel = cancel.clone(); let meta_interval_secs = default_interval * 2; info!("Meta-alert: monitoring-offline check every {meta_interval_secs}s ({} targets)", health_target_names.len()); Some(tokio::spawn(async move { let mut interval = tokio::time::interval( std::time::Duration::from_secs(meta_interval_secs), ); interval.tick().await; // consume immediate first tick let mut was_all_down = false; loop { tokio::select! { _ = cancel.cancelled() => break, _ = interval.tick() => {} } let mut all_down = true; for name in &health_target_names { if let Ok(Some(snap)) = db::get_latest_health(&pool, name).await && (snap.status == HealthStatus::Operational || snap.status == HealthStatus::Degraded) { all_down = false; break; } } if all_down && !was_all_down { alerter.send_monitoring_offline_alert(health_target_names.len()).await; } else if !all_down && was_all_down { alerter.send_monitoring_recovery().await; } was_all_down = all_down; } })) }