qubit_budget/resource/error/budget_error.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Defines the aggregate error for finite resource constraints.
9
10use std::fmt::Debug;
11
12use thiserror::Error;
13
14use super::InsufficientBudgetError;
15use super::LimitExceededError;
16use crate::resource::Observation;
17
18/// Aggregate error for APIs that can perform both point and cumulative checks.
19///
20/// APIs with a single failure mode return [`LimitExceededError`] or
21/// [`InsufficientBudgetError`] directly. This type remains the common carrier
22/// for composite operations and measured-value errors. Releasable pool release
23/// failures use the separate [`crate::ResourceReleaseError`] type.
24///
25/// # Type Parameters
26///
27/// * `R` - Caller-defined resource value retained for diagnostics.
28/// * `Q` - Copyable measurement value used by the failed constraint.
29///
30/// # Examples
31///
32/// ```
33/// use qubit_budget::BudgetError;
34/// use qubit_budget::LimitExceededError;
35///
36/// let error = BudgetError::from(LimitExceededError::exact("depth", 3_u64, 2));
37/// assert_eq!(error.configured_limit(), 2);
38/// assert_eq!(error.exact_observed(), Some(3));
39/// ```
40#[must_use]
41#[derive(Debug, Error, Clone, PartialEq, Eq)]
42pub enum BudgetError<R, Q = u64>
43where
44 Q: Copy + Debug,
45{
46 /// A point measurement exceeded its configured maximum.
47 #[error("resource {resource:?} measured {observed}, exceeding the maximum of {maximum:?}")]
48 LimitExceeded {
49 /// Resource associated with the failed point check.
50 resource: R,
51 /// Observed point measurement or safe lower bound.
52 observed: Observation<Q>,
53 /// Configured inclusive point maximum.
54 maximum: Q,
55 },
56
57 /// A cumulative consumption request exceeded the remaining capacity.
58 #[error("resource {resource:?} requested {requested:?}, but only {remaining:?} of {limit:?} remains")]
59 Insufficient {
60 /// Resource associated with the failed consumption request.
61 resource: R,
62 /// Configured finite limit.
63 limit: Q,
64 /// Capacity remaining before the failed request.
65 remaining: Q,
66 /// Quantity requested by the failed operation.
67 requested: Q,
68 },
69}
70
71impl<R, Q> BudgetError<R, Q>
72where
73 Q: Copy + Debug,
74{
75 /// Returns the resource associated with this failure.
76 ///
77 /// # Returns
78 ///
79 /// Returns the resource associated with this failure.
80 #[must_use]
81 #[inline(always)]
82 pub const fn resource(&self) -> &R {
83 match self {
84 Self::LimitExceeded { resource, .. } | Self::Insufficient { resource, .. } => resource,
85 }
86 }
87
88 /// Consumes this error and returns its associated resource.
89 ///
90 /// # Returns
91 ///
92 /// Consumes this error and returns its associated resource.
93 #[inline(always)]
94 #[must_use]
95 pub fn into_resource(self) -> R {
96 match self {
97 Self::LimitExceeded { resource, .. } | Self::Insufficient { resource, .. } => resource,
98 }
99 }
100
101 /// Returns the cumulative limit for budget and pool failures.
102 ///
103 /// Returns `Some(limit)` for [`Self::Insufficient`], or `None` for a
104 /// point-limit failure.
105 ///
106 /// # Returns
107 ///
108 /// Returns the cumulative limit for budget and pool failures.
109 #[must_use]
110 #[inline(always)]
111 pub const fn limit(&self) -> Option<Q> {
112 match self {
113 Self::LimitExceeded { .. } => None,
114 Self::Insufficient { limit, .. } => Some(*limit),
115 }
116 }
117
118 /// Returns the observation for a point-limit failure.
119 ///
120 /// Returns `Some(observed)` for [`Self::LimitExceeded`], or `None` for a
121 /// cumulative-budget or pool failure.
122 ///
123 /// # Returns
124 ///
125 /// Returns the observation for a point-limit failure.
126 #[must_use]
127 #[inline(always)]
128 pub const fn observation(&self) -> Option<Observation<Q>> {
129 match self {
130 Self::LimitExceeded { observed, .. } => Some(*observed),
131 Self::Insufficient { .. } => None,
132 }
133 }
134
135 /// Returns the exact point measurement when the observation is exact.
136 ///
137 /// # Returns
138 ///
139 /// Returns the exact point measurement when the observation is exact.
140 ///
141 /// `None` indicates that the observation is only a lower bound.
142 #[inline(always)]
143 #[must_use]
144 pub const fn exact_observed(&self) -> Option<Q> {
145 match self.observation() {
146 Some(Observation::Exact(value)) => Some(value),
147 Some(Observation::AtLeast(_)) | None => None,
148 }
149 }
150
151 /// Returns the safe lower bound of a point measurement.
152 ///
153 /// # Returns
154 ///
155 /// Returns the safe lower bound of a point measurement.
156 ///
157 /// `None` indicates that this is a cumulative-budget failure rather than a
158 /// point-limit failure.
159 #[inline(always)]
160 #[must_use]
161 pub const fn observed_lower_bound(&self) -> Option<Q> {
162 match self.observation() {
163 Some(observed) => Some(observed.lower_bound()),
164 None => None,
165 }
166 }
167
168 /// Returns the configured maximum for a point-limit failure.
169 ///
170 /// Returns `Some(maximum)` for [`Self::LimitExceeded`], or `None` for a
171 /// cumulative-budget or pool failure.
172 ///
173 /// # Returns
174 ///
175 /// Returns the configured maximum for a point-limit failure.
176 #[inline(always)]
177 #[must_use]
178 pub const fn maximum(&self) -> Option<Q> {
179 match self {
180 Self::LimitExceeded { maximum, .. } => Some(*maximum),
181 Self::Insufficient { .. } => None,
182 }
183 }
184
185 /// Returns the remaining capacity for a cumulative-budget failure.
186 ///
187 /// Returns `Some(remaining)` for [`Self::Insufficient`], including failed
188 /// pool acquisitions, or `None` for a point-limit failure.
189 ///
190 /// # Returns
191 ///
192 /// Returns the remaining capacity for a cumulative-budget failure.
193 #[inline(always)]
194 #[must_use]
195 pub const fn remaining(&self) -> Option<Q> {
196 match self {
197 Self::Insufficient { remaining, .. } => Some(*remaining),
198 Self::LimitExceeded { .. } => None,
199 }
200 }
201
202 /// Returns the requested quantity for a cumulative budget failure.
203 ///
204 /// Returns `Some(requested)` for [`Self::Insufficient`], or `None` for a
205 /// point-limit failure.
206 ///
207 /// # Returns
208 ///
209 /// Returns the requested quantity for a cumulative budget failure.
210 #[inline(always)]
211 #[must_use]
212 pub const fn requested(&self) -> Option<Q> {
213 match self {
214 Self::LimitExceeded { .. } => None,
215 Self::Insufficient { requested, .. } => Some(*requested),
216 }
217 }
218}
219
220impl<R, Q> BudgetError<R, Q>
221where
222 Q: crate::ResourceQuantity,
223{
224 /// Returns the configured limit for either point or cumulative failures.
225 ///
226 /// # Returns
227 ///
228 /// Returns the configured limit for either point or cumulative failures.
229 #[must_use]
230 #[inline]
231 pub const fn configured_limit(&self) -> Q {
232 match self {
233 Self::LimitExceeded { maximum, .. } => *maximum,
234 Self::Insufficient { limit, .. } => *limit,
235 }
236 }
237
238 /// Returns cumulative usage before a failed request, when applicable.
239 ///
240 /// # Returns
241 ///
242 /// Returns cumulative usage before a failed request, when applicable.
243 ///
244 /// `None` indicates that this is a point-limit failure with no cumulative
245 /// usage.
246 #[must_use]
247 #[inline]
248 pub fn used(&self) -> Option<Q> {
249 match self {
250 Self::LimitExceeded { .. } => None,
251 Self::Insufficient { limit, remaining, .. } => Some(*limit - *remaining),
252 }
253 }
254}
255
256impl<R, Q> From<LimitExceededError<R, Q>> for BudgetError<R, Q>
257where
258 Q: Copy + Debug,
259{
260 /// Converts a precise point-limit failure into an aggregate error.
261 ///
262 /// # Parameters
263 ///
264 /// * `error` - Precise point-limit failure to convert.
265 ///
266 /// # Returns
267 ///
268 /// Converts a precise point-limit failure into an aggregate error.
269 #[inline(always)]
270 fn from(error: LimitExceededError<R, Q>) -> Self {
271 let LimitExceededError {
272 resource,
273 observed,
274 maximum,
275 } = error;
276 Self::LimitExceeded {
277 resource,
278 observed,
279 maximum,
280 }
281 }
282}
283
284impl<R, Q> From<InsufficientBudgetError<R, Q>> for BudgetError<R, Q>
285where
286 Q: Copy + Debug,
287{
288 /// Converts a precise cumulative-budget failure into an aggregate error.
289 ///
290 /// # Parameters
291 ///
292 /// * `error` - Precise cumulative-budget failure to convert.
293 ///
294 /// # Returns
295 ///
296 /// Converts a precise cumulative-budget failure into an aggregate error.
297 #[inline(always)]
298 fn from(error: InsufficientBudgetError<R, Q>) -> Self {
299 let InsufficientBudgetError {
300 resource,
301 limit,
302 remaining,
303 requested,
304 } = error;
305 Self::Insufficient {
306 resource,
307 limit,
308 remaining,
309 requested,
310 }
311 }
312}