use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetryHint {
Retryable,
RetryableWithBackoff,
NotRetryable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
ConnectionFailed,
ConnectionTimeout,
ServiceUnavailable,
InvalidConfiguration,
MissingConfiguration,
ExecutionFailed,
ExecutionTimeout,
ExecutionSuspended,
DurabilityUnavailable,
NondeterministicReplay,
StaleAuthority,
ActivationCancelled,
ActivationContended,
UnknownOutcome,
PayloadConflict,
IllegalTransition,
StateVersionConflict,
RequiredChildUnresolved,
InvalidInput,
InvalidMessage,
InvalidState,
ResourceExhausted,
QuotaExceeded,
InternalError,
NotImplemented,
SandboxExecutionFailed,
SandboxUnavailable,
UnsupportedLanguage,
UnsupportedOperation,
}
#[derive(Error, Debug)]
pub enum SdkError {
#[error("gRPC transport error: {0}")]
Transport(#[from] tonic::transport::Error),
#[error("gRPC status error: {0}")]
Status(#[from] tonic::Status),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Other error: {0}")]
Other(#[from] anyhow::Error),
#[error("Connection error: {message}")]
Connection {
message: String,
code: ErrorCode,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Worker registration failed: {message}")]
Registration { message: String, code: ErrorCode },
#[error("Invalid configuration: {message}")]
Configuration {
message: String,
field: Option<String>,
},
#[error("Function invocation error: {message}")]
Invocation {
message: String,
function_name: Option<String>,
},
#[error("State management error: {message}")]
State { message: String, code: ErrorCode },
#[error("Timeout error: {message}")]
Timeout {
message: String,
operation: String,
duration_ms: Option<u64>,
},
#[error("Invalid message: {message}")]
InvalidMessage {
message: String,
field: Option<String>,
},
#[error("Execution suspended: {message}")]
SuspendedExecution { message: String, reason: String },
#[error("Durable timer suspended: {}", suspension.timer_key)]
DurableSuspension {
suspension: Box<crate::pb::WorkerSuspension>,
},
#[error("Replay error: {message}")]
ReplayError {
message: String,
step_id: Option<String>,
},
#[error("Durable activation error ({code:?}): {message}")]
Activation {
message: String,
code: ErrorCode,
activation_id: Option<String>,
attempt: Option<u32>,
},
#[error("Service call error: {message}")]
ServiceCallError { message: String, service: String },
#[error("Telemetry error: {0}")]
TelemetryError(String),
#[error("Internal error: {0}")]
Internal(String),
#[error("Service unavailable: {message}")]
Unavailable {
message: String,
service: Option<String>,
},
#[error("Invalid argument: {message}")]
InvalidArgument {
message: String,
argument: Option<String>,
},
#[error("LM API error ({status}): {message}")]
LmApiError {
status: u16,
provider: String,
message: String,
request_id: Option<String>,
},
#[error("Sandbox error ({operation}): {message}")]
Sandbox {
message: String,
operation: String,
code: ErrorCode,
},
}
impl SdkError {
pub fn code(&self) -> ErrorCode {
match self {
Self::Transport(_) | Self::Status(_) => ErrorCode::ConnectionFailed,
Self::Connection { code, .. } => *code,
Self::Registration { code, .. } => *code,
Self::Configuration { .. } => ErrorCode::InvalidConfiguration,
Self::Invocation { .. } => ErrorCode::ExecutionFailed,
Self::State { code, .. } => *code,
Self::Timeout { .. } => ErrorCode::ExecutionTimeout,
Self::InvalidMessage { .. } => ErrorCode::InvalidMessage,
Self::SuspendedExecution { .. } => ErrorCode::ExecutionSuspended,
Self::DurableSuspension { .. } => ErrorCode::ExecutionSuspended,
Self::ReplayError { .. } => ErrorCode::ExecutionFailed,
Self::Activation { code, .. } => *code,
Self::ServiceCallError { .. } => ErrorCode::ServiceUnavailable,
Self::TelemetryError(_) => ErrorCode::InternalError,
Self::Internal(_) => ErrorCode::InternalError,
Self::Unavailable { .. } => ErrorCode::ServiceUnavailable,
Self::InvalidArgument { .. } => ErrorCode::InvalidInput,
Self::Serialization(_) => ErrorCode::InvalidInput,
Self::Other(_) => ErrorCode::InternalError,
Self::LmApiError { status, .. } => match *status {
401 | 403 => ErrorCode::InvalidConfiguration,
429 => ErrorCode::ResourceExhausted,
408 => ErrorCode::ExecutionTimeout,
500 | 502 | 503 | 504 | 529 => ErrorCode::ServiceUnavailable,
_ => ErrorCode::InvalidInput,
},
Self::Sandbox { code, .. } => *code,
}
}
pub fn retry_hint(&self) -> RetryHint {
match self {
Self::Transport(_) | Self::Status(_) => RetryHint::RetryableWithBackoff,
Self::Connection { code, .. } => match code {
ErrorCode::ConnectionTimeout | ErrorCode::ServiceUnavailable => {
RetryHint::RetryableWithBackoff
}
_ => RetryHint::NotRetryable,
},
Self::Unavailable { .. } => RetryHint::RetryableWithBackoff,
Self::ServiceCallError { .. } => RetryHint::RetryableWithBackoff,
Self::Timeout { .. } => RetryHint::Retryable,
Self::State {
code: ErrorCode::ResourceExhausted,
..
} => RetryHint::RetryableWithBackoff,
Self::Configuration { .. }
| Self::InvalidMessage { .. }
| Self::InvalidArgument { .. }
| Self::Serialization(_)
| Self::Internal(_)
| Self::TelemetryError(_) => RetryHint::NotRetryable,
Self::Invocation { .. } | Self::ReplayError { .. } => RetryHint::NotRetryable,
Self::Activation { .. } => RetryHint::NotRetryable,
Self::SuspendedExecution { .. } => RetryHint::NotRetryable,
Self::DurableSuspension { .. } => RetryHint::NotRetryable,
Self::Registration { .. } => RetryHint::NotRetryable,
Self::State { .. } => RetryHint::NotRetryable,
Self::Other(_) => RetryHint::NotRetryable,
Self::LmApiError { status, .. } => match *status {
408 | 429 | 500 | 502 | 503 | 504 | 529 => RetryHint::RetryableWithBackoff,
_ => RetryHint::NotRetryable,
},
Self::Sandbox { code, .. } => match code {
ErrorCode::SandboxUnavailable => RetryHint::RetryableWithBackoff,
_ => RetryHint::NotRetryable,
},
}
}
pub fn is_retryable(&self) -> bool {
matches!(
self.retry_hint(),
RetryHint::Retryable | RetryHint::RetryableWithBackoff
)
}
}
pub type Result<T> = std::result::Result<T, SdkError>;