Skip to main content

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}