runner_q/activity/error.rs
1/// Error type for activity operations that supports retry semantics.
2///
3/// This enum distinguishes between errors that should trigger a retry
4/// and those that indicate permanent failure.
5///
6/// # Examples
7///
8/// ```rust
9/// use runner_q::ActivityError;
10/// use runner_q::ActivityHandlerResult;
11///
12/// // Retryable error - temporary network issue
13/// let retry_error = ActivityError::Retry("Network timeout".to_string());
14///
15/// // Non-retryable error - invalid input data
16/// let permanent_error = ActivityError::NonRetry("Invalid user ID format".to_string());
17///
18/// // Using in activity handlers
19/// fn process_user_data(payload: serde_json::Value) -> ActivityHandlerResult {
20/// let user_id = payload["user_id"].as_str()
21/// .ok_or_else(|| ActivityError::NonRetry("Missing user_id".to_string()))?;
22///
23/// if user_id.is_empty() {
24/// return Err(ActivityError::NonRetry("Empty user_id".to_string()));
25/// }
26///
27/// // Simulate processing that might fail temporarily
28/// if payload["retry_processing"].as_bool().unwrap_or(false) {
29/// Err(ActivityError::Retry("Processing failed, will retry".to_string()))
30/// } else {
31/// Ok(Some(serde_json::json!({"processed": true})))
32/// }
33/// }
34/// ```
35#[derive(Debug)]
36pub enum ActivityError {
37 /// Error that should trigger a retry attempt.
38 ///
39 /// Use this for temporary failures like network timeouts, temporary service
40 /// unavailability, or other transient issues that might resolve on retry.
41 Retry(String),
42
43 /// Error that should not trigger a retry.
44 ///
45 /// Use this for permanent failures like invalid input data, authentication
46 /// errors, or other issues that won't be resolved by retrying.
47 NonRetry(String),
48}
49
50impl std::fmt::Display for ActivityError {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 ActivityError::Retry(msg) => write!(f, "Retryable error: {}", msg),
54 ActivityError::NonRetry(msg) => write!(f, "Non-retryable error: {}", msg),
55 }
56 }
57}
58
59impl std::error::Error for ActivityError {}
60
61impl From<serde_json::Error> for ActivityError {
62 fn from(error: serde_json::Error) -> Self {
63 if error.is_retryable() {
64 ActivityError::Retry(format!("JSON error: {}", error))
65 } else {
66 ActivityError::NonRetry(format!("JSON error: {}", error))
67 }
68 }
69}
70
71impl From<std::io::Error> for ActivityError {
72 fn from(error: std::io::Error) -> Self {
73 if error.is_retryable() {
74 ActivityError::Retry(format!("IO error: {}", error))
75 } else {
76 ActivityError::NonRetry(format!("IO error: {}", error))
77 }
78 }
79}
80
81/// Trait to determine if an error should be retried.
82///
83/// This trait allows custom error types to specify their retry behavior,
84/// enabling automatic conversion to `ActivityError` with appropriate retry semantics.
85///
86/// # Examples
87///
88/// ```rust
89/// use runner_q::RetryableError;
90/// use runner_q::ActivityError;
91///
92/// // Custom error type
93/// #[derive(Debug)]
94/// pub struct DatabaseError {
95/// message: String,
96/// is_connection_error: bool,
97/// }
98///
99/// impl RetryableError for DatabaseError {
100/// fn is_retryable(&self) -> bool {
101/// self.is_connection_error
102/// }
103/// }
104///
105/// impl From<DatabaseError> for ActivityError {
106/// fn from(err: DatabaseError) -> Self {
107/// if err.is_retryable() {
108/// ActivityError::Retry(err.message)
109/// } else {
110/// ActivityError::NonRetry(err.message)
111/// }
112/// }
113/// }
114///
115/// // Usage in activity handler
116/// fn handle_database_operation() -> Result<(), DatabaseError> {
117/// // ... database operation that might fail
118/// Err(DatabaseError {
119/// message: "Connection timeout".to_string(),
120/// is_connection_error: true,
121/// })
122/// }
123/// ```
124pub trait RetryableError {
125 /// Determine if this error should trigger a retry attempt.
126 ///
127 /// Return `true` for temporary errors that might resolve on retry,
128 /// `false` for permanent errors that won't be fixed by retrying.
129 fn is_retryable(&self) -> bool;
130}
131
132// Implement RetryableError for common error types
133
134/// Implementation for std::io::Error
135impl RetryableError for std::io::Error {
136 fn is_retryable(&self) -> bool {
137 match self.kind() {
138 std::io::ErrorKind::TimedOut => true,
139 std::io::ErrorKind::Interrupted => true,
140 std::io::ErrorKind::WouldBlock => true,
141 std::io::ErrorKind::ConnectionRefused => true,
142 std::io::ErrorKind::ConnectionAborted => true,
143 std::io::ErrorKind::ConnectionReset => true,
144 std::io::ErrorKind::PermissionDenied => false,
145 std::io::ErrorKind::NotFound => false,
146 std::io::ErrorKind::AlreadyExists => false,
147 _ => true, // Default to retryable for other IO errors
148 }
149 }
150}
151
152/// Implementation for serde_json::Error
153impl RetryableError for serde_json::Error {
154 fn is_retryable(&self) -> bool {
155 // JSON parsing errors are typically not retryable
156 false
157 }
158}