aion-server 0.26.0

Aion workflow server library: HTTP, gRPC, WebSocket, and worker endpoints. Run it with the `aion` binary from the aion-cli crate.
Documentation
//! Red-first specimens for the guard: the bind-time fill, the
//! never-overwrite-a-successor rule, and the identity compare at exit.

use super::RecordUpdate;
use crate::control::claim_at_birth;
use crate::control::pid_file::{IncarnationState, read};
use crate::control::test_records::{
    GRPC, HTTP, TestResult, intended, own_birth_record, own_serving_record, plant,
};

/// The bind-time fill: addresses in, drain window in, state moved out of
/// BOOTING, and — the easily-forgotten half — the stage CLEARED, so a served
/// server never reports itself as mid-boot forever.
#[test]
fn the_bind_fill_records_the_addresses_and_moves_to_serving() -> TestResult {
    let home = tempfile::tempdir()?;
    let guard = claim_at_birth(home.path(), &own_birth_record()?, intended()?)?;
    guard
        .stage_reporter()
        .report("store-open", "opening the store".to_owned());

    let update = guard.update_own(|record| {
        record.state = IncarnationState::Serving;
        record.http_address = HTTP.parse().ok();
        record.grpc_address = GRPC.parse().ok();
        record.drain_timeout_seconds = 30;
        record.stage = None;
        record.stage_detail = None;
    })?;
    assert_eq!(update, RecordUpdate::Written);

    let written = read(home.path())?.ok_or("the filled record must be readable")?;
    assert_eq!(written.state, IncarnationState::Serving);
    assert_eq!(written.http_address, Some(HTTP.parse()?));
    assert_eq!(written.grpc_address, Some(GRPC.parse()?));
    assert_eq!(written.drain_timeout_seconds, 30);
    assert_eq!(written.stage, None, "a serving server is not mid-boot");
    // The guard's own copy is the one on disk, so its exit compare and every
    // reader of `record()` see the same thing.
    assert_eq!(guard.record()?, written);
    Ok(())
}

/// 🔴 A record replaced by a successor is never overwritten.
///
/// The guard writes many times during a life (stages, the bind fill, the
/// drain flip). If any of those writes could land after a successor claimed
/// the home, a dying server would silently take the live one's address away.
/// The update reports `NotOurs` and touches nothing.
#[test]
fn an_update_never_overwrites_a_successors_record() -> TestResult {
    let home = tempfile::tempdir()?;
    let guard = claim_at_birth(home.path(), &own_birth_record()?, intended()?)?;

    // A successor's record: same live process (the only live incarnation a
    // test can mint), but a different incarnation identity.
    let mut successor = own_serving_record(None, None)?;
    successor.started_at_unix_secs = successor.started_at_unix_secs.wrapping_add(97);
    successor.commit = "successor".to_owned();
    plant(home.path(), &successor)?;

    let update = guard.update_own(|record| {
        record.state = IncarnationState::Draining;
    })?;
    assert_eq!(
        update,
        RecordUpdate::NotOurs,
        "a foreign record must be reported, not written over"
    );
    assert_eq!(
        read(home.path())?.as_ref(),
        Some(&successor),
        "the successor's record must survive untouched"
    );

    // And the exit compare agrees: the lingering guard leaves it alone.
    drop(guard);
    assert_eq!(read(home.path())?.as_ref(), Some(&successor));
    Ok(())
}

/// 🔴 The identity-compare fix at the exit seam.
///
/// The guard's `Drop` used to compare WHOLE RECORDS against the copy it wrote
/// at claim time. Every stage write moves the file away from that copy, so a
/// server that reported any progress at all would leave its own record behind
/// on a clean exit — and the next boot would find live-looking debris.
///
/// The specimen churns the stage through the real reporter (not by hand, and
/// not built from the fix), then drops the guard and asserts the file is gone.
#[test]
fn the_guard_removes_its_own_record_despite_stage_churn() -> TestResult {
    let home = tempfile::tempdir()?;
    let guard = claim_at_birth(home.path(), &own_birth_record()?, intended()?)?;
    let claimed = guard.record()?;

    let reporter = guard.stage_reporter();
    for shard in 1..=4 {
        reporter.report("wal-recovery", format!("materializing shard {shard} of 4"));
    }
    let churned = read(home.path())?.ok_or("the churned record must be readable")?;
    assert_ne!(
        churned, claimed,
        "the specimen only means something if the record genuinely moved"
    );
    assert!(churned.is_same_incarnation(&claimed));

    drop(guard);
    assert_eq!(
        read(home.path())?,
        None,
        "a guard must remove its own record however far its stage has moved"
    );
    Ok(())
}

/// An unclaimed boot's guard owns nothing: its updates are no-ops that report
/// themselves, and its exit removes nothing.
#[test]
fn an_unclaimed_guard_writes_and_removes_nothing() -> TestResult {
    let home = tempfile::tempdir()?;
    let holder = own_serving_record(
        Some("127.0.0.1:19090".parse()?),
        Some("127.0.0.1:19091".parse()?),
    )?;
    plant(home.path(), &holder)?;

    let guard = claim_at_birth(home.path(), &own_birth_record()?, intended()?)?;
    assert!(!guard.holds_claim());
    assert_eq!(
        guard.update_own(|record| record.state = IncarnationState::Serving)?,
        RecordUpdate::Unclaimed
    );
    assert_eq!(read(home.path())?.as_ref(), Some(&holder));
    drop(guard);
    assert_eq!(read(home.path())?.as_ref(), Some(&holder));
    Ok(())
}

/// A record removed out from under a live holder is reported and NOT
/// recreated: the disappearance is a fact an operator needs to see, and a
/// silently re-minted record would erase the only evidence of it.
#[test]
fn an_update_after_the_record_disappears_reports_rather_than_recreating() -> TestResult {
    let home = tempfile::tempdir()?;
    let guard = claim_at_birth(home.path(), &own_birth_record()?, intended()?)?;
    std::fs::remove_file(crate::control::pid_file::pid_file_path(home.path()))?;

    assert_eq!(
        guard.update_own(|record| record.state = IncarnationState::Serving)?,
        RecordUpdate::NotOurs
    );
    assert_eq!(
        read(home.path())?,
        None,
        "the disappearance must stay visible"
    );
    Ok(())
}