cloud_sdk/rate_limit/
quota.rs1use core::fmt;
2
3use super::{DelaySeconds, QuotaExtension, WallClockTimestamp};
4
5pub const MAX_QUOTA_BUCKETS: usize = 8;
7pub const MAX_QUOTA_EXTENSIONS_PER_BUCKET: usize = 4;
9pub const MAX_QUOTA_BUCKET_ID_BYTES: usize = 64;
11
12#[derive(Clone, Copy, Eq, PartialEq)]
14pub struct QuotaBucketId {
15 bytes: [u8; MAX_QUOTA_BUCKET_ID_BYTES],
16 len: u8,
17}
18
19impl QuotaBucketId {
20 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 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub enum QuotaReset {
66 After(DelaySeconds),
68 At(WallClockTimestamp),
70 Unknown,
72}
73
74#[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 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 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 #[must_use]
132 pub const fn id(&self) -> QuotaBucketId {
133 self.id
134 }
135 #[must_use]
137 pub const fn limit(&self) -> u64 {
138 self.limit
139 }
140 #[must_use]
142 pub const fn remaining(&self) -> u64 {
143 self.remaining
144 }
145 #[must_use]
147 pub const fn reset(&self) -> QuotaReset {
148 self.reset
149 }
150 #[must_use]
152 pub const fn is_exhausted(&self) -> bool {
153 self.remaining == 0
154 }
155 pub fn extensions(&self) -> impl Iterator<Item = &QuotaExtension> {
157 self.extensions.iter().filter_map(Option::as_ref)
158 }
159}
160
161#[derive(Clone, Debug, Eq, PartialEq)]
176pub struct QuotaBuckets {
177 buckets: [Option<QuotaBucket>; MAX_QUOTA_BUCKETS],
178 len: u8,
179}
180
181impl QuotaBuckets {
182 #[must_use]
184 pub const fn new() -> Self {
185 Self {
186 buckets: [const { None }; MAX_QUOTA_BUCKETS],
187 len: 0,
188 }
189 }
190
191 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 #[must_use]
208 pub const fn len(&self) -> usize {
209 self.len as usize
210 }
211 #[must_use]
213 pub const fn is_empty(&self) -> bool {
214 self.len == 0
215 }
216 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
230pub enum QuotaError {
231 BucketIdEmpty,
233 BucketIdTooLong,
235 InvalidBucketId,
237 LimitZero,
239 RemainingExceedsLimit,
241 TooManyBuckets,
243 DuplicateBucket,
245 TooManyExtensions,
247 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);