//! Scan-pipeline health-check task. Polls the makenotwork health endpoint //! on a per-target interval, applies thresholds (audit doc § 6), and fires //! alerts on operational → degraded / unreachable transitions. use tokio::task::JoinHandle; use tracing::{info, warn}; use pom::alerts::Alerter; use pom::checks::scan_pipeline; use pom::config::Config; use pom::db; use super::{CheckInterval, configured_targets}; pub(crate) fn spawn_scan_pipeline_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) { let Some(sp_config) = target_config.scan_pipeline else { continue; }; let name = name.clone(); let label = target_config.label.clone(); let alerter = alerter.cloned(); let pool = pool.clone(); let cancel = cancel.clone(); info!( "{name}: scan-pipeline check every {}s against {}", sp_config.interval_secs, sp_config.base_url, ); handles.push(tokio::spawn(async move { let mut ticks = CheckInterval::new(sp_config.interval_secs, cancel); // Track previous status so we only fire alerts on transitions. Seed // from the ledger so a restart while the pipeline is already-degraded // doesn't re-fire: if the latest scan-pipeline alert is the degraded // (not the recovery) one, start from that non-operational status. let mut previous_status: Option = db::get_latest_alert_matching( &pool, &format!("scan_pipeline:{name}"), "scan_pipeline_%", ) .await .ok() .flatten() .filter(|a| a.alert_type == "scan_pipeline_degraded") .and_then(|a| a.to_status); while ticks.next().await { let result = scan_pipeline::check_scan_pipeline( &name, &sp_config.base_url, sp_config.timeout_secs, ) .await; if result.issues.is_empty() && result.error.is_none() { info!( "{name}: scan pipeline operational (queue p={}/r={}, held={})", result.queue_pending, result.queue_running, result.held_total ); } else { warn!( target = %name, status = %result.status, issues = ?result.issues, "scan pipeline non-operational" ); } // Persist so /status.json can render the latest result from the // ledger rather than re-probing on every viewer poll. if let Err(e) = db::insert_scan_pipeline_check(&pool, &result).await { tracing::error!("{name}: failed to store scan pipeline check: {e}"); } // Fire alerts on status transitions only. if let Some(ref alerter) = alerter { let prev_ok = previous_status .as_deref() .is_none_or(|s| s == "operational"); let now_ok = result.status == "operational"; if prev_ok && !now_ok { alerter .send_scan_pipeline_alert(&name, &label, &result.status, &result.issues) .await; } else if !prev_ok && now_ok { alerter.send_scan_pipeline_recovery(&name, &label).await; } } previous_status = Some(result.status); } })); } handles }