Skip to main content

throttle_net/
error.rs

1//! The domain error type.
2//!
3//! The acquire path is mostly infallible: it returns a [`Decision`](crate::Decision)
4//! or, for the waiting surface, simply succeeds once tokens are free. The one
5//! failure that no amount of waiting can fix is a request whose cost exceeds the
6//! limiter's capacity — that is reported as a [`ThrottleError`] rather than left
7//! to spin forever.
8//!
9//! [`ThrottleError`] implements [`error_forge::ForgeError`], so it carries the
10//! same kind/retryability metadata as every other domain error in the portfolio
11//! stack.
12
13use core::fmt;
14
15use error_forge::ForgeError;
16
17/// An acquisition that cannot complete.
18///
19/// The enum is `#[non_exhaustive]`: later phases introduce new failure modes
20/// (deadlines, a tripped circuit breaker, a closed limiter), so a `match` on it
21/// must include a wildcard arm.
22///
23/// # Examples
24///
25/// ```
26/// # async fn run() {
27/// use throttle_net::{Throttle, ThrottleError};
28///
29/// // Capacity is 5; asking for 9 can never be satisfied.
30/// let throttle = Throttle::per_second(5);
31/// let err = throttle.acquire_with_cost(9).await.unwrap_err();
32/// assert!(matches!(err, ThrottleError::CostExceedsCapacity { cost: 9, capacity: 5 }));
33/// # }
34/// ```
35#[non_exhaustive]
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ThrottleError {
38    /// The requested cost is larger than the limiter's capacity, so the bucket
39    /// can never hold enough tokens to grant it. Reduce the cost or raise the
40    /// limiter's capacity. This is a configuration mismatch, not a transient
41    /// condition, so it is **not** retryable.
42    CostExceedsCapacity {
43        /// The number of tokens the caller asked for.
44        cost: u32,
45        /// The limiter's maximum capacity, which `cost` exceeded.
46        capacity: u32,
47    },
48}
49
50impl fmt::Display for ThrottleError {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            Self::CostExceedsCapacity { cost, capacity } => write!(
54                f,
55                "requested cost {cost} exceeds limiter capacity {capacity}; it can never be granted"
56            ),
57        }
58    }
59}
60
61impl std::error::Error for ThrottleError {}
62
63impl ForgeError for ThrottleError {
64    fn kind(&self) -> &'static str {
65        match self {
66            Self::CostExceedsCapacity { .. } => "CostExceedsCapacity",
67        }
68    }
69
70    fn caption(&self) -> &'static str {
71        "Throttle acquisition error"
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::ThrottleError;
78    use error_forge::ForgeError;
79
80    #[test]
81    fn test_display_names_both_values() {
82        let msg = ThrottleError::CostExceedsCapacity {
83            cost: 9,
84            capacity: 5,
85        }
86        .to_string();
87        assert!(msg.contains('9'));
88        assert!(msg.contains('5'));
89    }
90
91    #[test]
92    fn test_forge_kind_matches_variant() {
93        let err = ThrottleError::CostExceedsCapacity {
94            cost: 1,
95            capacity: 0,
96        };
97        assert_eq!(err.kind(), "CostExceedsCapacity");
98    }
99
100    #[test]
101    fn test_capacity_mismatch_is_not_retryable() {
102        // Retrying the same oversized cost on the same limiter never succeeds.
103        let err = ThrottleError::CostExceedsCapacity {
104            cost: 9,
105            capacity: 5,
106        };
107        assert!(!err.is_retryable());
108    }
109}