error_rail/traits/
transient.rs

1//! Transient error classification for retry strategies.
2//!
3//! This module provides the [`TransientError`] trait for classifying errors
4//! as transient (temporary, potentially recoverable by retry) or permanent.
5//!
6//! # Integration with External Libraries
7//!
8//! error-rail itself does not implement retry/circuit-breaker logic. Instead,
9//! it provides this trait to easily integrate with external resilience libraries
10//! like `backoff`, `retry`, or custom implementations.
11//!
12//! # Examples
13//!
14//! ```
15//! use error_rail::traits::TransientError;
16//!
17//! #[derive(Debug)]
18//! enum MyError {
19//!     NetworkTimeout,
20//!     RateLimited { retry_after_ms: u64 },
21//!     InvalidInput,
22//!     DatabaseError,
23//! }
24//!
25//! impl TransientError for MyError {
26//!     fn is_transient(&self) -> bool {
27//!         matches!(self, MyError::NetworkTimeout | MyError::RateLimited { .. })
28//!     }
29//!
30//!     fn retry_after_hint(&self) -> Option<core::time::Duration> {
31//!         match self {
32//!             MyError::RateLimited { retry_after_ms } => {
33//!                 Some(core::time::Duration::from_millis(*retry_after_ms))
34//!             }
35//!             _ => None,
36//!         }
37//!     }
38//! }
39//! ```
40
41use core::time::Duration;
42
43/// Classification of errors as transient or permanent.
44///
45/// Transient errors are temporary failures that may succeed if retried,
46/// such as network timeouts, rate limiting, or temporary service unavailability.
47///
48/// This trait enables integration with external retry/circuit-breaker libraries
49/// by providing a standard interface for error classification.
50///
51/// # Design Philosophy
52///
53/// error-rail follows the principle of composition over built-in features.
54/// Rather than implementing full retry logic, we provide this classification
55/// trait that works seamlessly with:
56///
57/// - [`ErrorPipeline::should_retry`](crate::ErrorPipeline::should_retry)
58/// - External crates like `backoff`, `retry`, `again`
59/// - Custom retry implementations
60///
61/// # Examples
62///
63/// ## Basic Implementation
64///
65/// ```
66/// use error_rail::traits::TransientError;
67///
68/// #[derive(Debug)]
69/// struct TimeoutError;
70///
71/// impl TransientError for TimeoutError {
72///     fn is_transient(&self) -> bool {
73///         true // Timeouts are always transient
74///     }
75/// }
76/// ```
77///
78/// ## With Retry Hint
79///
80/// ```
81/// use error_rail::traits::TransientError;
82/// use core::time::Duration;
83///
84/// #[derive(Debug)]
85/// struct RateLimitError {
86///     retry_after_secs: u64,
87/// }
88///
89/// impl TransientError for RateLimitError {
90///     fn is_transient(&self) -> bool {
91///         true
92///     }
93///
94///     fn retry_after_hint(&self) -> Option<Duration> {
95///         Some(Duration::from_secs(self.retry_after_secs))
96///     }
97/// }
98/// ```
99pub trait TransientError {
100    /// Returns `true` if this error is transient and may succeed on retry.
101    ///
102    /// # Guidelines
103    ///
104    /// Return `true` for:
105    /// - Network timeouts
106    /// - Rate limiting (HTTP 429)
107    /// - Service temporarily unavailable (HTTP 503)
108    /// - Connection reset/refused (may indicate temporary overload)
109    /// - Deadlock/lock contention errors
110    ///
111    /// Return `false` for:
112    /// - Authentication/authorization failures
113    /// - Invalid input/validation errors
114    /// - Resource not found
115    /// - Business logic violations
116    fn is_transient(&self) -> bool;
117
118    /// Returns `true` if this error is permanent and should not be retried.
119    ///
120    /// Default implementation returns `!self.is_transient()`.
121    #[inline]
122    fn is_permanent(&self) -> bool {
123        !self.is_transient()
124    }
125
126    /// Optional hint for how long to wait before retrying.
127    ///
128    /// This is useful for rate-limiting errors that include a `Retry-After` header.
129    /// External retry libraries can use this to implement respectful backoff.
130    ///
131    /// Returns `None` by default, indicating no specific wait time is suggested.
132    #[inline]
133    fn retry_after_hint(&self) -> Option<Duration> {
134        None
135    }
136
137    /// Returns the maximum number of retry attempts for this error.
138    ///
139    /// Returns `None` by default, indicating no specific limit is suggested.
140    /// External libraries should use their own default limits.
141    #[inline]
142    fn max_retries_hint(&self) -> Option<u32> {
143        None
144    }
145}
146
147/// Blanket implementation for standard I/O errors.
148#[cfg(feature = "std")]
149impl TransientError for std::io::Error {
150    fn is_transient(&self) -> bool {
151        use std::io::ErrorKind;
152        matches!(
153            self.kind(),
154            ErrorKind::ConnectionRefused
155                | ErrorKind::ConnectionReset
156                | ErrorKind::ConnectionAborted
157                | ErrorKind::TimedOut
158                | ErrorKind::Interrupted
159                | ErrorKind::WouldBlock
160        )
161    }
162}
163
164/// Extension methods for working with transient errors.
165pub trait TransientErrorExt<T, E: TransientError> {
166    /// Converts a transient error to `Some(Err(e))` for retry, or `None` to stop.
167    ///
168    /// This is useful for integrating with retry libraries that use `Option` to
169    /// signal whether to continue retrying.
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// use error_rail::traits::{TransientError, TransientErrorExt};
175    ///
176    /// #[derive(Debug)]
177    /// struct MyError { transient: bool }
178    /// impl TransientError for MyError {
179    ///     fn is_transient(&self) -> bool { self.transient }
180    /// }
181    ///
182    /// let transient_err: Result<(), MyError> = Err(MyError { transient: true });
183    /// assert!(transient_err.retry_if_transient().is_some());
184    ///
185    /// let permanent_err: Result<(), MyError> = Err(MyError { transient: false });
186    /// assert!(permanent_err.retry_if_transient().is_none());
187    /// ```
188    fn retry_if_transient(self) -> Option<Result<T, E>>;
189}
190
191impl<T, E: TransientError> TransientErrorExt<T, E> for Result<T, E> {
192    fn retry_if_transient(self) -> Option<Result<T, E>> {
193        match &self {
194            Ok(_) => None, // Success, no retry needed
195            Err(e) if e.is_transient() => Some(self),
196            Err(_) => None, // Permanent error, stop retrying
197        }
198    }
199}