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}