use std::error::Error;
use std::sync::Arc;
use std::time::Instant;
use liminal::durability::bridge::block_on;
use liminal::durability::{DurableStore, open_ephemeral};
use liminal_protocol::wire::{
ClientRequest, ConnectionIncarnation, EnrollmentRequest, EnrollmentToken, Generation,
RecordAdmission, RecordAdmissionAttemptToken, ServerValue,
};
use super::ProductionParticipantHandler;
use super::operation_batch::CHECKPOINT_EVERY;
use super::restore_checkpoint_locator::{CheckpointLocator, checkpoint_conversation_route};
use super::tests::{dispatch, test_participant_config};
use super::tests_store_tally::{Asked, Tally, under};
type TestResult = Result<(), Box<dyn Error>>;
pub(super) fn enrolled(
handler: &ProductionParticipantHandler,
conversation_id: u64,
) -> Result<(ConnectionIncarnation, u64), Box<dyn Error>> {
let sender = ConnectionIncarnation::new(conversation_id, 1);
let mut tokens = [[0_u8; 16]; 2];
tokens[0][..8].copy_from_slice(&conversation_id.to_be_bytes());
tokens[1] = tokens[0];
tokens[1][15] = 1;
let bound = dispatch(
handler,
sender,
ClientRequest::Enrollment(EnrollmentRequest {
conversation_id,
enrollment_token: EnrollmentToken::new(tokens[0]),
}),
)?;
let ServerValue::EnrollBound(bound) = bound else {
return Err(format!("enrollment did not bind: {bound:?}").into());
};
let recipient = dispatch(
handler,
ConnectionIncarnation::new(conversation_id, 2),
ClientRequest::Enrollment(EnrollmentRequest {
conversation_id,
enrollment_token: EnrollmentToken::new(tokens[1]),
}),
)?;
if !matches!(recipient, ServerValue::EnrollBound(_)) {
return Err(format!("the recipient did not bind: {recipient:?}").into());
}
Ok((sender, bound.participant_id()))
}
pub(super) fn admit(
handler: &ProductionParticipantHandler,
conversation_id: u64,
member: (ConnectionIncarnation, u64),
count: u64,
) -> TestResult {
admit_range(handler, conversation_id, member, 0..count)
}
fn admit_range(
handler: &ProductionParticipantHandler,
conversation_id: u64,
(connection, participant_id): (ConnectionIncarnation, u64),
operations: std::ops::Range<u64>,
) -> TestResult {
for operation in operations {
let mut token = [0; 16];
token[..8].copy_from_slice(&conversation_id.to_be_bytes());
token[8..].copy_from_slice(&operation.to_be_bytes());
let answer = dispatch(
handler,
connection,
ClientRequest::RecordAdmission(RecordAdmission {
conversation_id,
participant_id,
capability_generation: Generation::ONE,
record_admission_attempt_token: RecordAdmissionAttemptToken::new(token),
payload: operation.to_be_bytes().to_vec(),
}),
)?;
if !matches!(answer, ServerValue::RecordCommitted(_)) {
return Err(format!(
"record {operation} on conversation {conversation_id} answered {answer:?}"
)
.into());
}
}
Ok(())
}
pub(super) fn own_locator(
store: &dyn DurableStore,
conversation_id: u64,
) -> Result<CheckpointLocator, Box<dyn Error>> {
let bytes = block_on(store.get_routed(
&checkpoint_conversation_route(conversation_id),
&CheckpointLocator::routed_key(conversation_id),
))??
.ok_or_else(|| format!("conversation {conversation_id} has no checkpoint locator"))?;
Ok(CheckpointLocator::decode_canonical(
&bytes,
conversation_id,
)?)
}
#[test]
fn two_conversations_on_one_shard_keep_their_own_checkpoints() -> TestResult {
let store: Arc<dyn DurableStore> = Arc::new(open_ephemeral(1)?);
let handler = ProductionParticipantHandler::new(Arc::clone(&store), test_participant_config())?;
let first = enrolled(&handler, 10)?;
let second = enrolled(&handler, 11)?;
admit(&handler, 10, first, 260)?;
admit(&handler, 11, second, 260)?;
assert!(own_locator(store.as_ref(), 10)?.covered_sequence() >= 255);
assert!(own_locator(store.as_ref(), 11)?.covered_sequence() >= 255);
Ok(())
}
#[test]
fn cutting_one_conversations_windows_leaves_the_other_conversations_keys() -> TestResult {
let store: Arc<dyn DurableStore> = Arc::new(open_ephemeral(1)?);
let rows = |id: u64| {
vec![
CheckpointLocator::routed_key(id),
super::route_keys::namespaced(id, b"checkpoint/body/v1/x"),
]
};
for id in [10, 11] {
let items = rows(id)
.into_iter()
.map(|key| liminal::durability::RoutedBatchItem {
key,
expected: None,
value: vec![1],
ttl: None,
})
.collect();
block_on(store.apply_routed_batch(&checkpoint_conversation_route(id), items))??;
}
let route = checkpoint_conversation_route(10);
for (from, to) in super::route_keys::conversation_windows(10, &[])? {
block_on(store.remove_routed_range(&route, &from, &to))??;
}
let left: Vec<Vec<u8>> = block_on(store.scan_routed(&route, &[], &[0xff]))??
.into_iter()
.map(|(key, _)| key)
.collect();
let mut kept = rows(11);
kept.sort();
assert_eq!(left, kept);
Ok(())
}
#[test]
fn three_checkpoint_periods_leave_one_body_and_its_parts_under_the_namespace() -> TestResult {
const TABLE: &str = "checkpoint_crossing";
let tally = Tally::new(Arc::new(open_ephemeral(1)?));
let handler = ProductionParticipantHandler::new(tally.shared(), test_participant_config())?;
let member = enrolled(&handler, 10)?;
let mut covered = 0;
let mut crossings: Vec<Asked> = Vec::new();
let mut ordinary = Asked::default();
let started = Instant::now();
tally.take()?;
for operation in 0..(3 * CHECKPOINT_EVERY + 64) {
admit_range(&handler, 10, member, operation..operation + 1)?;
let asked = tally.take()?;
let now =
own_locator(tally.inner.as_ref(), 10).map_or(0, CheckpointLocator::covered_sequence);
if now > covered {
covered = now;
crossings.push(asked);
if crossings.len() == 3 {
break;
}
} else {
ordinary.get_routed_calls = ordinary.get_routed_calls.max(asked.get_routed_calls);
ordinary.commits = ordinary.commits.max(asked.commits);
}
}
println!(
"three periods in {} ms; an ordinary record asks at most {} routed reads and {} commits; crossings {crossings:?}",
started.elapsed().as_millis(),
ordinary.get_routed_calls,
ordinary.commits
);
let [first, second, third] = crossings.as_slice() else {
return Err(format!("{} crossings, not three", crossings.len()).into());
};
for crossing in [first, second, third] {
assert_eq!(
crossing.apply_routed_batch_calls, 1,
"one routed batch per crossing: {crossing:?}"
);
assert_eq!(crossing.scan_routed_calls, 0, "{crossing:?}");
}
println!(
"crossing routed reads: first {} second {} third {} (a period of identities, each walking an index path whose depth grows with the log of the conversation's identities)",
first.get_routed_calls, second.get_routed_calls, third.get_routed_calls
);
under(TABLE, "get_routed_calls", third.get_routed_calls)?;
under(
TABLE,
"remove_routed_range_calls",
third.remove_routed_range_calls,
)?;
let route = checkpoint_conversation_route(10);
let (from, to) = super::route_keys::conversation_window(10);
let keys: Vec<Vec<u8>> = block_on(tally.inner.scan_routed(&route, &from, &to))??
.into_iter()
.map(|(key, _)| key)
.collect();
let bodies = keys
.iter()
.filter(|key| key.starts_with(&super::route_keys::namespaced(10, b"checkpoint/body/v1/")))
.count();
let parts = keys
.iter()
.filter(|key| {
key.starts_with(&super::route_keys::namespaced(
10,
b"checkpoint/body/part/v1/",
))
})
.count();
let locator = own_locator(tally.inner.as_ref(), 10)?;
let body = super::checkpoint_body_parts::read_body_for_tests(
tally.inner.as_ref(),
10,
locator.body_key(),
)?
.ok_or("the locator names no body")?;
assert_eq!(bodies, 1, "bodies under the namespace after three periods");
assert_eq!(
parts,
super::checkpoint_body_parts::lay_out(10, body).parts.len(),
"parts under the namespace are the manifest's"
);
Ok(())
}