synops/maskinrommet/src/stdb_monitor.rs
vegard 0f03886091 Backup: daglig PG-dump, STDB-krasj-recovery, helsesjekk (oppgave 12.2)
Tre ting implementert:

1. PG-dump rutine (scripts/backup-pg.sh):
   - Daglig cron kl. 03:00 UTC via /etc/cron.d/synops-backup
   - pg_dump -Fc (custom format, komprimert), konsistent uten nedetid
   - Rotasjon: beholder 30 dager, sletter eldre
   - Verifiserer at dump-filen er gyldig (ikke tom)

2. STDB → PG gjenoppbygging ved krasj (stdb_monitor.rs):
   - Bakgrunnsmonitor sjekker STDB hvert 30. sekund
   - Oppdager krasj (var oppe → nå nede)
   - Venter på at containeren restarter (maks 10 min)
   - Kjører warmup (PG → STDB) automatisk
   - Hele prosessen logges

3. Forbedret backup-helsesjekk (health.rs):
   - Sjekker /srv/synops/backup/pg/ for nyeste dump
   - Rapporterer ok/stale/missing i /admin/health
2026-03-18 11:11:32 +00:00

145 lines
4.6 KiB
Rust

// 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<AtomicBool>,
}
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<AtomicBool>) {
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);
}