Skip to main content

tower_resilience_bulkhead/
error.rs

1//! Error types for bulkhead pattern.
2
3use tower_resilience_core::ResilienceError;
4
5/// Errors that can occur when using a bulkhead.
6#[derive(Debug, Clone, thiserror::Error)]
7pub enum BulkheadError {
8    /// The bulkhead rejected the call because it's at capacity.
9    #[error("bulkhead is full: max concurrent calls ({max_concurrent_calls}) reached")]
10    BulkheadFull {
11        /// Maximum concurrent calls allowed.
12        max_concurrent_calls: usize,
13    },
14    /// Timeout waiting for a permit.
15    #[error("timeout waiting for bulkhead permit")]
16    Timeout,
17}
18
19/// Result type for bulkhead operations.
20pub type Result<T> = std::result::Result<T, BulkheadError>;
21
22/// Service-level error that wraps inner service errors.
23///
24/// This error type is returned by the [`Bulkhead`](crate::Bulkhead) service and
25/// allows services with any error type to be wrapped without requiring
26/// `From<BulkheadError>` implementations.
27///
28/// # Examples
29///
30/// ```rust
31/// use tower_resilience_bulkhead::BulkheadServiceError;
32///
33/// // Match on the error to determine the cause
34/// fn handle_error<E: std::fmt::Debug>(err: BulkheadServiceError<E>) {
35///     match err {
36///         BulkheadServiceError::Bulkhead(e) => {
37///             println!("Bulkhead error: {}", e);
38///         }
39///         BulkheadServiceError::Inner(e) => {
40///             println!("Inner service error: {:?}", e);
41///         }
42///     }
43/// }
44/// ```
45#[derive(Debug)]
46pub enum BulkheadServiceError<E> {
47    /// Bulkhead-specific error (full or timeout).
48    Bulkhead(BulkheadError),
49    /// Error from the inner service.
50    Inner(E),
51}
52
53impl<E> BulkheadServiceError<E> {
54    /// Returns true if this is a bulkhead-specific error.
55    pub fn is_bulkhead(&self) -> bool {
56        matches!(self, BulkheadServiceError::Bulkhead(_))
57    }
58
59    /// Returns true if this is an inner service error.
60    pub fn is_inner(&self) -> bool {
61        matches!(self, BulkheadServiceError::Inner(_))
62    }
63
64    /// Converts this error into the inner error, if any.
65    pub fn into_inner(self) -> Option<E> {
66        match self {
67            BulkheadServiceError::Bulkhead(_) => None,
68            BulkheadServiceError::Inner(e) => Some(e),
69        }
70    }
71
72    /// Returns a reference to the bulkhead error, if any.
73    pub fn bulkhead_error(&self) -> Option<&BulkheadError> {
74        match self {
75            BulkheadServiceError::Bulkhead(e) => Some(e),
76            BulkheadServiceError::Inner(_) => None,
77        }
78    }
79}
80
81impl<E: std::fmt::Display> std::fmt::Display for BulkheadServiceError<E> {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        match self {
84            BulkheadServiceError::Bulkhead(e) => write!(f, "{}", e),
85            BulkheadServiceError::Inner(e) => write!(f, "inner service error: {}", e),
86        }
87    }
88}
89
90impl<E: std::error::Error + 'static> std::error::Error for BulkheadServiceError<E> {
91    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
92        match self {
93            BulkheadServiceError::Bulkhead(e) => Some(e),
94            BulkheadServiceError::Inner(e) => Some(e),
95        }
96    }
97}
98
99impl<E> From<BulkheadError> for BulkheadServiceError<E> {
100    fn from(err: BulkheadError) -> Self {
101        BulkheadServiceError::Bulkhead(err)
102    }
103}
104
105// Conversion to ResilienceError for zero-boilerplate error handling
106impl<E> From<BulkheadError> for ResilienceError<E> {
107    fn from(err: BulkheadError) -> Self {
108        match err {
109            BulkheadError::Timeout => ResilienceError::Timeout { layer: "bulkhead" },
110            BulkheadError::BulkheadFull {
111                max_concurrent_calls,
112            } => ResilienceError::BulkheadFull {
113                concurrent_calls: max_concurrent_calls,
114                max_concurrent: max_concurrent_calls,
115            },
116        }
117    }
118}
119
120impl<E> From<BulkheadServiceError<E>> for ResilienceError<E> {
121    fn from(err: BulkheadServiceError<E>) -> Self {
122        match err {
123            BulkheadServiceError::Bulkhead(e) => e.into(),
124            BulkheadServiceError::Inner(e) => ResilienceError::Application(e),
125        }
126    }
127}
128
129// Flattening conversion: when inner error is already ResilienceError<E>,
130// pass it through instead of double-wrapping in Application(ResilienceError<E>).
131// This enables idempotent .unified() composition across multiple layers.
132impl<E> From<BulkheadServiceError<ResilienceError<E>>> for ResilienceError<E> {
133    fn from(err: BulkheadServiceError<ResilienceError<E>>) -> Self {
134        match err {
135            BulkheadServiceError::Bulkhead(e) => e.into(),
136            BulkheadServiceError::Inner(re) => re,
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    /// Compile-time assertion that BulkheadError is Send + Sync + 'static.
146    /// This is required for compatibility with tower's BoxError.
147    const _: () = {
148        const fn assert_send_sync_static<T: Send + Sync + 'static>() {}
149        assert_send_sync_static::<BulkheadError>();
150    };
151
152    #[test]
153    fn test_into_box_error() {
154        let err = BulkheadError::BulkheadFull {
155            max_concurrent_calls: 10,
156        };
157        let boxed: Box<dyn std::error::Error + Send + Sync> = Box::new(err);
158        assert!(boxed.to_string().contains("bulkhead is full"));
159    }
160
161    #[test]
162    fn test_timeout_into_box_error() {
163        let err = BulkheadError::Timeout;
164        let boxed: Box<dyn std::error::Error + Send + Sync> = Box::new(err);
165        assert!(boxed.to_string().contains("timeout"));
166    }
167}