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}