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

nordisk/edda

crates/nornir-build-thing/examples/name_collision_recovery.rs

//! **The crash-recovery proof, on a live engine, across two processes.**
//!
//! An interrupted build leaves its container behind. Its name is deterministic
//! (`draupnir-<job>`), so the NEXT run of that job wants the same name — and
//! before draupnir 0.1.14 the box was wedged from that moment on: every ISO
//! compose died 381 s in with the daemon's `the container name "…" is already in
//! use by …`, four layers below the user, and only a human with `podman rm -f`
//! could clear it (measured, gunnar appliance bring-up 2026-09-20).
//!
//! It runs in two modes, deliberately in two SEPARATE processes, because that is
//! the whole point: the run that leaves the wreckage is the run that was killed,
//! and a killed process runs no destructor. Recovery has to belong to the next
//! start.
//!
//! ```text
//! cargo run -p nornir-build-thing --features cargo-container \
//!   --example name_collision_recovery -- wreck <image> [job]   # then kill nothing, just leave it
//! cargo run ... --example name_collision_recovery -- recover <image> [job]
//! ```
//!
//! `wreck` leaves a stopped container called `draupnir-<job>` behind.
//! `recover` drives the REAL [`DraupnirCargoRunner`] at that same job twice:
//! over the stopped leftover (must be reaped, announced, and the build must run)
//! and over a LIVE container of the same name (must be refused by name, and the
//! live one must still be there afterwards — it is somebody else's build).
use draupnir::container::{ContainerBoot, ContainerControl, ContainerState};
use draupnir::{Boot, BootSpec};
use nornir_build_thing::cargo_container::{
    CargoContainerRunner, CargoRunRequest, DraupnirCargoRunner,
};
use std::time::Duration;

fn request(name: &str, image: &str) -> CargoRunRequest {
    CargoRunRequest {
        name: name.to_string(),
        image: image.to_string(),
        script: "echo THE-BUILD-RAN".to_string(),
        binds: Vec::new(),
        timeout: Duration::from_secs(300),
        network: false,
        mem_limit_mb: 512,
        cpus: 1,
    }
}

fn held(boot: &ContainerBoot, job: &str) -> Vec<(String, ContainerState)> {
    boot.list_containers(&format!("draupnir-{job}"))
        .expect("the engine can be asked who holds a name")
}

fn main() -> anyhow::Result<()> {
    let mode = std::env::args().nth(1).expect("mode: wreck | recover");
    let image = std::env::args().nth(2).expect("an image reference to run in");
    let job = std::env::args()
        .nth(3)
        .unwrap_or_else(|| "name-collision-proof".to_string());
    let boot = ContainerBoot::new();

    if mode == "wreck" {
        // Exactly what a killed ladder run leaves. The container is BOOTED and
        // then this process dies where it stands — no drain, no `stop`, no
        // destructor, which is the whole reason the cleanup at the end of
        // `drive_to_completion` cannot be the answer: the run that leaves the
        // wreckage is the run that never reaches its own last line. The container
        // finishes its command by itself a moment later and stays there, exited,
        // holding the name for ever.
        let spec = BootSpec::container(&job, &image)
            .with_cmd(["/bin/sh", "-eu", "-c", "echo wreckage"]);
        let machine = boot.boot(&spec)?;
        println!("[wreck] booted {} — now dying without cleaning up", machine.id);
        std::io::Write::flush(&mut std::io::stdout()).ok();
        // 137 is what a `pkill -9`ed build reports. Nothing below this line runs.
        std::process::exit(137);
    }
    assert_eq!(mode, "recover", "mode is `wreck` or `recover`");

    println!("[recover] before: {:?}", held(&boot, &job));
    let runner = DraupnirCargoRunner::new();
    let out = runner.run(&request(&job, &image))?;
    println!(
        "[proof 1] the build RAN over the leftover: exit={:?} stdout={:?}",
        out.exit_code, out.stdout
    );
    assert_eq!(out.exit_code, Some(0), "the next run must recover by itself");

    // --- and the other half: a LIVE container holding the name. -----------
    let live_job = format!("{job}-live");
    let live = BootSpec::container(&live_job, &image)
        .with_cmd(["/bin/sh", "-eu", "-c", "sleep 600"]);
    let live_machine = boot.boot(&live)?;
    println!("[stage] a LIVE container holds draupnir-{live_job}");

    let refused = runner
        .run(&request(&live_job, &image))
        .expect_err("a live container is a concurrent build, not wreckage");
    println!("[proof 2] {refused:#}");
    let after = held(&boot, &live_job);
    println!("[proof 2] and it is STILL running afterwards: {after:?}");
    assert!(
        matches!(after.first().map(|(_, s)| s.clone()), Some(ContainerState::Running)),
        "a refusal must never kill the other lane's build"
    );

    // Clean up only what this example started.
    boot.stop(&live_machine);
    println!("[done] both proofs held");
    Ok(())
}