use std::error::Error;
use std::sync::Arc;
use liminal::durability::bridge::block_on;
use liminal::durability::{DurableStore, open_ephemeral};
use liminal_protocol::lifecycle::ImmutableSequenceCandidate;
use liminal_protocol::wire::{
AttachAttemptToken, ClientRequest, ConnectionIncarnation, CredentialAttachRequest,
EnrollmentRequest, EnrollmentToken, Generation, RecordAdmission, RecordAdmissionAttemptToken,
ServerValue,
};
use crate::server::participant::{
ConnectionFateClass, ConnectionFateWorkItem, ParticipantConnectionConversations,
ParticipantSemanticError, ParticipantSemanticHandler,
};
use super::ProductionParticipantHandler;
use super::log::{DecodedStoredOperation, OperationLog, StoredOperation};
use super::state::ConversationAuthority;
use super::tests::{dispatch, dispatch_tracked, test_participant_config};
use super::tests_marker_ack_fixture::marker_fixture_config;
use super::tests_w1b_pending_died_restart::{PendingRestartFixture, pending_restart_fixture};
const BOOT_OPEN_SEQUENCE: u64 = 307;
fn boot_over(
fixture: &PendingRestartFixture,
) -> Result<ProductionParticipantHandler, ParticipantSemanticError> {
let mut config = test_participant_config();
config.max_retained_record_rows = 4;
ProductionParticipantHandler::new(Arc::clone(&fixture.handler.store), config)
}
fn installed_lane(
handler: &ProductionParticipantHandler,
conversation_id: u64,
) -> Result<Vec<ImmutableSequenceCandidate>, Box<dyn Error>> {
let cell = handler.cell(conversation_id)?;
let owner = cell
.lock()
.map_err(|_| "boot-drain conversation owner lock was poisoned")?;
let lane = owner
.as_ref()
.and_then(ConversationAuthority::frontier)
.map(|frontier| {
frontier
.frontiers()
.sequence()
.immutable_candidates()
.to_vec()
})
.unwrap_or_default();
drop(owner);
Ok(lane)
}
fn operation_rows(
store: &Arc<dyn DurableStore>,
conversation_id: u64,
) -> Result<Vec<StoredOperation>, Box<dyn Error>> {
let log = OperationLog::new(Arc::clone(store), conversation_id);
let mut rows = Vec::new();
let mut sequence = 0;
while let Some(entry) = block_on(log.read_at(sequence))?? {
let DecodedStoredOperation::V3(operation) = entry.operation else {
return Err(format!("conversation {conversation_id} row {sequence} is not v3").into());
};
rows.push(operation);
sequence = sequence
.checked_add(1)
.ok_or("durable log sequence overflowed")?;
}
Ok(rows)
}
fn recovery_work_item(fixture: &PendingRestartFixture) -> ConnectionFateWorkItem {
ConnectionFateWorkItem {
open_sequence: BOOT_OPEN_SEQUENCE,
connection_incarnation: fixture.peer_connection,
class: ConnectionFateClass::ConnectionLost,
tracked_conversations: vec![fixture.conversation_id],
}
}
#[test]
fn boot_drains_a_pending_terminal_lane_and_reaches_listening() -> Result<(), Box<dyn Error>> {
let fixture = pending_restart_fixture()?;
let booted = boot_over(&fixture)?;
let lane = installed_lane(&booted, fixture.conversation_id)?;
if !lane.is_empty() {
return Err(format!("boot left the restored candidate lane occupied: {lane:?}").into());
}
let replayed = booted.replay_aggregate_reference(fixture.conversation_id, &fixture.log)?;
let durable_lane = replayed.frontier().map_or_else(Vec::new, |frontier| {
frontier
.frontiers()
.sequence()
.immutable_candidates()
.to_vec()
});
if !durable_lane.is_empty() {
return Err(format!("the boot drain did not survive replay: {durable_lane:?}").into());
}
let consumer: &dyn ParticipantSemanticHandler = &booted;
consumer
.handle_connection_fate(recovery_work_item(&fixture))
.map_err(|error| {
format!("the retained Open failed before Complete at the recovery consumer: {error}")
})?;
Ok(())
}
const MARKER_CONVERSATION: u64 = 4242;
fn two_marker_store() -> Result<Arc<dyn DurableStore>, Box<dyn Error>> {
let store: Arc<dyn DurableStore> = Arc::new(open_ephemeral(1)?);
let handler = ProductionParticipantHandler::new(Arc::clone(&store), marker_fixture_config())?;
let first_connection = ConnectionIncarnation::new(0xB7, 1);
let second_connection = ConnectionIncarnation::new(0xB7, 2);
let ServerValue::EnrollBound(first) = dispatch(
&handler,
first_connection,
ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: MARKER_CONVERSATION,
enrollment_token: EnrollmentToken::new([0xB1; 16]),
}),
)?
else {
return Err("the two-marker fixture's first member did not enroll".into());
};
let ServerValue::EnrollBound(_second) = dispatch(
&handler,
second_connection,
ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: MARKER_CONVERSATION,
enrollment_token: EnrollmentToken::new([0xB2; 16]),
}),
)?
else {
return Err("the two-marker fixture's second member did not enroll".into());
};
let mut conversations = ParticipantConnectionConversations::default();
let attached = dispatch_tracked(
&handler,
first_connection,
&mut conversations,
ClientRequest::CredentialAttach(CredentialAttachRequest {
conversation_id: MARKER_CONVERSATION,
participant_id: first.participant_id(),
capability_generation: Generation::ONE,
attach_secret: first.attach_secret(),
attach_attempt_token: AttachAttemptToken::new([0xB5; 16]),
accept_marker_delivery_seq: None,
}),
)?;
let ServerValue::AttachBound(attached) = attached else {
return Err(format!("the two-marker fixture did not attach: {attached:?}").into());
};
let committed = dispatch_tracked(
&handler,
first_connection,
&mut conversations,
ClientRequest::RecordAdmission(RecordAdmission {
conversation_id: MARKER_CONVERSATION,
participant_id: first.participant_id(),
capability_generation: attached.origin_binding_epoch().capability_generation,
record_admission_attempt_token: RecordAdmissionAttemptToken::new([0xB9; 16]),
payload: vec![0xBA],
}),
)?;
if !matches!(committed, ServerValue::RecordCommitted(_)) {
return Err(
format!("the two-marker fixture's record did not commit: {committed:?}").into(),
);
}
let lane = installed_lane(&handler, MARKER_CONVERSATION)?;
let [
ImmutableSequenceCandidate::Marker(_),
ImmutableSequenceCandidate::Marker(_),
] = lane.as_slice()
else {
return Err(format!(
"the two-marker fixture no longer mints exactly two marker candidates: {lane:?}"
)
.into());
};
drop(handler);
Ok(store)
}
#[test]
fn boot_empties_a_two_marker_lane_in_two_drains() -> Result<(), Box<dyn Error>> {
let store = two_marker_store()?;
let before = operation_rows(&store, MARKER_CONVERSATION)?;
let drains_before = marker_drain_rows(&before);
let booted = ProductionParticipantHandler::new(Arc::clone(&store), marker_fixture_config())?;
let lane = installed_lane(&booted, MARKER_CONVERSATION)?;
if !lane.is_empty() {
return Err(format!("boot left the restored marker lane occupied: {lane:?}").into());
}
let after = operation_rows(&store, MARKER_CONVERSATION)?;
let drains = marker_drain_rows(&after)
.checked_sub(drains_before)
.ok_or("the boot drain removed durable marker-drain rows")?;
if drains != 2 {
return Err(format!("boot emptied the two-marker lane in {drains} drains, not two").into());
}
Ok(())
}
fn marker_drain_rows(rows: &[StoredOperation]) -> usize {
rows.iter()
.filter(|operation| matches!(**operation, StoredOperation::MarkerDrained { .. }))
.count()
}