1use std::{collections::VecDeque, fmt, time::Duration};
7
8use parking_lot::Mutex;
9use tokio::time::{Instant, sleep};
10
11use crate::Error;
12
13const DEFAULT_HISTORY_REQUESTS: usize = 50;
14const DEFAULT_HISTORY_WINDOW: Duration = Duration::from_secs(30);
15const DEFAULT_GENERAL_REQUESTS: usize = 200;
16const DEFAULT_GENERAL_WINDOW: Duration = Duration::from_mins(1);
17const MAX_INITIAL_CAPACITY: usize = 1_024;
18pub(crate) const MAX_COOLDOWN: Duration = Duration::from_hours(24);
30
31#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
33#[non_exhaustive]
34pub enum RateLimitKind {
35 History,
37 General,
39}
40
41impl fmt::Display for RateLimitKind {
42 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 Self::History => formatter.write_str("history"),
45 Self::General => formatter.write_str("general"),
46 }
47 }
48}
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
52#[non_exhaustive]
53pub struct RateLimit {
54 max_requests: usize,
55 window: Duration,
56}
57
58impl RateLimit {
59 pub fn new(max_requests: usize, window: Duration) -> Result<Self, Error> {
66 if max_requests == 0
67 || window.is_zero()
68 || std::time::Instant::now().checked_add(window).is_none()
69 {
70 return Err(Error::Configuration(
71 "rate limits require a positive request count and representable window".to_owned(),
72 ));
73 }
74 Ok(Self {
75 max_requests,
76 window,
77 })
78 }
79
80 const fn trusted(max_requests: usize, window: Duration) -> Self {
81 Self {
82 max_requests,
83 window,
84 }
85 }
86
87 #[must_use]
89 pub const fn max_requests(self) -> usize {
90 self.max_requests
91 }
92
93 #[must_use]
95 pub const fn window(self) -> Duration {
96 self.window
97 }
98}
99
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105#[non_exhaustive]
106pub struct RateLimitConfig {
107 history: RateLimit,
108 general: RateLimit,
109}
110
111impl RateLimitConfig {
112 #[must_use]
114 pub const fn new(history: RateLimit, general: RateLimit) -> Self {
115 Self { history, general }
116 }
117
118 #[must_use]
120 pub const fn history(self) -> RateLimit {
121 self.history
122 }
123
124 #[must_use]
126 pub const fn general(self) -> RateLimit {
127 self.general
128 }
129
130 pub(crate) const fn limit(self, kind: RateLimitKind) -> RateLimit {
131 match kind {
132 RateLimitKind::History => self.history,
133 RateLimitKind::General => self.general,
134 }
135 }
136}
137
138impl Default for RateLimitConfig {
139 fn default() -> Self {
140 Self {
141 history: RateLimit::trusted(DEFAULT_HISTORY_REQUESTS, DEFAULT_HISTORY_WINDOW),
142 general: RateLimit::trusted(DEFAULT_GENERAL_REQUESTS, DEFAULT_GENERAL_WINDOW),
143 }
144 }
145}
146
147pub(crate) struct RateLimits {
148 config: Option<RateLimitConfig>,
149 history: Option<WindowLimiter>,
150 general: Option<WindowLimiter>,
151}
152
153impl RateLimits {
154 pub(crate) fn new(config: Option<RateLimitConfig>) -> Self {
155 let history = config.map(|limits| WindowLimiter::new(limits.history));
156 let general = config.map(|limits| WindowLimiter::new(limits.general));
157 Self {
158 config,
159 history,
160 general,
161 }
162 }
163
164 pub(crate) const fn config(&self) -> Option<RateLimitConfig> {
165 self.config
166 }
167
168 pub(crate) fn limit(&self, kind: RateLimitKind) -> RateLimit {
169 self.config.unwrap_or_default().limit(kind)
170 }
171
172 pub(crate) async fn wait(&self, kind: RateLimitKind) {
173 let Some(limiter) = self.limiter(kind) else {
174 return;
175 };
176 loop {
177 match limiter.try_acquire(Instant::now()) {
178 Ok(()) => return,
179 Err(retry_after) => sleep(retry_after).await,
180 }
181 }
182 }
183
184 pub(crate) fn try_acquire(&self, kind: RateLimitKind) -> Result<(), Duration> {
185 self.limiter(kind)
186 .map_or(Ok(()), |limiter| limiter.try_acquire(Instant::now()))
187 }
188
189 pub(crate) fn cool_down(&self, kind: RateLimitKind, retry_after: Duration) {
190 if let Some(limiter) = self.limiter(kind) {
191 limiter.cool_down(Instant::now(), retry_after);
192 }
193 }
194
195 fn limiter(&self, kind: RateLimitKind) -> Option<&WindowLimiter> {
196 match kind {
197 RateLimitKind::History => self.history.as_ref(),
198 RateLimitKind::General => self.general.as_ref(),
199 }
200 }
201}
202
203struct WindowLimiter {
204 limit: RateLimit,
205 state: Mutex<WindowState>,
206}
207
208impl WindowLimiter {
209 fn new(limit: RateLimit) -> Self {
210 Self {
211 limit,
212 state: Mutex::new(WindowState::with_capacity(
213 limit.max_requests.min(MAX_INITIAL_CAPACITY),
214 )),
215 }
216 }
217
218 fn try_acquire(&self, now: Instant) -> Result<(), Duration> {
219 let mut state = self.state.lock();
220 state.expire(now, self.limit.window);
221 let retry_after = state.retry_after(now, self.limit);
222 if retry_after.is_zero() {
223 state.admitted.push_back(now);
224 Ok(())
225 } else {
226 Err(retry_after)
227 }
228 }
229
230 fn cool_down(&self, now: Instant, retry_after: Duration) {
231 let deadline = saturating_deadline(now, retry_after.min(MAX_COOLDOWN));
232 let mut state = self.state.lock();
233 if state
234 .cooldown_until
235 .is_none_or(|current| deadline > current)
236 {
237 state.cooldown_until = Some(deadline);
238 }
239 }
240}
241
242struct WindowState {
243 admitted: VecDeque<Instant>,
244 cooldown_until: Option<Instant>,
245}
246
247impl WindowState {
248 fn with_capacity(capacity: usize) -> Self {
249 Self {
250 admitted: VecDeque::with_capacity(capacity),
251 cooldown_until: None,
252 }
253 }
254
255 fn expire(&mut self, now: Instant, window: Duration) {
256 while self
257 .admitted
258 .front()
259 .is_some_and(|admitted| now.saturating_duration_since(*admitted) >= window)
260 {
261 self.admitted.pop_front();
262 }
263 if self.cooldown_until.is_some_and(|deadline| deadline <= now) {
264 self.cooldown_until = None;
265 }
266 }
267
268 fn retry_after(&self, now: Instant, limit: RateLimit) -> Duration {
269 let cooldown = self.cooldown_until.map_or(Duration::ZERO, |deadline| {
270 deadline.saturating_duration_since(now)
271 });
272 let capacity = if self.admitted.len() < limit.max_requests {
273 Duration::ZERO
274 } else {
275 self.admitted.front().map_or(limit.window, |oldest| {
276 oldest
277 .checked_add(limit.window)
278 .map_or(limit.window, |deadline| {
279 deadline.saturating_duration_since(now)
280 })
281 })
282 };
283 cooldown.max(capacity)
284 }
285}
286
287fn saturating_deadline(now: Instant, duration: Duration) -> Instant {
292 if let Some(deadline) = now.checked_add(duration) {
293 return deadline;
294 }
295 let mut low = 0_u64;
305 let mut high = u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX);
306 let mut deadline = now;
307 while high - low > 1 {
308 let mid = u64::midpoint(low, high);
309 if let Some(probe) = now.checked_add(Duration::from_nanos(mid)) {
310 low = mid;
311 deadline = probe;
312 } else {
313 high = mid;
314 }
315 }
316 deadline
317}
318
319#[cfg(test)]
320mod tests {
321 use std::sync::Arc;
322
323 use tokio::time::{Instant, advance};
324
325 use super::*;
326
327 fn limits(requests: usize, window: Duration) -> Arc<RateLimits> {
328 let limit = RateLimit::new(requests, window)
329 .unwrap_or_else(|error| panic!("test rate limit must be valid: {error}"));
330 Arc::new(RateLimits::new(Some(RateLimitConfig::new(limit, limit))))
331 }
332
333 #[tokio::test(start_paused = true)]
334 async fn wait_resumes_when_the_rolling_window_releases_capacity() {
335 let window = Duration::from_secs(10);
336 let limits = limits(1, window);
337 assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
338
339 let started = Instant::now();
340 let waiting_limits = Arc::clone(&limits);
341 let waiter = tokio::spawn(async move {
342 waiting_limits.wait(RateLimitKind::General).await;
343 Instant::now()
344 });
345 tokio::task::yield_now().await;
346 assert!(!waiter.is_finished());
347
348 advance(window).await;
349 let admitted = waiter
350 .await
351 .unwrap_or_else(|error| panic!("waiting task must finish: {error}"));
352 assert_eq!(admitted.saturating_duration_since(started), window);
353 }
354
355 #[tokio::test(start_paused = true)]
356 async fn provider_cooldown_blocks_admission_until_its_deadline() {
357 let cooldown = Duration::from_secs(15);
358 let limits = limits(2, Duration::from_secs(30));
359 limits.cool_down(RateLimitKind::General, cooldown);
360
361 assert_eq!(limits.try_acquire(RateLimitKind::General), Err(cooldown));
362 advance(cooldown).await;
363 assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
364 }
365
366 #[tokio::test(start_paused = true)]
367 async fn an_oversized_cooldown_is_clamped_not_skipped() {
368 let limits = limits(2, Duration::from_secs(30));
369 limits.cool_down(RateLimitKind::General, Duration::MAX);
370
371 assert_eq!(
372 limits.try_acquire(RateLimitKind::General),
373 Err(Duration::from_hours(24)),
374 "an unrepresentable provider cooldown must still block admission"
375 );
376 }
377
378 #[test]
379 fn an_unrepresentable_deadline_search_terminates_and_still_blocks() {
380 let now = Instant::now();
381 let deadline = saturating_deadline(now, Duration::MAX);
382 let offset = deadline.saturating_duration_since(now);
386 assert!(
387 offset > Duration::from_hours(24) && offset <= Duration::from_nanos(u64::MAX),
388 "the saturated deadline must still block admission within the \
389 searched nanosecond domain, found {offset:?}"
390 );
391 }
392
393 #[tokio::test(start_paused = true)]
394 async fn rolling_window_expires_staggered_attempts_individually() {
395 let window = Duration::from_secs(10);
396 let limits = limits(2, window);
397 assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
398 advance(Duration::from_secs(5)).await;
399 assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
400
401 advance(Duration::from_secs(5)).await;
402 assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
403 assert_eq!(
404 limits.try_acquire(RateLimitKind::General),
405 Err(Duration::from_secs(5))
406 );
407 }
408
409 #[tokio::test(start_paused = true)]
410 async fn cancelled_waiter_leaves_no_ghost_reservation() {
411 let window = Duration::from_secs(10);
412 let limits = limits(1, window);
413 assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
414 let waiting_limits = Arc::clone(&limits);
415 let waiter = tokio::spawn(async move {
416 waiting_limits.wait(RateLimitKind::General).await;
417 });
418 tokio::task::yield_now().await;
419 waiter.abort();
420
421 advance(window).await;
422 assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
423 }
424
425 #[tokio::test(start_paused = true)]
426 async fn default_history_budget_reaches_its_limit_before_throttling() {
427 let limits = Arc::new(RateLimits::new(Some(RateLimitConfig::default())));
428 for request_number in 1..=50 {
429 assert_eq!(
430 limits.try_acquire(RateLimitKind::History),
431 Ok(()),
432 "history request {request_number} must be admitted"
433 );
434 }
435 assert_eq!(
436 limits.try_acquire(RateLimitKind::History),
437 Err(Duration::from_secs(30))
438 );
439 }
440
441 #[tokio::test(start_paused = true)]
442 async fn default_general_budget_reaches_its_limit_before_throttling() {
443 let limits = Arc::new(RateLimits::new(Some(RateLimitConfig::default())));
444 for request_number in 1..=200 {
445 assert_eq!(
446 limits.try_acquire(RateLimitKind::General),
447 Ok(()),
448 "general request {request_number} must be admitted"
449 );
450 }
451 assert_eq!(
452 limits.try_acquire(RateLimitKind::General),
453 Err(Duration::from_mins(1))
454 );
455 }
456
457 #[tokio::test(start_paused = true)]
458 async fn later_provider_cooldown_cannot_be_shortened() {
459 let limits = limits(200, Duration::from_mins(1));
460 limits.cool_down(RateLimitKind::General, Duration::from_secs(30));
461 limits.cool_down(RateLimitKind::General, Duration::from_secs(5));
462 assert_eq!(
463 limits.try_acquire(RateLimitKind::General),
464 Err(Duration::from_secs(30))
465 );
466 }
467
468 #[tokio::test(start_paused = true)]
469 async fn admission_uses_the_later_of_window_and_provider_cooldown() {
470 let limits = limits(1, Duration::from_secs(20));
471 assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
472 limits.cool_down(RateLimitKind::General, Duration::from_secs(5));
473 assert_eq!(
474 limits.try_acquire(RateLimitKind::General),
475 Err(Duration::from_secs(20))
476 );
477 }
478
479 #[test]
480 fn history_and_general_budgets_are_independent() {
481 let limits = limits(1, Duration::from_secs(30));
482 assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
483 assert_eq!(limits.try_acquire(RateLimitKind::History), Ok(()));
484 assert!(limits.try_acquire(RateLimitKind::General).is_err());
485 assert!(limits.try_acquire(RateLimitKind::History).is_err());
486 }
487
488 #[test]
489 fn public_defaults_match_the_provider_contract() {
490 let config = RateLimitConfig::default();
491 assert_eq!(config.history().max_requests(), 50);
492 assert_eq!(config.history().window(), Duration::from_secs(30));
493 assert_eq!(config.general().max_requests(), 200);
494 assert_eq!(config.general().window(), Duration::from_mins(1));
495 }
496
497 #[test]
498 fn public_rate_limit_rejects_zero_values() {
499 assert!(RateLimit::new(0, Duration::from_secs(1)).is_err());
500 assert!(RateLimit::new(1, Duration::ZERO).is_err());
501 }
502}