use std::error::Error;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use liminal_protocol::wire::{
AttachAttemptToken, AttachSecret, ClientRequest, ConversationId, CredentialAttachRequest,
EnrollmentRequest, EnrollmentToken, Generation, MarkerSettlementBackpressure, ParticipantAck,
ParticipantId, RecordAdmission, RecordAdmissionAttemptToken, ServerPush, ServerValue,
};
use liminal_sdk::{
ConnectionPoolConfig, PARTICIPANT_PUMP_WINDOW, ParticipantResumeStore, RemoteConfig,
RemoteOperationRecordOutcome, RemoteParticipantHandle, RemoteParticipantInbound,
RemoteParticipantSendOutcome, SdkError,
};
use liminal_server::config::types::ParticipantConfig;
use liminal_server::config::{LimitsConfig, ServerConfig, ServicesConfig};
use liminal_server::server::connection::{
ConnectionServices, ConnectionSupervisor, LiminalConnectionServices,
};
use liminal_server::server::listener::ServerListener;
const SETTLEMENT_CONVERSATION: ConversationId = 0xA516;
const MAX_DEMUX_FRAMES: usize = 128;
const MAX_DRAIN_FRAMES: usize = 128;
const MAX_WINDOW_HUNT_COMMITS: u8 = 24;
const FIXTURE_QUIET: Duration = Duration::from_millis(250);
const fn settlement_participant_config() -> ParticipantConfig {
ParticipantConfig {
wire_frame_limit: 65_536,
attach_receipt_ttl_ms: 60_000,
receipt_provenance_ttl_ms: 600_000,
live_receipt_server_report_threshold: 1_024,
max_live_attach_receipts_per_participant: 8,
receipt_provenance_server_report_threshold: 4_096,
receipt_provenance_per_conversation_report_threshold: 256,
max_receipt_provenance_per_participant: 64,
max_retired_identity_slots_server: 1_024,
identity_slots: 4,
observer_recovery_max_entries: 64,
max_semantic_conversations_per_connection: 32,
max_ordinary_record_entries: 1,
max_ordinary_record_bytes: 58,
max_generated_marker_entries: 1,
max_generated_marker_bytes: 4_096,
mandatory_transaction_bound_entries: 4,
mandatory_transaction_bound_bytes: 16_384,
full_recovery_claim_entries: 4,
full_recovery_claim_bytes: 16_384,
retained_capacity_entries: 14,
retained_capacity_bytes: 65_536,
max_retained_record_rows: 16,
closure_episode_churn_limit: 1_024,
}
}
#[derive(Debug, Default, Clone)]
struct SettlementStore {
canonical: Arc<Mutex<Vec<u8>>>,
}
impl ParticipantResumeStore for SettlementStore {
fn persist(&mut self, canonical_lpcr: &[u8]) -> Result<(), SdkError> {
let mut guard = self.canonical.lock().map_err(|_| SdkError::Store {
description: "the settlement resume store lock was poisoned".to_owned(),
})?;
guard.clear();
guard.extend_from_slice(canonical_lpcr);
drop(guard);
Ok(())
}
}
type SdkParticipant = RemoteParticipantHandle<SettlementStore>;
fn server_config(store_dir: Option<&Path>) -> Result<ServerConfig, Box<dyn Error>> {
Ok(ServerConfig {
listen_address: "127.0.0.1:0".parse()?,
health_listen_address: "127.0.0.1:0".parse()?,
drain_timeout_ms: 30_000,
channels: Vec::new(),
routing_rules: Vec::new(),
persistence_path: store_dir.map(Path::to_path_buf),
cluster: None,
auth: None,
services: ServicesConfig::default(),
limits: LimitsConfig::default(),
websocket: None,
participant: Some(settlement_participant_config()),
})
}
const fn pool() -> ConnectionPoolConfig {
ConnectionPoolConfig::new(1, 1, 8)
}
struct SettlementServer {
listener: Option<ServerListener>,
supervisor: ConnectionSupervisor,
address: String,
}
impl SettlementServer {
fn start(store_dir: Option<&Path>) -> Result<Self, Box<dyn Error>> {
let config = server_config(store_dir)?;
let services = Arc::new(LiminalConnectionServices::from_config(&config)?);
let supervisor =
ConnectionSupervisor::with_services(services as Arc<dyn ConnectionServices>)?;
let listener = ServerListener::bind(&config, supervisor.clone())?;
let address = listener.local_addr().to_string();
Ok(Self {
listener: Some(listener),
supervisor,
address,
})
}
fn shutdown(mut self) -> Result<(), Box<dyn Error>> {
if let Some(listener) = self.listener.take() {
listener.shutdown()?;
}
self.supervisor.shutdown();
Ok(())
}
}
struct Client {
label: &'static str,
handle: SdkParticipant,
_config: RemoteConfig,
pushes: Vec<ServerPush>,
participant_id: ParticipantId,
generation: Generation,
attach_secret: AttachSecret,
acked_through: u64,
}
impl Client {
fn enroll(
server: &SettlementServer,
label: &'static str,
enrollment_token: [u8; 16],
) -> Result<Self, Box<dyn Error>> {
let config = RemoteConfig::new(
server.address.clone(),
label,
SETTLEMENT_CONVERSATION.to_string(),
pool(),
)?
.connect_tcp()?;
let handle = RemoteParticipantHandle::new(&config, SettlementStore::default())?;
let mut client = Self {
label,
handle,
_config: config,
pushes: Vec::new(),
participant_id: 0,
generation: Generation::ONE,
attach_secret: AttachSecret::new([0; 32]),
acked_through: 0,
};
let enrolled = client.exchange(ClientRequest::Enrollment(EnrollmentRequest {
conversation_id: SETTLEMENT_CONVERSATION,
enrollment_token: EnrollmentToken::new(enrollment_token),
}))?;
let ServerValue::EnrollBound(bound) = enrolled else {
return Err(format!("[{label}] enrollment did not bind: {enrolled:?}").into());
};
client.participant_id = bound.participant_id();
client.generation = bound.capability_generation();
client.attach_secret = bound.attach_secret();
Ok(client)
}
fn exchange(&mut self, request: ClientRequest) -> Result<ServerValue, Box<dyn Error>> {
let recorded = self
.handle
.record_operation(request)
.map_err(|error| format!("[{}] record_operation failed: {error:?}", self.label))?;
let operation = match recorded {
RemoteOperationRecordOutcome::Recorded(operation)
| RemoteOperationRecordOutcome::Continuous(operation) => operation,
RemoteOperationRecordOutcome::Refused { request, reason } => {
return Err(format!(
"[{}] SDK refused outbound request {request:?}: {reason:?}",
self.label
)
.into());
}
};
let sent = self
.handle
.send_operation(operation)
.map_err(|error| format!("[{}] send_operation failed: {error:?}", self.label))?;
match sent {
RemoteParticipantSendOutcome::Sent { .. } => {}
RemoteParticipantSendOutcome::TransportLost { error, .. } => {
return Err(
format!("[{}] SDK transport lost while sending: {error}", self.label).into(),
);
}
}
for _ in 0..MAX_DEMUX_FRAMES {
let inbound = self
.handle
.receive()
.map_err(|error| format!("[{}] receive failed: {error:?}", self.label))?;
match inbound {
RemoteParticipantInbound::Applied { value, .. }
| RemoteParticipantInbound::Refused { value, .. } => return Ok(value),
RemoteParticipantInbound::Push { value, .. } => self.pushes.push(value),
}
}
Err(format!(
"[{}] no response arrived within {MAX_DEMUX_FRAMES} inbound frames",
self.label
)
.into())
}
fn drain(&mut self) -> Result<usize, Box<dyn Error>> {
self.drain_within(PARTICIPANT_PUMP_WINDOW)
}
fn drain_within(&mut self, quiet: Duration) -> Result<usize, Box<dyn Error>> {
let mut drained = 0_usize;
while drained < MAX_DRAIN_FRAMES {
let inbound = self
.handle
.receive_within(quiet)
.map_err(|error| format!("[{}] the push drain failed: {error:?}", self.label))?;
match inbound {
None => return Ok(drained),
Some(RemoteParticipantInbound::Push { value, .. }) => {
self.pushes.push(value);
drained += 1;
}
Some(other) => {
return Err(format!(
"[{}] the drain read a correlated frame it never asked for: {other:?}",
self.label
)
.into());
}
}
}
Err(format!(
"[{}] the drain never reached silence within {MAX_DRAIN_FRAMES} frames",
self.label
)
.into())
}
fn commit_record(&mut self, nonce: u8) -> Result<ServerValue, Box<dyn Error>> {
self.exchange(ClientRequest::RecordAdmission(RecordAdmission {
conversation_id: SETTLEMENT_CONVERSATION,
participant_id: self.participant_id,
capability_generation: self.generation,
record_admission_attempt_token: RecordAdmissionAttemptToken::new([nonce; 16]),
payload: vec![nonce],
}))
}
fn attach(&mut self, attempt_token: [u8; 16]) -> Result<ServerValue, Box<dyn Error>> {
self.exchange(ClientRequest::CredentialAttach(CredentialAttachRequest {
conversation_id: SETTLEMENT_CONVERSATION,
participant_id: self.participant_id,
capability_generation: self.generation,
attach_secret: self.attach_secret,
attach_attempt_token: AttachAttemptToken::new(attempt_token),
accept_marker_delivery_seq: None,
}))
}
fn delivered_through(&self) -> u64 {
self.pushes
.iter()
.filter_map(|push| match push {
ServerPush::ParticipantDelivery(delivery)
if delivery.conversation_id == SETTLEMENT_CONVERSATION =>
{
Some(delivery.delivery_seq)
}
ServerPush::ParticipantDelivery(_)
| ServerPush::ObserverProgressed { .. }
| ServerPush::MarkerSettled { .. } => None,
})
.max()
.unwrap_or(0)
}
fn pump_and_ack(&mut self, quiet: Duration) -> Result<(), Box<dyn Error>> {
self.drain_within(quiet)?;
let through_seq = self.delivered_through();
if through_seq <= self.acked_through {
return Ok(());
}
let acked = self.exchange(ClientRequest::ParticipantAck(ParticipantAck {
conversation_id: SETTLEMENT_CONVERSATION,
participant_id: self.participant_id,
capability_generation: self.generation,
through_seq,
}))?;
match acked {
ServerValue::AckCommitted(_) | ServerValue::AckNoOp(_) => {
self.acked_through = through_seq;
Ok(())
}
other => Err(format!(
"[{}] acknowledging delivery {through_seq} did not commit: {other:?}",
self.label
)
.into()),
}
}
fn attach_and_adopt(&mut self, attempt_token: [u8; 16]) -> Result<(), Box<dyn Error>> {
let bound = self.attach(attempt_token)?;
let ServerValue::AttachBound(bound) = bound else {
return Err(format!("[{}] attach did not bind: {bound:?}", self.label).into());
};
self.generation = bound.capability_generation();
self.attach_secret = bound.attach_secret();
Ok(())
}
}
fn settlement_wakes(pushes: &[ServerPush]) -> Vec<(ConversationId, u64)> {
pushes
.iter()
.filter_map(|push| match *push {
ServerPush::MarkerSettled {
conversation_id,
refused_epoch,
} => Some((conversation_id, refused_epoch)),
ServerPush::ObserverProgressed { .. } | ServerPush::ParticipantDelivery(_) => None,
})
.collect()
}
struct SettlementWindow {
server: SettlementServer,
refused: Client,
uninvolved: Client,
refused_epoch: u64,
refused_attempt_token: [u8; 16],
commits: u8,
}
fn open_settlement_window(store_dir: Option<&Path>) -> Result<SettlementWindow, Box<dyn Error>> {
let server = SettlementServer::start(store_dir)?;
let mut refused = Client::enroll(&server, "a5-refused", [0xA5; 16])?;
let mut uninvolved = Client::enroll(&server, "a5-uninvolved", [0xB5; 16])?;
uninvolved.attach_and_adopt([0xB0; 16])?;
refused.attach_and_adopt([0xA0; 16])?;
for nonce in 1..=MAX_WINDOW_HUNT_COMMITS {
let committed = refused.commit_record(nonce)?;
if !matches!(committed, ServerValue::RecordCommitted(_)) {
return Err(format!(
"record {nonce} must commit to generate marker debt, got {committed:?}"
)
.into());
}
refused.pump_and_ack(FIXTURE_QUIET)?;
uninvolved.pump_and_ack(FIXTURE_QUIET)?;
let attempt_token = [0xC0 | (nonce & 0x0F); 16];
match refused.attach(attempt_token)? {
ServerValue::MarkerSettlementBackpressure(
MarkerSettlementBackpressure::CredentialAttach {
conversation_id,
refused_epoch,
},
) => {
assert_eq!(conversation_id, SETTLEMENT_CONVERSATION);
return Ok(SettlementWindow {
server,
refused,
uninvolved,
refused_epoch,
refused_attempt_token: attempt_token,
commits: nonce,
});
}
ServerValue::AttachBound(bound) => {
refused.generation = bound.capability_generation();
refused.attach_secret = bound.attach_secret();
}
other => {
return Err(format!(
"the window probe must either bind or hear the settlement row, got {other:?}"
)
.into());
}
}
}
Err(format!(
"no marker-settlement window opened within {MAX_WINDOW_HUNT_COMMITS} commits"
)
.into())
}
#[test]
fn the_settlement_wake_reaches_the_refused_connection_and_no_other() -> Result<(), Box<dyn Error>> {
settlement_wake_reaches_only_the_refused_connection(None)
}
#[test]
fn the_settlement_wake_reaches_the_refused_connection_and_no_other_on_disk()
-> Result<(), Box<dyn Error>> {
let store_dir = tempfile::tempdir()?;
let path: PathBuf = store_dir.path().to_path_buf();
println!("disk arm persistence_path = {}", path.display());
let database_config = path.join("durability").join("config.json");
assert!(
!database_config.exists(),
"the before-image must be an empty directory, or the control below proves nothing: {}",
database_config.display()
);
settlement_wake_reaches_only_the_refused_connection(Some(&path))?;
assert!(
database_config.exists(),
"this arm must have run against a PERSISTENT haematite database rooted at the configured \
path -- {} is the config file build_durable_store_with writes for persistence_path = \
Some(..) and never for the ephemeral store. Its absence means this test passed as a \
second ephemeral run and measured nothing.",
database_config.display()
);
Ok(())
}
fn settlement_wake_reaches_only_the_refused_connection(
store_dir: Option<&Path>,
) -> Result<(), Box<dyn Error>> {
let SettlementWindow {
server,
mut refused,
mut uninvolved,
refused_epoch,
commits,
..
} = open_settlement_window(store_dir)?;
println!("settlement window opened after {commits} commits, refused_epoch = {refused_epoch}");
println!(
"before the clearing write: refused drained {}, uninvolved drained {}",
refused.drain()?,
uninvolved.drain()?
);
assert!(
settlement_wakes(&refused.pushes).is_empty(),
"a wake reached the refused client before anything cleared: {:?}",
refused.pushes
);
assert!(
settlement_wakes(&uninvolved.pushes).is_empty(),
"a wake reached the uninvolved client before anything cleared: {:?}",
uninvolved.pushes
);
let cleared = uninvolved.commit_record(0xE1)?;
assert!(
matches!(cleared, ServerValue::RecordCommitted(_)),
"the clearing write must commit through the DrainFirst arm, got {cleared:?}"
);
let drained = refused.drain()?;
println!("refused client drained {drained} pushes after the clearing write");
assert_eq!(
settlement_wakes(&refused.pushes),
vec![(SETTLEMENT_CONVERSATION, refused_epoch)],
"the refused connection must receive exactly its own wake, carrying the conversation \
and the epoch its refusal named -- refused_epoch is load-bearing, it is what the \
stage-11 retry discipline matches on. Its inbox held: {:?}",
refused.pushes
);
let drained = uninvolved.drain()?;
println!("uninvolved client drained {drained} pushes after the clearing write");
assert_eq!(
settlement_wakes(&uninvolved.pushes),
Vec::new(),
"§0.16 build obligation 3: an uninvolved live participant on the same conversation -- \
even the one that DROVE the clearing write -- must receive no settlement wake. Its \
inbox held: {:?}",
uninvolved.pushes
);
drop(refused);
drop(uninvolved);
server.shutdown()?;
Ok(())
}
#[test]
fn the_refused_attach_binds_when_retried_after_its_matching_wake() -> Result<(), Box<dyn Error>> {
let SettlementWindow {
server,
mut refused,
mut uninvolved,
refused_epoch,
refused_attempt_token,
..
} = open_settlement_window(None)?;
let cleared = uninvolved.commit_record(0xE2)?;
assert!(
matches!(cleared, ServerValue::RecordCommitted(_)),
"the clearing write must commit through the DrainFirst arm, got {cleared:?}"
);
refused.drain()?;
assert_eq!(
settlement_wakes(&refused.pushes),
vec![(SETTLEMENT_CONVERSATION, refused_epoch)],
"the retry below is only lawful once a MarkerSettled carrying THIS refusal's epoch has \
arrived; without that match it would be a retry on a coincidence. Inbox: {:?}",
refused.pushes
);
let retried = refused.attach(refused_attempt_token)?;
match retried {
ServerValue::AttachBound(bound) => {
assert_eq!(bound.conversation_id(), SETTLEMENT_CONVERSATION);
assert_eq!(bound.participant_id(), refused.participant_id);
}
other => {
return Err(format!(
"the retry after a matching MarkerSettled must bind, got {other:?}"
)
.into());
}
}
drop(refused);
drop(uninvolved);
server.shutdown()?;
Ok(())
}