use std::error::Error;
use std::sync::Arc;
use liminal_protocol::lifecycle::BindingState;
use liminal_protocol::wire::{
ClientRequest, ConnectionIncarnation, LeaveAttemptToken, LeaveRequest, ServerValue,
};
use crate::server::participant::ParticipantConnectionConversations;
use super::ProductionParticipantHandler;
use super::log::OperationLog;
use super::state::ConversationAuthority;
use super::tests::{dispatch_tracked, test_participant_config};
use super::tests_pending_died_live_reachability::{
CONVERSATION, LiveResidence, build_residence_with_retention, live_binding_state,
};
const HEADROOM_RETAINED_ROWS: u64 = 16;
const fn victim_leave(residence: &LiveResidence, token_byte: u8) -> LeaveRequest {
LeaveRequest {
conversation_id: CONVERSATION,
participant_id: residence.victim_id,
capability_generation: residence.victim_epoch.capability_generation,
attach_secret: residence.victim_attach_secret,
leave_attempt_token: LeaveAttemptToken::new([token_byte; 16]),
}
}
fn send_leave(
handler: &ProductionParticipantHandler,
connection: ConnectionIncarnation,
request: LeaveRequest,
) -> Result<ServerValue, Box<dyn Error>> {
dispatch_tracked(
handler,
connection,
&mut ParticipantConnectionConversations::default(),
ClientRequest::Leave(request),
)
}
fn assert_leave_committed(outcome: &Result<ServerValue, Box<dyn Error>>, context: &str) {
let rendered = match outcome {
Ok(answer) => format!("answered {answer:?}"),
Err(error) => error.to_string(),
};
assert!(
matches!(outcome, Ok(ServerValue::LeaveCommitted(_))),
"{context}: the detached victim's ordinary Leave did not commit: {rendered}"
);
}
fn killed_detached_residence() -> Result<LiveResidence, Box<dyn Error>> {
let residence = build_residence_with_retention(true, HEADROOM_RETAINED_ROWS)?;
let binding = live_binding_state(&residence)?;
if !matches!(binding, Some(BindingState::Detached)) {
return Err(
format!("the killed headroom fixture did not rest Detached: {binding:?}").into(),
);
}
Ok(residence)
}
fn victim_state(
authority: &ConversationAuthority,
participant_id: u64,
) -> Result<String, Box<dyn Error>> {
let slot = authority
.slots
.get(&participant_id)
.ok_or("the victim's slot is absent")?;
let frontier = authority
.frontier()
.ok_or("the frontier owner is taken")?
.frontiers()
.active_identities()
.participants()
.iter()
.find(|participant| participant.participant_index() == participant_id)
.ok_or("the victim is absent from the frontier")?
.cursor();
Ok(format!(
"binding {:?}, member cursor {}, frontier cursor {frontier}, log head {}",
slot.binding,
slot.member.cursor(),
authority.next_log_sequence
))
}
fn live_victim_state(residence: &LiveResidence) -> Result<String, Box<dyn Error>> {
let cell = residence.handler.cell(CONVERSATION)?;
let owner = cell
.lock()
.map_err(|_| "detached-leave conversation lock was poisoned")?;
let authority = owner
.as_ref()
.ok_or("detached-leave conversation owner was discarded")?;
let state = victim_state(authority, residence.victim_id);
drop(owner);
state
}
#[test]
fn a_killed_then_detached_victims_ordinary_leave_commits() -> Result<(), Box<dyn Error>> {
let residence = killed_detached_residence()?;
let outcome = send_leave(
&residence.handler,
ConnectionIncarnation::new(511, 22),
victim_leave(&residence, 0xF5),
);
assert_leave_committed(&outcome, "live");
Ok(())
}
#[test]
fn replaying_the_stored_ordinary_fate_restores_the_live_state_and_the_leave_commits()
-> Result<(), Box<dyn Error>> {
let residence = killed_detached_residence()?;
let live = live_victim_state(&residence)?;
let log = OperationLog::new(Arc::clone(&residence.store), CONVERSATION);
let replayed = residence
.handler
.replay_aggregate_reference(CONVERSATION, &log)?;
assert_eq!(
victim_state(&replayed, residence.victim_id)?,
live,
"replay of the stored fate disagrees with the live owner"
);
let outcome = send_leave(
&residence.handler,
ConnectionIncarnation::new(511, 23),
victim_leave(&residence, 0xF6),
);
assert_leave_committed(&outcome, "after replay");
Ok(())
}
#[test]
fn after_a_reboot_the_detached_victims_ordinary_leave_commits() -> Result<(), Box<dyn Error>> {
let residence = killed_detached_residence()?;
let live = live_victim_state(&residence)?;
let victim_id = residence.victim_id;
let request = victim_leave(&residence, 0xF7);
let LiveResidence { store, handler, .. } = residence;
drop(handler);
let mut config = test_participant_config();
config.max_retained_record_rows = HEADROOM_RETAINED_ROWS;
let rebooted = ProductionParticipantHandler::new(Arc::clone(&store), config)?;
let log = OperationLog::new(Arc::clone(&store), CONVERSATION);
let booted = rebooted.replay_and_repair(CONVERSATION, &log)?;
assert_eq!(
victim_state(&booted, victim_id)?,
live,
"the boot replay disagrees with the live owner"
);
drop(booted);
let outcome = send_leave(&rebooted, ConnectionIncarnation::new(512, 1), request);
assert_leave_committed(&outcome, "after a reboot");
Ok(())
}