// STDB-overvåker: oppdager SpacetimeDB-krasj og gjenoppbygger fra PG. // // Kjører i bakgrunnen med jevnlig helsesjekk. Hvis STDB var oppe og // deretter feiler, kjøres warmup automatisk for å gjenoppbygge tilstand. // // Sekvens ved krasj: // 1. Oppdage at STDB er nede (helsesjekk feiler) // 2. Vente til STDB er tilbake (container restarter) // 3. Kjøre warmup (PG → STDB) // 4. Logge hendelsen // // Ref: docs/infra/backup.md, docs/infra/synkronisering.md use sqlx::PgPool; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use crate::stdb::StdbClient; /// Start STDB-overvåker i bakgrunnen. /// Sjekker STDB-helse hvert 30. sekund og kjører warmup ved krasj. pub fn start_stdb_monitor(db: PgPool, stdb: StdbClient) { tokio::spawn(async move { monitor_loop(db, stdb).await; }); } /// Intern tilstand for overvåkeren. struct MonitorState { /// Var STDB oppe ved forrige sjekk? was_up: bool, /// Pågår det en recovery akkurat nå? recovering: Arc, } async fn monitor_loop(db: PgPool, stdb: StdbClient) { let mut state = MonitorState { was_up: true, // Antar oppe etter warmup ved oppstart recovering: Arc::new(AtomicBool::new(false)), }; // Vent litt etter oppstart slik at warmup fullføres først tokio::time::sleep(std::time::Duration::from_secs(60)).await; tracing::info!("STDB-overvåker startet (sjekker hvert 30s)"); let mut interval = tokio::time::interval(std::time::Duration::from_secs(30)); loop { interval.tick().await; // Ikke sjekk hvis recovery allerede pågår if state.recovering.load(Ordering::Relaxed) { continue; } let is_up = check_stdb_health(&stdb).await; match (state.was_up, is_up) { (true, false) => { // STDB gikk ned! Logg og start recovery-venting. tracing::error!("STDB-overvåker: SpacetimeDB er NEDE — starter recovery-prosess"); state.recovering.store(true, Ordering::Relaxed); let db_clone = db.clone(); let stdb_clone = stdb.clone(); let recovering = state.recovering.clone(); tokio::spawn(async move { recover_stdb(db_clone, stdb_clone, recovering).await; }); } (false, true) => { // STDB kom tilbake uten vår hjelp (recovery-tasken fikset det) tracing::info!("STDB-overvåker: SpacetimeDB er tilbake"); state.was_up = true; } (false, false) => { // Fortsatt nede — recovery-tasken håndterer dette } (true, true) => { // Alt OK } } if is_up { state.was_up = true; } else if !state.recovering.load(Ordering::Relaxed) { state.was_up = false; } } } /// Sjekk om STDB svarer på en enkel reducer-kall. async fn check_stdb_health(stdb: &StdbClient) -> bool { stdb.delete_node("__healthcheck_nonexistent__").await.is_ok() } /// Vent til STDB er tilbake, deretter kjør warmup. async fn recover_stdb(db: PgPool, stdb: StdbClient, recovering: Arc) { let max_wait = std::time::Duration::from_secs(600); // Maks 10 min let check_interval = std::time::Duration::from_secs(10); let start = std::time::Instant::now(); tracing::info!("STDB-recovery: venter på at SpacetimeDB starter opp igjen (maks 10 min)"); // Vent til STDB svarer loop { if start.elapsed() > max_wait { tracing::error!( "STDB-recovery: SpacetimeDB kom ikke tilbake innen {} sekunder — gir opp", max_wait.as_secs() ); recovering.store(false, Ordering::Relaxed); return; } tokio::time::sleep(check_interval).await; if check_stdb_health(&stdb).await { tracing::info!( "STDB-recovery: SpacetimeDB svarer igjen etter {}s", start.elapsed().as_secs() ); break; } } // STDB er tilbake — kjør warmup tracing::info!("STDB-recovery: kjører warmup (PG → STDB)"); match crate::warmup::run(&db, &stdb).await { Ok(stats) => { tracing::info!( "STDB-recovery: warmup fullført ({} noder, {} edges, {} access)", stats.nodes, stats.edges, stats.access ); } Err(e) => { tracing::error!("STDB-recovery: warmup feilet: {e}"); } } recovering.store(false, Ordering::Relaxed); }