Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/appliance-pid1/tests/quiesce_for_power_off.rs

//! **A clean power-off leaves the data disk's log CLEAN** (2026-10-01, the KVM proof of
//! test2: `XFS (vda3): Starting recovery` after a stop that froze /data). Linux 6.12's XFS
//! freeze only QUIESCES the log (`xfs_fs_freeze` → `xfs_log_quiesce`, no unmount record);
//! the record that lets the next mount skip recovery is written by a READ-ONLY REMOUNT
//! (`xfs_remount_ro` → `xfs_log_clean`). So: `sync(2)`, then this process's own writers on
//! the data mount released (pointed at /dev/null), then the remount read-only — and the
//! freeze only when the remount is refused. Each step bounded, each said.

use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use appliance_pid1::power::{quiesce_for_power_off, QuiesceOps};

#[derive(Default)]
struct Fake {
    order: Mutex<Vec<&'static str>>,
    remount_refused: bool,
    hangs: Option<&'static str>,
}

impl Fake {
    fn step(&self, s: &'static str) {
        self.order.lock().unwrap().push(s);
        if self.hangs == Some(s) {
            std::thread::sleep(Duration::from_secs(30));
        }
    }
}

impl QuiesceOps for Fake {
    fn sync_all(&self) {
        self.step("sync");
    }
    fn release_writers(&self, _data: &Path) -> usize {
        self.step("release");
        2
    }
    fn remount_readonly(&self, _data: &Path) -> std::io::Result<()> {
        self.step("remount");
        if self.remount_refused {
            Err(std::io::Error::from_raw_os_error(16))
        } else {
            Ok(())
        }
    }
    fn freeze(&self, _data: &Path) -> std::io::Result<()> {
        self.step("freeze");
        Ok(())
    }
}

fn run(f: Arc<Fake>) -> (Vec<String>, Vec<String>, Duration) {
    let said = Mutex::new(Vec::<String>::new());
    let t0 = Instant::now();
    let steps = quiesce_for_power_off(f, Path::new("/data"), Duration::from_millis(500), &|l: &str| said.lock().unwrap().push(l.to_string()));
    (steps, said.into_inner().unwrap(), t0.elapsed())
}

#[test]
fn sync_then_writers_released_then_a_read_only_remount_and_no_freeze_when_it_takes() {
    let f = Arc::new(Fake::default());
    let (steps, said, _) = run(f.clone());
    assert_eq!(*f.order.lock().unwrap(), vec!["sync", "release", "remount"], "the remount cleans the log; no freeze after it");
    assert!(steps.iter().any(|s| s.contains("read-only") && s.contains("clean")), "{steps:?}");
    assert_eq!(said, steps, "every step is said as it happens");
}

#[test]
fn a_refused_remount_falls_back_to_the_freeze_and_says_the_log_is_not_clean() {
    let f = Arc::new(Fake { remount_refused: true, ..Default::default() });
    let (steps, _, _) = run(f.clone());
    assert_eq!(*f.order.lock().unwrap(), vec!["sync", "release", "remount", "freeze"]);
    assert!(steps.iter().any(|s| s.contains("NOT remounted")), "{steps:?}");
    assert!(steps.iter().any(|s| s.contains("frozen") && s.contains("replay")), "a freeze does not clean the log, and says so: {steps:?}");
}

#[test]
fn a_step_that_hangs_does_not_hold_the_power_off() {
    let f = Arc::new(Fake { hangs: Some("remount"), ..Default::default() });
    let (steps, _, took) = run(f);
    assert!(took < Duration::from_secs(5), "held {took:?}");
    assert!(steps.iter().any(|s| s.contains("not back within")), "{steps:?}");
}