use frame_core::error::{FailureReason, TombstoneKind};
use liminal_protocol::wire::{DetachedCause, DiedCause, ServerValue};
use liminal_sdk::SdkError;
use liminal_sdk::remote::{ParticipantResumeStore, RemoteParticipantError};
use crate::error::RefusalClass;
use crate::outcome::DepartReason;
use crate::store::ResumeStore;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReceiveFate {
QuantumElapsed,
ConnectionFate,
}
pub(crate) fn classify_receive_error(error: &RemoteParticipantError) -> ReceiveFate {
match error {
RemoteParticipantError::Transport(sdk_error) => {
let text = sdk_error.to_string();
if text.contains("os error 35")
|| text.contains("Resource temporarily unavailable")
|| text.contains("timed out")
{
ReceiveFate::QuantumElapsed
} else {
ReceiveFate::ConnectionFate
}
}
_ => ReceiveFate::ConnectionFate,
}
}
pub(crate) fn failure_from_died(cause: DiedCause) -> FailureReason {
let (tombstone, detail) = match cause {
DiedCause::ConnectionLost => (
TombstoneKind::NoConnection,
"conversation peer lost its connection".to_owned(),
),
DiedCause::ProcessKilled => (
TombstoneKind::Killed,
"conversation peer process was killed".to_owned(),
),
DiedCause::ProtocolError => (
TombstoneKind::Error,
"conversation peer committed a protocol error".to_owned(),
),
DiedCause::UncleanServerRestart {
prior_server_incarnation,
} => (
TombstoneKind::Error,
format!(
"peer binding recovered after unclean server restart \
(prior incarnation {prior_server_incarnation})"
),
),
};
FailureReason {
child: None,
tombstone: Some(tombstone),
detail,
}
}
pub(crate) const fn depart_from_detached(cause: DetachedCause) -> DepartReason {
match cause {
DetachedCause::CleanDeregister => DepartReason::Deregistered,
DetachedCause::Superseded => DepartReason::Superseded,
DetachedCause::ServerShutdown => DepartReason::ServerShutdown,
}
}
pub(crate) fn classify_refusal(value: &ServerValue) -> (RefusalClass, String) {
let class = match value {
ServerValue::ConnectionConversationCapacityExceeded(_)
| ServerValue::MarkerClosureCapacityExceeded(_)
| ServerValue::ReceiptCapacityExceeded(_)
| ServerValue::IdentityCapacityExceeded(_)
| ServerValue::ObserverBackpressure(_) => RefusalClass::Capacity,
ServerValue::StaleAuthority(_)
| ServerValue::StaleOrUnknownReceipt(_)
| ServerValue::ReceiptExpired(_)
| ServerValue::NoBinding(_)
| ServerValue::Retired(_) => RefusalClass::Authority,
ServerValue::ParticipantUnknown(_)
| ServerValue::EnrollmentKnown(_)
| ServerValue::ConnectionConversationBindingOccupied(_)
| ServerValue::AttemptTokenBodyConflict(_) => RefusalClass::Identity,
ServerValue::ConversationOrderExhausted(_)
| ServerValue::ConversationSequenceExhausted(_) => RefusalClass::Order,
_ => RefusalClass::Protocol,
};
(class, format!("{value:?}"))
}
pub(crate) fn mint_token16() -> [u8; 16] {
*uuid::Uuid::new_v4().as_bytes()
}
pub(crate) struct StoreAdapter<S> {
inner: S,
}
impl<S> StoreAdapter<S> {
pub(crate) const fn new(inner: S) -> Self {
Self { inner }
}
}
impl<S: ResumeStore> ParticipantResumeStore for StoreAdapter<S> {
fn persist(&mut self, canonical_lpcr: &[u8]) -> Result<(), SdkError> {
self.inner
.persist(canonical_lpcr)
.map_err(|error| SdkError::Store {
description: error.detail,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn died_causes_map_to_tombstone_grammar() {
let lost = failure_from_died(DiedCause::ConnectionLost);
assert_eq!(lost.tombstone, Some(TombstoneKind::NoConnection));
assert_eq!(lost.child, None);
assert!(lost.detail.contains("lost its connection"));
let killed = failure_from_died(DiedCause::ProcessKilled);
assert_eq!(killed.tombstone, Some(TombstoneKind::Killed));
let protocol = failure_from_died(DiedCause::ProtocolError);
assert_eq!(protocol.tombstone, Some(TombstoneKind::Error));
let restart = failure_from_died(DiedCause::UncleanServerRestart {
prior_server_incarnation: 7,
});
assert_eq!(restart.tombstone, Some(TombstoneKind::Error));
assert!(restart.detail.contains("incarnation 7"));
}
#[test]
fn detached_causes_map_to_departure_vocabulary() {
assert_eq!(
depart_from_detached(DetachedCause::CleanDeregister),
DepartReason::Deregistered
);
assert_eq!(
depart_from_detached(DetachedCause::Superseded),
DepartReason::Superseded
);
assert_eq!(
depart_from_detached(DetachedCause::ServerShutdown),
DepartReason::ServerShutdown
);
}
#[test]
fn quantum_elapse_strings_classify_as_elapse() {
let elapse = RemoteParticipantError::Transport(SdkError::Connection {
description: "failed to read frame from server: Resource temporarily \
unavailable (os error 35)"
.to_owned(),
});
assert_eq!(classify_receive_error(&elapse), ReceiveFate::QuantumElapsed);
let death = RemoteParticipantError::Transport(SdkError::Connection {
description: "server closed the connection before a full frame arrived".to_owned(),
});
assert_eq!(classify_receive_error(&death), ReceiveFate::ConnectionFate);
let state = RemoteParticipantError::StateUnavailable;
assert_eq!(classify_receive_error(&state), ReceiveFate::ConnectionFate);
}
}