use std::fmt;
use std::time::Duration;
use rlmesh_grpc::error::{EnvError, EnvErrorCode};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorCode {
Unspecified,
Timeout,
InvalidAction,
NotReady,
Busy,
Internal,
Crashed,
Cancelled,
Closed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnvironmentError {
pub code: ErrorCode,
pub message: String,
pub is_recoverable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelError {
pub message: String,
pub is_recoverable: bool,
}
impl fmt::Display for ModelError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ModelError {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
Address(String),
Connection(String),
Timeout(Duration),
Environment(EnvironmentError),
Model(ModelError),
Server(String),
Internal(String),
}
impl Error {
pub fn model(message: impl Into<String>) -> Self {
Self::Model(ModelError {
message: message.into(),
is_recoverable: false,
})
}
pub fn model_recoverable(message: impl Into<String>) -> Self {
Self::Model(ModelError {
message: message.into(),
is_recoverable: true,
})
}
pub fn is_recoverable(&self) -> bool {
match self {
Self::Timeout(_) => true,
Self::Environment(error) => error.is_recoverable,
Self::Model(error) => error.is_recoverable,
Self::Connection(_) => true,
_ => false,
}
}
}
pub(crate) fn join_results<T>(a: Result<T>, b: Result<()>, ctx: &str) -> Result<T> {
match (a, b) {
(Ok(value), Ok(())) => Ok(value),
(Err(err), Ok(())) => Err(err),
(Ok(_), Err(err)) => Err(err),
(Err(a_err), Err(b_err)) => Err(Error::Internal(format!(
"{ctx}: {a_err}; close hook failed: {b_err}"
))),
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::Unspecified => "UNSPECIFIED",
Self::Timeout => "TIMEOUT",
Self::InvalidAction => "INVALID_ACTION",
Self::NotReady => "NOT_READY",
Self::Busy => "BUSY",
Self::Internal => "INTERNAL",
Self::Crashed => "CRASHED",
Self::Cancelled => "CANCELLED",
Self::Closed => "CLOSED",
};
f.write_str(label)
}
}
impl fmt::Display for EnvironmentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {}", self.code, self.message)
}
}
impl std::error::Error for EnvironmentError {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Address(message) => write!(f, "invalid address: {message}"),
Self::Connection(message) => write!(f, "connection error: {message}"),
Self::Timeout(duration) => write!(f, "timeout after {duration:?}"),
Self::Environment(error) => write!(f, "environment error: {error}"),
Self::Model(error) => write!(f, "model error: {error}"),
Self::Server(message) => write!(f, "server error: {message}"),
Self::Internal(message) => write!(f, "internal error: {message}"),
}
}
}
impl std::error::Error for Error {}
fn map_env_error_code(code: EnvErrorCode) -> ErrorCode {
match code {
EnvErrorCode::Unspecified => ErrorCode::Unspecified,
EnvErrorCode::Timeout => ErrorCode::Timeout,
EnvErrorCode::InvalidAction => ErrorCode::InvalidAction,
EnvErrorCode::NotReady => ErrorCode::NotReady,
EnvErrorCode::Busy => ErrorCode::Busy,
EnvErrorCode::Internal => ErrorCode::Internal,
EnvErrorCode::Crashed => ErrorCode::Crashed,
EnvErrorCode::Cancelled => ErrorCode::Cancelled,
EnvErrorCode::Closed => ErrorCode::Closed,
_ => ErrorCode::Internal,
}
}
impl From<EnvError> for EnvironmentError {
fn from(value: EnvError) -> Self {
Self {
code: map_env_error_code(value.code),
message: value.message,
is_recoverable: value.is_recoverable,
}
}
}
impl From<rlmesh_grpc::error::ProtocolError> for Error {
fn from(value: rlmesh_grpc::error::ProtocolError) -> Self {
Self::Internal(value.to_string())
}
}
impl From<rlmesh_adapters::v1::ApplyError> for Error {
fn from(value: rlmesh_adapters::v1::ApplyError) -> Self {
Self::model(value.message)
}
}
impl From<rlmesh_grpc::error::Error> for Error {
fn from(value: rlmesh_grpc::error::Error) -> Self {
match value {
rlmesh_grpc::error::Error::Transport(error) => match error {
rlmesh_grpc::error::TransportError::InvalidAddress(message) => {
Self::Address(message)
}
rlmesh_grpc::error::TransportError::BindFailed(message) => Self::Server(message),
rlmesh_grpc::error::TransportError::ConnectFailed(message) => {
Self::Connection(message)
}
rlmesh_grpc::error::TransportError::ConnectionClosed => {
Self::Connection("connection closed".to_string())
}
rlmesh_grpc::error::TransportError::Io(error) => {
Self::Connection(error.to_string())
}
rlmesh_grpc::error::TransportError::MessageTooLarge { size, max } => {
Self::Connection(format!("message too large: {size} > {max}"))
}
rlmesh_grpc::error::TransportError::Unavailable(message) => {
Self::Connection(message)
}
rlmesh_grpc::error::TransportError::Status { code, message } => {
Self::Connection(format!("{code:?}: {message}"))
}
other => Self::Connection(other.to_string()),
},
rlmesh_grpc::error::Error::Protocol(error) => Self::Internal(error.to_string()),
rlmesh_grpc::error::Error::Environment(error) => {
Self::Environment(EnvironmentError::from(error))
}
rlmesh_grpc::error::Error::Model(error) => Self::Model(ModelError {
message: error.message,
is_recoverable: error.is_recoverable,
}),
rlmesh_grpc::error::Error::Timeout(duration) => Self::Timeout(duration),
rlmesh_grpc::error::Error::Cancelled(message) => Self::Connection(message),
rlmesh_grpc::error::Error::Client(error) => Self::Connection(error.to_string()),
other => Self::Internal(other.to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grpc_model_error_maps_to_model_variant_preserving_recoverability() {
let grpc = rlmesh_grpc::error::Error::Model(rlmesh_grpc::error::ModelError {
code: rlmesh_grpc::error::ModelErrorCode::Internal,
message: "handler declined".to_string(),
is_recoverable: true,
debug_info: None,
});
match Error::from(grpc) {
Error::Model(model) => {
assert_eq!(model.message, "handler declined");
assert!(model.is_recoverable);
}
other => panic!("expected Error::Model, got {other:?}"),
}
}
#[test]
fn model_error_constructors_set_recoverability() {
assert!(!Error::model("nope").is_recoverable());
assert!(Error::model_recoverable("retry").is_recoverable());
}
}