Skip to main content

cloud_sdk/rate_limit/
quota.rs

1use core::fmt;
2
3use super::{DelaySeconds, QuotaExtension, WallClockTimestamp};
4
5/// Maximum provider quota buckets retained from one response.
6pub const MAX_QUOTA_BUCKETS: usize = 8;
7/// Maximum informational extensions retained per quota bucket.
8pub const MAX_QUOTA_EXTENSIONS_PER_BUCKET: usize = 4;
9/// Maximum exact-byte provider quota bucket identity length.
10pub const MAX_QUOTA_BUCKET_ID_BYTES: usize = 64;
11
12/// Provider-defined quota bucket identity in fixed-capacity storage.
13#[derive(Clone, Copy, Eq, PartialEq)]
14pub struct QuotaBucketId {
15    bytes: [u8; MAX_QUOTA_BUCKET_ID_BYTES],
16    len: u8,
17}
18
19impl QuotaBucketId {
20    /// Copies one stable ASCII bucket identity.
21    pub fn new(value: &[u8]) -> Result<Self, QuotaError> {
22        if value.is_empty() {
23            return Err(QuotaError::BucketIdEmpty);
24        }
25        if value.len() > MAX_QUOTA_BUCKET_ID_BYTES {
26            return Err(QuotaError::BucketIdTooLong);
27        }
28        if !value
29            .iter()
30            .all(|byte| byte.is_ascii_alphanumeric() || b"-_.:/".contains(byte))
31        {
32            return Err(QuotaError::InvalidBucketId);
33        }
34        let mut result = Self {
35            bytes: [0; MAX_QUOTA_BUCKET_ID_BYTES],
36            len: 0,
37        };
38        let target = result
39            .bytes
40            .get_mut(..value.len())
41            .ok_or(QuotaError::BucketIdTooLong)?;
42        target.copy_from_slice(value);
43        result.len = u8::try_from(value.len()).map_err(|_| QuotaError::BucketIdTooLong)?;
44        Ok(result)
45    }
46
47    /// Returns the exact provider bucket identity.
48    #[must_use]
49    pub fn as_bytes(&self) -> &[u8] {
50        self.bytes.get(..usize::from(self.len)).unwrap_or_default()
51    }
52}
53
54impl fmt::Debug for QuotaBucketId {
55    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56        formatter
57            .debug_tuple("QuotaBucketId")
58            .field(&self.as_bytes())
59            .finish()
60    }
61}
62
63/// Reset semantics for one provider quota bucket.
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub enum QuotaReset {
66    /// Relative delay from the caller-supplied observation time.
67    After(DelaySeconds),
68    /// Absolute Unix wall-clock reset timestamp.
69    At(WallClockTimestamp),
70    /// Provider exposed the bucket without actionable reset metadata.
71    Unknown,
72}
73
74/// One coherent provider quota bucket.
75#[derive(Clone, Debug, Eq, PartialEq)]
76pub struct QuotaBucket {
77    id: QuotaBucketId,
78    limit: u64,
79    remaining: u64,
80    reset: QuotaReset,
81    extensions: [Option<QuotaExtension>; MAX_QUOTA_EXTENSIONS_PER_BUCKET],
82    extension_len: u8,
83}
84
85impl QuotaBucket {
86    /// Creates a bucket with no informational extensions.
87    pub const fn new(
88        id: QuotaBucketId,
89        limit: u64,
90        remaining: u64,
91        reset: QuotaReset,
92    ) -> Result<Self, QuotaError> {
93        if limit == 0 {
94            return Err(QuotaError::LimitZero);
95        }
96        if remaining > limit {
97            return Err(QuotaError::RemainingExceedsLimit);
98        }
99        Ok(Self {
100            id,
101            limit,
102            remaining,
103            reset,
104            extensions: [const { None }; MAX_QUOTA_EXTENSIONS_PER_BUCKET],
105            extension_len: 0,
106        })
107    }
108
109    /// Appends one unique informational extension.
110    pub fn try_add_extension(&mut self, extension: QuotaExtension) -> Result<(), QuotaError> {
111        if self
112            .extensions()
113            .any(|current| current.name() == extension.name())
114        {
115            return Err(QuotaError::DuplicateExtension);
116        }
117        let index = usize::from(self.extension_len);
118        let slot = self
119            .extensions
120            .get_mut(index)
121            .ok_or(QuotaError::TooManyExtensions)?;
122        *slot = Some(extension);
123        self.extension_len = self
124            .extension_len
125            .checked_add(1)
126            .ok_or(QuotaError::TooManyExtensions)?;
127        Ok(())
128    }
129
130    /// Returns the bucket identity.
131    #[must_use]
132    pub const fn id(&self) -> QuotaBucketId {
133        self.id
134    }
135    /// Returns the request limit.
136    #[must_use]
137    pub const fn limit(&self) -> u64 {
138        self.limit
139    }
140    /// Returns the remaining request count.
141    #[must_use]
142    pub const fn remaining(&self) -> u64 {
143        self.remaining
144    }
145    /// Returns the reset semantics.
146    #[must_use]
147    pub const fn reset(&self) -> QuotaReset {
148        self.reset
149    }
150    /// Reports whether this bucket is exhausted.
151    #[must_use]
152    pub const fn is_exhausted(&self) -> bool {
153        self.remaining == 0
154    }
155    /// Iterates retained informational extensions.
156    pub fn extensions(&self) -> impl Iterator<Item = &QuotaExtension> {
157        self.extensions.iter().filter_map(Option::as_ref)
158    }
159}
160
161/// Fixed-capacity set of distinct provider quota buckets.
162///
163/// The aggregate is deliberately not [`Copy`] because its bounded inline
164/// storage can be several kilobytes. Clone it only when a separate owned
165/// snapshot is required.
166///
167/// ```compile_fail
168/// use cloud_sdk::rate_limit::QuotaBuckets;
169///
170/// let buckets = QuotaBuckets::new();
171/// let moved = buckets;
172/// let _ = buckets.len();
173/// let _ = moved;
174/// ```
175#[derive(Clone, Debug, Eq, PartialEq)]
176pub struct QuotaBuckets {
177    buckets: [Option<QuotaBucket>; MAX_QUOTA_BUCKETS],
178    len: u8,
179}
180
181impl QuotaBuckets {
182    /// Creates an empty quota set.
183    #[must_use]
184    pub const fn new() -> Self {
185        Self {
186            buckets: [const { None }; MAX_QUOTA_BUCKETS],
187            len: 0,
188        }
189    }
190
191    /// Appends one bucket atomically.
192    pub fn try_push(&mut self, bucket: QuotaBucket) -> Result<(), QuotaError> {
193        if self.iter().any(|current| current.id == bucket.id) {
194            return Err(QuotaError::DuplicateBucket);
195        }
196        let index = usize::from(self.len);
197        let slot = self
198            .buckets
199            .get_mut(index)
200            .ok_or(QuotaError::TooManyBuckets)?;
201        *slot = Some(bucket);
202        self.len = self.len.checked_add(1).ok_or(QuotaError::TooManyBuckets)?;
203        Ok(())
204    }
205
206    /// Returns the number of retained buckets.
207    #[must_use]
208    pub const fn len(&self) -> usize {
209        self.len as usize
210    }
211    /// Reports whether no quota bucket was supplied.
212    #[must_use]
213    pub const fn is_empty(&self) -> bool {
214        self.len == 0
215    }
216    /// Iterates retained buckets in provider order.
217    pub fn iter(&self) -> impl Iterator<Item = &QuotaBucket> {
218        self.buckets.iter().filter_map(Option::as_ref)
219    }
220}
221
222impl Default for QuotaBuckets {
223    fn default() -> Self {
224        Self::new()
225    }
226}
227
228/// Invalid quota bucket or bounded collection.
229#[derive(Clone, Copy, Debug, Eq, PartialEq)]
230pub enum QuotaError {
231    /// A bucket identity was empty.
232    BucketIdEmpty,
233    /// A bucket identity exceeded its fixed bound.
234    BucketIdTooLong,
235    /// A bucket identity contained unsupported bytes.
236    InvalidBucketId,
237    /// A bucket limit was zero.
238    LimitZero,
239    /// Remaining requests exceeded the bucket limit.
240    RemainingExceedsLimit,
241    /// More than the bounded number of buckets was supplied.
242    TooManyBuckets,
243    /// A bucket identity appeared more than once.
244    DuplicateBucket,
245    /// More than the bounded number of extensions was supplied.
246    TooManyExtensions,
247    /// An extension name appeared more than once in a bucket.
248    DuplicateExtension,
249}
250
251impl_static_error!(QuotaError,
252    Self::BucketIdEmpty => "quota bucket identity is empty",
253    Self::BucketIdTooLong => "quota bucket identity exceeds its length limit",
254    Self::InvalidBucketId => "quota bucket identity contains an invalid byte",
255    Self::LimitZero => "quota bucket limit must be nonzero",
256    Self::RemainingExceedsLimit => "quota bucket remaining count exceeds its limit",
257    Self::TooManyBuckets => "quota bucket count exceeds its fixed capacity",
258    Self::DuplicateBucket => "quota bucket identity is duplicated",
259    Self::TooManyExtensions => "quota extension count exceeds its fixed capacity",
260    Self::DuplicateExtension => "quota extension name is duplicated",
261);