use crate::bus::{BusError, BusFault, QueryError, SourceLabelError};
use crate::identity::ExecutionId;
use crate::supervisor::api::execution::SnapshotError;
use crate::version::FrameworkVersion;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CompatibilityRefusal {
RemoteNewer,
LocalNewer,
}
impl std::fmt::Display for CompatibilityRefusal {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::RemoteNewer => formatter.write_str("remote framework line is newer"),
Self::LocalNewer => formatter.write_str("local framework line is newer"),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConnectError {
#[error("no Phoxal execution is reachable at {endpoint}")]
NoExecution { endpoint: String },
#[error(
"{count} Phoxal executions are reachable at {endpoint}, which must identify exactly one: {executions:?}"
)]
MultipleExecutions {
endpoint: String,
count: usize,
executions: Vec<ExecutionId>,
},
#[error(transparent)]
SourceLabel(#[from] SourceLabelError),
#[error("remote framework {remote} is incompatible with local framework {local}: {refusal}")]
IncompatibleFramework {
remote: FrameworkVersion,
local: FrameworkVersion,
refusal: CompatibilityRefusal,
},
#[error("the frozen supervisor bootstrap reply could not be decoded: {detail}")]
UnreadableBootstrap { detail: String },
#[error("the supervisor identity was lost while the session was being established")]
SupervisorUnavailable,
#[error("the supervisor returned an invalid initial snapshot: {0}")]
Snapshot(#[from] SnapshotError),
#[error(transparent)]
Bus(#[from] BusError),
#[error(transparent)]
Query(#[from] QueryError),
}
impl ConnectError {
#[must_use]
pub const fn is_compatibility_refusal(&self) -> bool {
matches!(
self,
Self::IncompatibleFramework { .. } | Self::UnreadableBootstrap { .. }
)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum DisconnectReason {
SessionClosed,
SupervisorIdentityLost,
SnapshotStreamFailed { detail: String },
TransportFault { fault: BusFault },
LifecycleEnded,
}
impl std::fmt::Display for DisconnectReason {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SessionClosed => formatter.write_str("the session owner closed"),
Self::SupervisorIdentityLost => {
formatter.write_str("the supervisor identity token was lost")
}
Self::SnapshotStreamFailed { detail } => {
write!(formatter, "the supervisor snapshot stream failed: {detail}")
}
Self::TransportFault { fault } => write!(formatter, "transport fault: {fault}"),
Self::LifecycleEnded => {
formatter.write_str("the session lifecycle ended without a terminal cause")
}
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum SessionError {
#[error("the session ended: {reason}")]
Disconnected { reason: DisconnectReason },
#[error(transparent)]
Bus(#[from] BusError),
#[error(transparent)]
Query(#[from] QueryError),
}
#[derive(Debug, thiserror::Error)]
pub enum CloseError {
#[error("the session transport did not close cleanly: {detail}")]
Transport { detail: String },
#[error("the session lifecycle task failed: {detail}")]
Lifecycle { detail: String },
}
#[cfg(test)]
mod tests {
use super::*;
fn refusal(remote: FrameworkVersion, local: FrameworkVersion) -> ConnectError {
crate::session::connection::ensure_compatible_framework(remote, local)
.expect_err("different lines are incompatible")
}
#[test]
fn compatibility_refusal_preserves_versions_and_which_peer_is_newer() {
let older = FrameworkVersion::new(0, 60, 4);
let newer = FrameworkVersion::new(0, 61, 2);
assert!(matches!(
refusal(newer, older),
ConnectError::IncompatibleFramework {
remote,
local,
refusal: CompatibilityRefusal::RemoteNewer,
} if remote == newer && local == older
));
assert!(matches!(
refusal(older, newer),
ConnectError::IncompatibleFramework {
remote,
local,
refusal: CompatibilityRefusal::LocalNewer,
} if remote == older && local == newer
));
}
#[test]
fn compatibility_errors_are_neutral_structured_facts() {
let error = refusal(
FrameworkVersion::new(0, 61, 0),
FrameworkVersion::new(0, 60, 0),
);
let rendered = error.to_string();
assert!(rendered.contains("0.61.0"), "{rendered}");
assert!(rendered.contains("0.60.0"), "{rendered}");
assert!(
rendered.contains("remote framework line is newer"),
"{rendered}"
);
}
}