liminal-protocol 0.2.0

Shared participant-lifecycle protocol types for liminal
Documentation
#![allow(clippy::panic)]

use crate::outcome::CandidatePhase;
use crate::wire::{
    BindingEpoch, CloseCause, ConnectionIncarnation, DetachedCause, DiedCause, Generation,
};

use super::binding::{
    ActiveBinding, AdmissionOrder, BindingState, BindingTerminalDisposition, BindingTerminalKind,
    CommittedBindingTerminal, CommittedBindingTerminalPosition, DetachedBindingTransition,
    DiedBindingTransition, PendingBindingTerminalPosition, PendingFinalization,
};

fn binding() -> ActiveBinding {
    ActiveBinding {
        participant_id: 3,
        conversation_id: 7,
        binding_epoch: BindingEpoch::new(ConnectionIncarnation::new(11, 13), Generation::ONE),
    }
}

fn committed() -> BindingTerminalDisposition {
    BindingTerminalDisposition::Committed(CommittedBindingTerminalPosition::new(17, 19))
}

fn pending() -> BindingTerminalDisposition {
    BindingTerminalDisposition::Pending(PendingBindingTerminalPosition::new(17))
}

#[test]
fn binding_terminal_order_fixes_phase_and_derives_index_from_id() {
    let order = AdmissionOrder::binding_terminal(17, binding().participant_id);
    assert_eq!(order.transaction_order(), 17);
    assert_eq!(order.candidate_phase(), CandidatePhase::BindingTerminal);
    assert_eq!(order.participant_index(), binding().participant_id);
}

#[test]
fn detached_fates_have_only_detached_causes() {
    let DetachedBindingTransition::Committed(clean) = binding().clean_disconnect(committed())
    else {
        panic!("committed placement must append the clean terminal");
    };
    assert_eq!(clean.cause(), DetachedCause::CleanDeregister);
    assert_eq!(clean.delivery_seq(), 19);
    assert_eq!(clean.admission_order().participant_index(), 3);

    let superseded = binding().superseded(CommittedBindingTerminalPosition::new(23, 29));
    assert_eq!(superseded.cause(), DetachedCause::Superseded);
    assert_eq!(superseded.delivery_seq(), 29);

    let DetachedBindingTransition::Pending(shutdown) = binding().server_shutdown(pending()) else {
        panic!("pending placement must preserve the shutdown terminal");
    };
    assert_eq!(shutdown.cause(), DetachedCause::ServerShutdown);
    let state = DetachedBindingTransition::Pending(shutdown).binding_state();
    assert_eq!(
        state,
        BindingState::PendingFinalization(PendingFinalization::Detached(shutdown))
    );
}

#[test]
fn died_fates_have_only_died_causes() {
    let DiedBindingTransition::Committed(connection_lost) = binding().connection_lost(committed())
    else {
        panic!("committed placement must append the Died terminal");
    };
    assert_eq!(connection_lost.cause(), DiedCause::ConnectionLost);

    let DiedBindingTransition::Committed(process_killed) = binding().process_killed(committed())
    else {
        panic!("committed placement must append the Died terminal");
    };
    assert_eq!(process_killed.cause(), DiedCause::ProcessKilled);

    let DiedBindingTransition::Pending(protocol_error) = binding().protocol_error(pending()) else {
        panic!("pending placement must preserve the Died terminal");
    };
    assert_eq!(protocol_error.cause(), DiedCause::ProtocolError);
}

#[test]
fn pending_commit_preserves_identity_order_and_cause() {
    let DiedBindingTransition::Pending(pending_terminal) = binding().connection_lost(pending())
    else {
        panic!("fixture selects pending finalization");
    };
    let pending = PendingFinalization::Died(pending_terminal);
    let committed = pending.commit(31);

    assert_eq!(committed.participant_id(), 3);
    assert_eq!(committed.conversation_id(), 7);
    assert_eq!(committed.binding_epoch(), binding().binding_epoch);
    assert_eq!(committed.admission_order(), pending.admission_order());
    assert_eq!(committed.delivery_seq(), 31);
    assert_eq!(committed.kind(), BindingTerminalKind::Died);
    assert_eq!(committed.close_cause(), CloseCause::ConnectionLost);
    assert_eq!(committed.died_cause(), Some(DiedCause::ConnectionLost));
    assert_eq!(committed.detached_cause(), None);
    assert!(matches!(committed, CommittedBindingTerminal::Died(_)));
}

#[test]
fn restart_cause_is_derived_from_old_binding_epoch() {
    let DiedBindingTransition::Pending(restart) = binding().unclean_server_restart(pending())
    else {
        panic!("fixture selects pending startup recovery");
    };
    assert_eq!(
        restart.cause(),
        DiedCause::UncleanServerRestart {
            prior_server_incarnation: 11,
        }
    );
    assert_eq!(
        PendingFinalization::Died(restart).close_cause(),
        CloseCause::UncleanServerRestart {
            prior_server_incarnation: 11,
        }
    );
}