use std::collections::BTreeSet;
use std::error::Error;
use std::sync::{Arc, Barrier};
use liminal::durability::bridge::block_on;
use liminal::durability::{CountingStore, DurableStore, StoreCounts};
use liminal_protocol::wire::{
ClientRequest, ConnectionIncarnation, EnrollmentRequest, EnrollmentToken,
IdentityCapacityExceeded, IdentityCapacityScope, ServerValue,
};
use crate::server::participant::ParticipantSemanticHandler;
use super::ProductionParticipantHandler;
use super::operation_batch::OperationBatch;
use super::registry_snapshot::{RegistrySets, write_registry_snapshot};
use super::route_key_migration::fresh_marker;
use super::tests::{dispatch, test_participant_config};
use super::tests_capacity::capacity_config;
use super::tests_memory_store::MemoryStore;
use super::tests_receipts::enroll;
use super::tests_restore_from_checkpoint_boot::{
assert_checkpoint, assert_enrollment_unchanged, boot, legacy_fixture,
};
use super::tests_seed::Shared;
const REGISTRY: &str = "liminal:participant-conversation-registry";
fn registered(count: u64) -> Result<Arc<dyn DurableStore>, Box<dyn Error>> {
let store: Arc<dyn DurableStore> = Arc::new(MemoryStore::default());
let payloads = (1..=count)
.map(|conversation_id| {
serde_json::to_vec(&serde_json::json!({
"schema_version": 1,
"conversation_id": conversation_id,
}))
})
.collect::<Result<Vec<_>, _>>()?;
assert_eq!(block_on(store.append_all(REGISTRY, payloads, 0))??, count);
let ids: BTreeSet<u64> = (1..=count).collect();
write_registry_snapshot(store.as_ref(), None, count, &RegistrySets::of_live(ids))?;
let mut marker = OperationBatch::new(REGISTRY);
marker.stage(fresh_marker())?;
block_on(marker.commit(store.as_ref(), || {}))??;
Ok(store)
}
fn construction_counts(store: Arc<dyn DurableStore>) -> Result<StoreCounts, Box<dyn Error>> {
let counted = Arc::new(CountingStore::new(Shared(store)));
let handler = ProductionParticipantHandler::new(
Arc::clone(&counted) as Arc<dyn DurableStore>,
test_participant_config(),
)?;
handler.repair_unclean_server_restart(2)?;
let counts = counted.snapshot();
drop(handler);
Ok(counts)
}
#[test]
fn construction_reads_the_same_at_ten_and_ten_thousand_conversations() -> Result<(), Box<dyn Error>>
{
let few = construction_counts(registered(10)?)?;
let many = construction_counts(registered(10_000)?)?;
assert_eq!(
few.named(),
many.named(),
"the handler's store work before bind grew with the registered count"
);
for counts in [few, many] {
assert_eq!(counts.apply_routed_batch_calls, 0);
assert_eq!(counts.append_calls, 0);
assert_eq!(counts.append_all_calls, 0);
assert_eq!(counts.flushes, 0);
}
Ok(())
}
#[test]
fn an_install_with_no_checkpoint_rebuilds_on_first_touch_not_before_bind()
-> Result<(), Box<dyn Error>> {
let (directory, store) = legacy_fixture()?;
let counted = Arc::new(CountingStore::new(Shared(Arc::clone(&store))));
let (handler, counts, diagnostics) = boot(Arc::clone(&counted) as Arc<dyn DurableStore>)?;
assert_eq!(counts.apply_routed_batch_calls, 0, "{diagnostics}");
assert!(!diagnostics.contains("CHECKPOINT_REBUILD"), "{diagnostics}");
let before_touch = counted.snapshot();
assert_enrollment_unchanged(&directory, &handler)?;
assert_checkpoint(store.as_ref())?;
let touched = counted.snapshot().since(&before_touch);
assert_eq!(
touched.apply_routed_batch_calls, 1,
"the first touch publishes the rebuilt checkpoint in one batch and the replayed answers commit nothing"
);
let before_again = counted.snapshot();
assert_enrollment_unchanged(&directory, &handler)?;
assert_eq!(
counted
.snapshot()
.since(&before_again)
.apply_routed_batch_calls,
0
);
Ok(())
}
#[test]
fn the_warm_restores_a_conversation_nobody_touches() -> Result<(), Box<dyn Error>> {
let (_directory, store) = legacy_fixture()?;
let counted = Arc::new(CountingStore::new(Shared(Arc::clone(&store))));
let (handler, counts, _) = boot(Arc::clone(&counted) as Arc<dyn DurableStore>)?;
assert_eq!(counts.apply_routed_batch_calls, 0);
let before_warm = counted.snapshot();
assert_eq!(
handler.restore_remaining("the warm")?,
0,
"the warm refused a conversation"
);
assert_checkpoint(store.as_ref())?;
assert_eq!(
counted
.snapshot()
.since(&before_warm)
.apply_routed_batch_calls,
1
);
let before_again = counted.snapshot();
assert_eq!(handler.restore_remaining("the warm")?, 0);
assert_eq!(
counted.snapshot().since(&before_again),
StoreCounts::default(),
"a finished warm still touched the store"
);
Ok(())
}
fn enrollment_after_clean_stop(count: u64) -> Result<StoreCounts, Box<dyn Error>> {
let store = registered(count)?;
let config = capacity_config(|config| config.max_retired_identity_slots_server = 1);
let incarnation = ConnectionIncarnation::new(71, 1);
let before_stop = ProductionParticipantHandler::new(Arc::clone(&store), config)?;
enroll(&before_stop, incarnation, count + 1, [1; 16])?;
before_stop.record_clean_stop_capacity();
drop(before_stop);
let counted = Arc::new(CountingStore::new(Shared(store)));
let after_stop =
ProductionParticipantHandler::new(Arc::clone(&counted) as Arc<dyn DurableStore>, config)?;
let before_enrol = counted.snapshot();
let refused = dispatch(
&after_stop,
incarnation,
ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: count + 2,
enrollment_token: EnrollmentToken::new([2; 16]),
}),
)?;
let ServerValue::IdentityCapacityExceeded(IdentityCapacityExceeded {
scope,
limit,
occupied,
..
}) = refused
else {
return Err(format!("the seeded ledger did not refuse at the cap: {refused:?}").into());
};
assert_eq!(
(scope, limit, occupied),
(IdentityCapacityScope::Server, 1, 1)
);
Ok(counted.snapshot().since(&before_enrol))
}
#[test]
fn after_a_clean_stop_enrollment_reads_the_same_at_ten_and_ten_thousand_conversations()
-> Result<(), Box<dyn Error>> {
let few = enrollment_after_clean_stop(10)?;
let many = enrollment_after_clean_stop(10_000)?;
assert_eq!(
few.named(),
many.named(),
"the first enrollment after a clean stop waited on the pending conversations"
);
assert_eq!(
few.apply_routed_batch_calls, 1,
"the snapshot is consumed once"
);
Ok(())
}
#[test]
fn a_request_racing_the_warm_restores_the_conversation_once() -> Result<(), Box<dyn Error>> {
let (directory, store) = legacy_fixture()?;
let counted = Arc::new(CountingStore::new(Shared(Arc::clone(&store))));
let (handler, _, _) = boot(Arc::clone(&counted) as Arc<dyn DurableStore>)?;
let before = counted.snapshot();
let barrier = Barrier::new(2);
let warm_refused = std::thread::scope(|scope| {
let warm = scope.spawn(|| {
barrier.wait();
handler.restore_remaining("the warm")
});
barrier.wait();
let request = assert_enrollment_unchanged(&directory, &handler);
let warm = warm.join().map_err(|_| "the warm thread panicked")?;
request?;
Ok::<_, Box<dyn Error>>(warm?)
})?;
assert_eq!(warm_refused, 0, "the warm refused the conversation");
assert_checkpoint(store.as_ref())?;
assert_eq!(
counted.snapshot().since(&before).apply_routed_batch_calls,
1,
"the request and the warm each ran the restore unit"
);
Ok(())
}