use std::error::Error;
use std::fs;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use liminal_protocol::wire::{
ClientRequest, EnrollmentRequest, EnrollmentToken, Generation, ParticipantDelivery, ServerPush,
ServerValue,
};
use liminal_sdk::remote::{
ParticipantResumeStore, RemoteConfig, RemoteOperationRecordOutcome, RemoteParticipantError,
RemoteParticipantHandle, RemoteParticipantInbound, RemoteParticipantSendOutcome,
};
use liminal_sdk::{ConnectionPoolConfig, SdkError};
pub const CONNECT_TIMEOUT: Duration = ANSWER_BUDGET;
pub const ANSWER_BUDGET: Duration = Duration::from_secs(30);
pub struct RawStore {
path: PathBuf,
}
impl ParticipantResumeStore for RawStore {
fn persist(&mut self, canonical_lpcr: &[u8]) -> Result<(), SdkError> {
let mut file = fs::File::create(&self.path).map_err(|error| SdkError::Store {
description: format!("create {}: {error}", self.path.display()),
})?;
file.write_all(canonical_lpcr)
.and_then(|()| file.sync_all())
.map_err(|error| SdkError::Store {
description: format!("write {}: {error}", self.path.display()),
})
}
}
fn remote_config(endpoint: &str) -> Result<RemoteConfig, Box<dyn Error>> {
Ok(RemoteConfig::new(
endpoint.to_owned(),
"events",
"unused-conversation-name",
ConnectionPoolConfig::new(1, 10, 16),
)?)
}
fn connect_with(
endpoint: &str,
build: impl Fn(&RemoteConfig) -> Result<RemoteParticipantHandle<RawStore>, RemoteParticipantError>,
) -> Result<RemoteParticipantHandle<RawStore>, Box<dyn Error>> {
let deadline = Instant::now() + CONNECT_TIMEOUT;
let mut last_error: Option<Box<dyn Error>> = None;
while Instant::now() < deadline {
let config = remote_config(endpoint)?;
let attempt = config
.connect_tcp()
.map_err(Box::<dyn Error>::from)
.and_then(|connected| build(&connected).map_err(Box::<dyn Error>::from));
match attempt {
Ok(handle) => return Ok(handle),
Err(error) => {
last_error = Some(error);
std::thread::sleep(Duration::from_millis(20));
}
}
}
Err(last_error.map_or_else(
|| "participant never connected within timeout".into(),
|error| format!("participant never connected within timeout: {error}").into(),
))
}
pub fn connect_fresh(
endpoint: &str,
lpcr_path: &Path,
) -> Result<RemoteParticipantHandle<RawStore>, Box<dyn Error>> {
connect_with(endpoint, |config| {
RemoteParticipantHandle::new(
config,
RawStore {
path: lpcr_path.to_path_buf(),
},
)
})
}
pub fn restore_from(
endpoint: &str,
lpcr_path: &Path,
canonical_lpcr: &[u8],
) -> Result<RemoteParticipantHandle<RawStore>, Box<dyn Error>> {
connect_with(endpoint, |config| {
RemoteParticipantHandle::restore(
config,
RawStore {
path: lpcr_path.to_path_buf(),
},
canonical_lpcr,
)
})
}
pub fn is_quantum_elapse(error: &RemoteParticipantError) -> bool {
match error {
RemoteParticipantError::Transport(sdk_error) => {
let text = sdk_error.to_string();
text.contains("os error 35")
|| text.contains("Resource temporarily unavailable")
|| text.contains("timed out")
}
_ => false,
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub enum ProbeAnswer {
CrateRefusedRecord { detail: String },
TransportLost { detail: String },
NoAnswer {
deliveries: Vec<ParticipantDelivery>,
},
Answer {
response: Box<RemoteParticipantInbound>,
deliveries: Vec<ParticipantDelivery>,
},
}
pub fn probe_submit(
handle: &RemoteParticipantHandle<RawStore>,
request: ClientRequest,
) -> Result<ProbeAnswer, Box<dyn Error>> {
let operation = match handle.record_operation(request)? {
RemoteOperationRecordOutcome::Recorded(operation)
| RemoteOperationRecordOutcome::Continuous(operation) => operation,
RemoteOperationRecordOutcome::Refused { request, reason } => {
return Ok(ProbeAnswer::CrateRefusedRecord {
detail: format!("request {request:?} refused: {reason:?}"),
});
}
};
match handle.send_operation(operation)? {
RemoteParticipantSendOutcome::Sent { .. } => {}
RemoteParticipantSendOutcome::TransportLost {
error,
operation_fate,
reconnect,
} => {
return Ok(ProbeAnswer::TransportLost {
detail: format!(
"send lost transport: {error}; operation fate {operation_fate:?}; \
reconnect {reconnect:?}"
),
});
}
}
let start = Instant::now();
let mut deliveries = Vec::new();
while start.elapsed() < ANSWER_BUDGET {
match handle.receive() {
Ok(RemoteParticipantInbound::Push {
value: ServerPush::ParticipantDelivery(delivery),
..
}) => deliveries.push(delivery),
Ok(RemoteParticipantInbound::Push { value, .. }) => {
return Err(format!("unexpected non-delivery push: {value:?}").into());
}
Ok(response) => {
return Ok(ProbeAnswer::Answer {
response: Box::new(response),
deliveries,
});
}
Err(error) if is_quantum_elapse(&error) => {}
Err(error) => {
return Ok(ProbeAnswer::TransportLost {
detail: format!("receive lost transport: {error}"),
});
}
}
}
Ok(ProbeAnswer::NoAnswer { deliveries })
}
pub fn send_without_answer(
handle: &RemoteParticipantHandle<RawStore>,
request: ClientRequest,
) -> Result<(), Box<dyn Error>> {
let operation = match handle.record_operation(request)? {
RemoteOperationRecordOutcome::Recorded(operation)
| RemoteOperationRecordOutcome::Continuous(operation) => operation,
RemoteOperationRecordOutcome::Refused { request, reason } => {
return Err(format!("crate refused to record {request:?}: {reason:?}").into());
}
};
match handle.send_operation(operation)? {
RemoteParticipantSendOutcome::Sent { .. } => Ok(()),
RemoteParticipantSendOutcome::TransportLost { error, .. } => {
Err(format!("send lost transport: {error}").into())
}
}
}
pub struct Enrolled {
pub participant_id: u64,
pub attach_secret: [u8; 32],
pub generation: Generation,
}
pub fn enroll(
handle: &RemoteParticipantHandle<RawStore>,
conversation_id: u64,
token: [u8; 16],
) -> Result<Enrolled, Box<dyn Error>> {
let answer = probe_submit(
handle,
ClientRequest::Enrollment(EnrollmentRequest {
conversation_id,
enrollment_token: EnrollmentToken::new(token),
}),
)?;
let ProbeAnswer::Answer { response, .. } = answer else {
return Err(format!("enrollment got no answer: {answer:?}").into());
};
let RemoteParticipantInbound::Applied { value, .. } = *response else {
return Err(format!("enrollment was not applied: {response:?}").into());
};
let ServerValue::EnrollBound(receipt) = value else {
return Err(format!("enrollment did not bind: {value:?}").into());
};
Ok(Enrolled {
participant_id: receipt.participant_id(),
attach_secret: receipt.attach_secret().into_bytes(),
generation: receipt.origin_binding_epoch().capability_generation,
})
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct DeliveryWatch {
pub deliveries: Vec<ParticipantDelivery>,
pub terminal: Option<String>,
}
pub fn watch_deliveries(
handle: &RemoteParticipantHandle<RawStore>,
budget: Duration,
) -> Result<DeliveryWatch, Box<dyn Error>> {
let start = Instant::now();
let mut deliveries = Vec::new();
let mut terminal = None;
while start.elapsed() < budget {
match handle.receive() {
Ok(RemoteParticipantInbound::Push {
value: ServerPush::ParticipantDelivery(delivery),
..
}) => deliveries.push(delivery),
Ok(other) => {
return Err(format!("unexpected non-delivery inbound: {other:?}").into());
}
Err(error) if is_quantum_elapse(&error) => {}
Err(error) => {
terminal = Some(format!("{error}"));
break;
}
}
}
Ok(DeliveryWatch {
deliveries,
terminal,
})
}