Skip to main content

projectx_client/
rate_limit.rs

1// SPDX-FileCopyrightText: 2026 Kevin Monaghan
2// SPDX-License-Identifier: MIT-0
3
4//! Provider REST rate-limit configuration and shared admission control.
5
6use std::{collections::VecDeque, fmt, time::Duration};
7
8use parking_lot::Mutex;
9use tokio::{
10    sync::Notify,
11    time::{Instant, sleep},
12};
13
14use crate::Error;
15
16const DEFAULT_HISTORY_REQUESTS: usize = 50;
17const DEFAULT_HISTORY_WINDOW: Duration = Duration::from_secs(30);
18const DEFAULT_GENERAL_REQUESTS: usize = 200;
19const DEFAULT_GENERAL_WINDOW: Duration = Duration::from_mins(1);
20const MAX_INITIAL_CAPACITY: usize = 1_024;
21/// Shared ceiling for one provider cooldown.
22///
23/// Provider `Retry-After` values are unbounded remote input. The limiter
24/// clamps them to this ceiling so no caller-supplied duration can overflow
25/// the clock into a skipped cooldown, and the REST client bounds the
26/// `retry_after` it parses and reports from the same constant, keeping
27/// admission and `Error::ProviderRateLimited` in lockstep. A provider
28/// cooldown longer than the ceiling is deliberately shortened to it rather
29/// than blocking admission for a duration the caller cannot distinguish
30/// from permanent; the ceiling matches the longest parsed `Retry-After`
31/// the REST client will act on.
32pub(crate) const MAX_COOLDOWN: Duration = Duration::from_hours(24);
33
34/// Identifies one provider REST rate-limit budget.
35#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
36#[non_exhaustive]
37pub enum RateLimitKind {
38    /// The dedicated `/api/History/retrieveBars` budget.
39    History,
40    /// The budget shared by all other authenticated REST endpoints.
41    General,
42}
43
44impl fmt::Display for RateLimitKind {
45    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            Self::History => formatter.write_str("history"),
48            Self::General => formatter.write_str("general"),
49        }
50    }
51}
52
53/// One rolling-window request limit.
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55#[non_exhaustive]
56pub struct RateLimit {
57    max_requests: usize,
58    window: Duration,
59}
60
61impl RateLimit {
62    /// Creates a non-zero rolling-window request limit.
63    ///
64    /// # Errors
65    ///
66    /// Returns [`Error::Configuration`] when `max_requests` or `window` is
67    /// zero, or when the duration cannot be represented by a monotonic clock.
68    pub fn new(max_requests: usize, window: Duration) -> Result<Self, Error> {
69        if max_requests == 0
70            || window.is_zero()
71            || std::time::Instant::now().checked_add(window).is_none()
72        {
73            return Err(Error::Configuration(
74                "rate limits require a positive request count and representable window".to_owned(),
75            ));
76        }
77        Ok(Self {
78            max_requests,
79            window,
80        })
81    }
82
83    const fn trusted(max_requests: usize, window: Duration) -> Self {
84        Self {
85            max_requests,
86            window,
87        }
88    }
89
90    /// Returns the maximum admitted requests in the rolling window.
91    #[must_use]
92    pub const fn max_requests(self) -> usize {
93        self.max_requests
94    }
95
96    /// Returns the rolling-window duration.
97    #[must_use]
98    pub const fn window(self) -> Duration {
99        self.window
100    }
101}
102
103/// Rate limits applied to authenticated `ProjectX` REST requests.
104///
105/// [`Default`] uses the provider's documented limits: 50 history requests per
106/// 30 seconds and 200 other authenticated requests per 60 seconds.
107#[derive(Clone, Copy, Debug, Eq, PartialEq)]
108#[non_exhaustive]
109pub struct RateLimitConfig {
110    history: RateLimit,
111    general: RateLimit,
112    exit_reserve: usize,
113}
114
115impl RateLimitConfig {
116    /// Creates configuration from dedicated history and general limits.
117    #[must_use]
118    pub const fn new(history: RateLimit, general: RateLimit) -> Self {
119        Self {
120            history,
121            general,
122            exit_reserve: 0,
123        }
124    }
125
126    /// Reserves general-window capacity for cancellation and position closes.
127    ///
128    /// Ordinary requests are admitted only below `general.max_requests() -
129    /// reserve`; exits may use the full general limit. Both classes count
130    /// against the same rolling window and provider cooldown. The default is zero.
131    ///
132    /// # Errors
133    ///
134    /// Returns [`Error::Configuration`] when `reserve` is not smaller than
135    /// the general request limit, which must retain ordinary capacity.
136    pub fn with_exit_reserve(mut self, reserve: usize) -> Result<Self, Error> {
137        if reserve >= self.general.max_requests {
138            return Err(Error::Configuration(
139                "exit reserve must be smaller than the general request limit".to_owned(),
140            ));
141        }
142        self.exit_reserve = reserve;
143        Ok(self)
144    }
145
146    /// Returns the general-window capacity reserved for cancellation and closes.
147    #[must_use]
148    pub const fn exit_reserve(self) -> usize {
149        self.exit_reserve
150    }
151
152    /// Returns the history endpoint limit.
153    #[must_use]
154    pub const fn history(self) -> RateLimit {
155        self.history
156    }
157
158    /// Returns the limit for all other authenticated endpoints.
159    #[must_use]
160    pub const fn general(self) -> RateLimit {
161        self.general
162    }
163
164    pub(crate) const fn limit(self, kind: RateLimitKind) -> RateLimit {
165        match kind {
166            RateLimitKind::History => self.history,
167            RateLimitKind::General => self.general,
168        }
169    }
170}
171
172impl Default for RateLimitConfig {
173    fn default() -> Self {
174        Self {
175            history: RateLimit::trusted(DEFAULT_HISTORY_REQUESTS, DEFAULT_HISTORY_WINDOW),
176            general: RateLimit::trusted(DEFAULT_GENERAL_REQUESTS, DEFAULT_GENERAL_WINDOW),
177            exit_reserve: 0,
178        }
179    }
180}
181
182pub(crate) struct RateLimits {
183    config: Option<RateLimitConfig>,
184    history: Option<WindowLimiter>,
185    general: Option<WindowLimiter>,
186}
187
188impl RateLimits {
189    pub(crate) fn new(config: Option<RateLimitConfig>) -> Self {
190        let history = config.map(|limits| WindowLimiter::new(limits.history, 0));
191        let general = config.map(|limits| WindowLimiter::new(limits.general, limits.exit_reserve));
192        Self {
193            config,
194            history,
195            general,
196        }
197    }
198
199    pub(crate) fn config(&self) -> Option<RateLimitConfig> {
200        self.config.map(|mut config| {
201            if let Some(limiter) = &self.general {
202                config.exit_reserve = limiter.state.lock().exit_reserve;
203            }
204            config
205        })
206    }
207
208    pub(crate) fn set_exit_reserve(&self, reserve: usize) -> Result<(), Error> {
209        let limiter = self.general.as_ref().ok_or_else(|| {
210            Error::Configuration("exit reserve requires enabled rate limits".to_owned())
211        })?;
212        let mut state = limiter.state.lock();
213        if reserve >= limiter.limit.max_requests {
214            return Err(Error::Configuration(
215                "exit reserve must be smaller than the general request limit".to_owned(),
216            ));
217        }
218        let changed = state.exit_reserve != reserve;
219        state.exit_reserve = reserve;
220        drop(state);
221        if changed {
222            limiter.policy_changed.notify_waiters();
223        }
224        Ok(())
225    }
226
227    pub(crate) fn limit(&self, kind: RateLimitKind) -> RateLimit {
228        self.config.unwrap_or_default().limit(kind)
229    }
230
231    pub(crate) async fn wait(&self, kind: RateLimitKind) {
232        let Some(limiter) = self.limiter(kind) else {
233            return;
234        };
235        loop {
236            let changed = limiter.policy_changed.notified();
237            tokio::pin!(changed);
238            changed.as_mut().enable();
239            match limiter.try_acquire(false) {
240                Ok(()) => return,
241                Err(retry_after) => {
242                    tokio::select! {
243                        () = &mut changed => {},
244                        () = sleep(retry_after) => {},
245                    }
246                }
247            }
248        }
249    }
250
251    pub(crate) fn try_acquire(&self, kind: RateLimitKind) -> Result<(), Duration> {
252        self.limiter(kind)
253            .map_or(Ok(()), |limiter| limiter.try_acquire(false))
254    }
255
256    pub(crate) fn try_acquire_exit(&self) -> Result<(), Duration> {
257        self.general
258            .as_ref()
259            .map_or(Ok(()), |limiter| limiter.try_acquire(true))
260    }
261
262    pub(crate) fn cool_down(&self, kind: RateLimitKind, retry_after: Duration) {
263        if let Some(limiter) = self.limiter(kind) {
264            limiter.cool_down(retry_after);
265        }
266    }
267
268    fn limiter(&self, kind: RateLimitKind) -> Option<&WindowLimiter> {
269        match kind {
270            RateLimitKind::History => self.history.as_ref(),
271            RateLimitKind::General => self.general.as_ref(),
272        }
273    }
274}
275
276struct WindowLimiter {
277    limit: RateLimit,
278    policy_changed: Notify,
279    state: Mutex<WindowState>,
280}
281
282impl WindowLimiter {
283    fn new(limit: RateLimit, exit_reserve: usize) -> Self {
284        Self {
285            limit,
286            policy_changed: Notify::new(),
287            state: Mutex::new(WindowState::with_capacity(
288                limit.max_requests.min(MAX_INITIAL_CAPACITY),
289                exit_reserve,
290            )),
291        }
292    }
293
294    fn try_acquire(&self, exit: bool) -> Result<(), Duration> {
295        let mut state = self.state.lock();
296        let capacity = if exit {
297            self.limit.max_requests
298        } else {
299            self.limit.max_requests - state.exit_reserve
300        };
301        let now = Instant::now();
302        state.expire(now, self.limit.window);
303        let retry_after = state.retry_after(now, self.limit.window, capacity);
304        if retry_after.is_zero() {
305            state.admitted.push_back(now);
306            Ok(())
307        } else {
308            Err(retry_after)
309        }
310    }
311
312    fn cool_down(&self, retry_after: Duration) {
313        let mut state = self.state.lock();
314        let deadline = saturating_deadline(Instant::now(), retry_after.min(MAX_COOLDOWN));
315        if state
316            .cooldown_until
317            .is_none_or(|current| deadline > current)
318        {
319            state.cooldown_until = Some(deadline);
320        }
321    }
322}
323
324struct WindowState {
325    exit_reserve: usize,
326    admitted: VecDeque<Instant>,
327    cooldown_until: Option<Instant>,
328}
329
330impl WindowState {
331    fn with_capacity(capacity: usize, exit_reserve: usize) -> Self {
332        Self {
333            exit_reserve,
334            admitted: VecDeque::with_capacity(capacity),
335            cooldown_until: None,
336        }
337    }
338
339    fn expire(&mut self, now: Instant, window: Duration) {
340        while self
341            .admitted
342            .front()
343            .is_some_and(|admitted| now.saturating_duration_since(*admitted) >= window)
344        {
345            self.admitted.pop_front();
346        }
347        if self.cooldown_until.is_some_and(|deadline| deadline <= now) {
348            self.cooldown_until = None;
349        }
350    }
351
352    fn retry_after(&self, now: Instant, window: Duration, capacity: usize) -> Duration {
353        let cooldown = self.cooldown_until.map_or(Duration::ZERO, |deadline| {
354            deadline.saturating_duration_since(now)
355        });
356        let capacity_delay = if self.admitted.len() < capacity {
357            Duration::ZERO
358        } else {
359            // Exits may fill beyond ordinary capacity; admission requires
360            // enough aggregate entries to expire, not only the oldest one.
361            self.admitted
362                .get(self.admitted.len() - capacity)
363                .map_or(window, |admitted| {
364                    admitted
365                        .checked_add(window)
366                        .map_or(window, |deadline| deadline.saturating_duration_since(now))
367                })
368        };
369        cooldown.max(capacity_delay)
370    }
371}
372
373/// Returns `now + duration`, or the latest instant the monotonic clock can
374/// represent when the addition overflows, so a cooldown the clock cannot
375/// express still blocks admission to the end of the clock instead of being
376/// dropped.
377fn saturating_deadline(now: Instant, duration: Duration) -> Instant {
378    if let Some(deadline) = now.checked_add(duration) {
379        return deadline;
380    }
381    // The full duration overflows, so a representable offset exists below
382    // it. Binary-search nanosecond offsets capped at `u64::MAX`, the largest
383    // `Duration::from_nanos` can express. Every assignment to `low` follows
384    // a successful clock probe, so `deadline` is only ever set from a
385    // representable offset; the initial zero offset is representable by
386    // definition and can only survive the search when not even the next
387    // nanosecond is. That terminal case yields a no-op cooldown —
388    // `expire` clears it on the next admission attempt — because no future
389    // instant exists to block until.
390    let mut low = 0_u64;
391    let mut high = u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX);
392    let mut deadline = now;
393    while high - low > 1 {
394        let mid = u64::midpoint(low, high);
395        if let Some(probe) = now.checked_add(Duration::from_nanos(mid)) {
396            low = mid;
397            deadline = probe;
398        } else {
399            high = mid;
400        }
401    }
402    deadline
403}
404
405#[cfg(test)]
406mod tests {
407    use std::sync::Arc;
408
409    use tokio::time::{Instant, advance};
410
411    use super::*;
412
413    fn limits(requests: usize, window: Duration) -> Arc<RateLimits> {
414        let limit = RateLimit::new(requests, window)
415            .unwrap_or_else(|error| panic!("test rate limit must be valid: {error}"));
416        Arc::new(RateLimits::new(Some(RateLimitConfig::new(limit, limit))))
417    }
418
419    #[tokio::test(start_paused = true)]
420    async fn wait_resumes_when_the_rolling_window_releases_capacity() {
421        let window = Duration::from_secs(10);
422        let limits = limits(1, window);
423        assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
424
425        let started = Instant::now();
426        let waiting_limits = Arc::clone(&limits);
427        let waiter = tokio::spawn(async move {
428            waiting_limits.wait(RateLimitKind::General).await;
429            Instant::now()
430        });
431        tokio::task::yield_now().await;
432        assert!(!waiter.is_finished());
433
434        advance(window).await;
435        let admitted = waiter
436            .await
437            .unwrap_or_else(|error| panic!("waiting task must finish: {error}"));
438        assert_eq!(admitted.saturating_duration_since(started), window);
439    }
440
441    #[tokio::test(start_paused = true)]
442    async fn provider_cooldown_blocks_admission_until_its_deadline() {
443        let cooldown = Duration::from_secs(15);
444        let limits = limits(2, Duration::from_secs(30));
445        limits.cool_down(RateLimitKind::General, cooldown);
446
447        assert_eq!(limits.try_acquire(RateLimitKind::General), Err(cooldown));
448        advance(cooldown).await;
449        assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
450    }
451
452    #[tokio::test(start_paused = true)]
453    async fn an_oversized_cooldown_is_clamped_not_skipped() {
454        let limits = limits(2, Duration::from_secs(30));
455        limits.cool_down(RateLimitKind::General, Duration::MAX);
456
457        assert_eq!(
458            limits.try_acquire(RateLimitKind::General),
459            Err(Duration::from_hours(24)),
460            "an unrepresentable provider cooldown must still block admission"
461        );
462    }
463
464    #[test]
465    fn an_unrepresentable_deadline_search_terminates_and_still_blocks() {
466        let now = Instant::now();
467        let deadline = saturating_deadline(now, Duration::MAX);
468        // The upper bound separates this from the fast path: a successful
469        // `checked_add(Duration::MAX)` would exceed the search's nanosecond
470        // cap, so passing both bounds proves the capped search ran.
471        let offset = deadline.saturating_duration_since(now);
472        assert!(
473            offset > Duration::from_hours(24) && offset <= Duration::from_nanos(u64::MAX),
474            "the saturated deadline must still block admission within the \
475             searched nanosecond domain, found {offset:?}"
476        );
477    }
478
479    #[tokio::test(start_paused = true)]
480    async fn rolling_window_expires_staggered_attempts_individually() {
481        let window = Duration::from_secs(10);
482        let limits = limits(2, window);
483        assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
484        advance(Duration::from_secs(5)).await;
485        assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
486
487        advance(Duration::from_secs(5)).await;
488        assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
489        assert_eq!(
490            limits.try_acquire(RateLimitKind::General),
491            Err(Duration::from_secs(5))
492        );
493    }
494
495    #[tokio::test(start_paused = true)]
496    async fn cancelled_waiter_leaves_no_ghost_reservation() {
497        let window = Duration::from_secs(10);
498        let limits = limits(1, window);
499        assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
500        let waiting_limits = Arc::clone(&limits);
501        let waiter = tokio::spawn(async move {
502            waiting_limits.wait(RateLimitKind::General).await;
503        });
504        tokio::task::yield_now().await;
505        waiter.abort();
506
507        advance(window).await;
508        assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
509    }
510
511    #[tokio::test(start_paused = true)]
512    async fn default_history_budget_reaches_its_limit_before_throttling() {
513        let limits = Arc::new(RateLimits::new(Some(RateLimitConfig::default())));
514        for request_number in 1..=50 {
515            assert_eq!(
516                limits.try_acquire(RateLimitKind::History),
517                Ok(()),
518                "history request {request_number} must be admitted"
519            );
520        }
521        assert_eq!(
522            limits.try_acquire(RateLimitKind::History),
523            Err(Duration::from_secs(30))
524        );
525    }
526
527    #[tokio::test(start_paused = true)]
528    async fn default_general_budget_reaches_its_limit_before_throttling() {
529        let limits = Arc::new(RateLimits::new(Some(RateLimitConfig::default())));
530        for request_number in 1..=200 {
531            assert_eq!(
532                limits.try_acquire(RateLimitKind::General),
533                Ok(()),
534                "general request {request_number} must be admitted"
535            );
536        }
537        assert_eq!(
538            limits.try_acquire(RateLimitKind::General),
539            Err(Duration::from_mins(1))
540        );
541    }
542
543    #[tokio::test(start_paused = true)]
544    async fn later_provider_cooldown_cannot_be_shortened() {
545        let limits = limits(200, Duration::from_mins(1));
546        limits.cool_down(RateLimitKind::General, Duration::from_secs(30));
547        limits.cool_down(RateLimitKind::General, Duration::from_secs(5));
548        assert_eq!(
549            limits.try_acquire(RateLimitKind::General),
550            Err(Duration::from_secs(30))
551        );
552    }
553
554    #[tokio::test(start_paused = true)]
555    async fn admission_uses_the_later_of_window_and_provider_cooldown() {
556        let limits = limits(1, Duration::from_secs(20));
557        assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
558        limits.cool_down(RateLimitKind::General, Duration::from_secs(5));
559        assert_eq!(
560            limits.try_acquire(RateLimitKind::General),
561            Err(Duration::from_secs(20))
562        );
563    }
564
565    #[test]
566    fn history_and_general_budgets_are_independent() {
567        let limits = limits(1, Duration::from_secs(30));
568        assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
569        assert_eq!(limits.try_acquire(RateLimitKind::History), Ok(()));
570        assert!(limits.try_acquire(RateLimitKind::General).is_err());
571        assert!(limits.try_acquire(RateLimitKind::History).is_err());
572    }
573
574    #[test]
575    fn public_defaults_match_the_provider_contract() {
576        let config = RateLimitConfig::default();
577        assert_eq!(config.history().max_requests(), 50);
578        assert_eq!(config.history().window(), Duration::from_secs(30));
579        assert_eq!(config.general().max_requests(), 200);
580        assert_eq!(config.general().window(), Duration::from_mins(1));
581    }
582
583    #[test]
584    fn public_rate_limit_rejects_zero_values() {
585        assert!(RateLimit::new(0, Duration::from_secs(1)).is_err());
586        assert!(RateLimit::new(1, Duration::ZERO).is_err());
587    }
588
589    fn reserved_limits() -> Arc<RateLimits> {
590        let config = RateLimitConfig::new(
591            RateLimit::new(5, Duration::from_secs(10)).unwrap_or_else(|error| panic!("{error}")),
592            RateLimit::new(5, Duration::from_secs(10)).unwrap_or_else(|error| panic!("{error}")),
593        )
594        .with_exit_reserve(2)
595        .unwrap_or_else(|error| panic!("{error}"));
596        Arc::new(RateLimits::new(Some(config)))
597    }
598
599    #[tokio::test(start_paused = true)]
600    async fn exit_reserve_keeps_ordinary_waiters_below_the_shared_aggregate_cap() {
601        let limits = reserved_limits();
602        for _ in 0..3 {
603            assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
604        }
605        assert_eq!(
606            limits.try_acquire(RateLimitKind::General),
607            Err(Duration::from_secs(10))
608        );
609        let waiting_limits = Arc::clone(&limits);
610        let waiter = tokio::spawn(async move {
611            waiting_limits.wait(RateLimitKind::General).await;
612        });
613        tokio::task::yield_now().await;
614        assert!(!waiter.is_finished());
615        assert_eq!(limits.try_acquire_exit(), Ok(()));
616        tokio::task::yield_now().await;
617        assert!(!waiter.is_finished());
618        assert_eq!(limits.try_acquire_exit(), Ok(()));
619        assert_eq!(limits.try_acquire_exit(), Err(Duration::from_secs(10)));
620        waiter.abort();
621        assert!(waiter.await.is_err_and(|error| error.is_cancelled()));
622        advance(Duration::from_secs(10)).await;
623        for _ in 0..3 {
624            assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
625        }
626        assert_eq!(
627            limits.try_acquire(RateLimitKind::General),
628            Err(Duration::from_secs(10))
629        );
630        assert_eq!(limits.try_acquire_exit(), Ok(()));
631    }
632
633    #[tokio::test(start_paused = true)]
634    async fn ordinary_retry_after_waits_for_enough_aggregate_entries_to_expire() {
635        let limits = reserved_limits();
636        assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
637        advance(Duration::from_secs(1)).await;
638        assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
639        advance(Duration::from_secs(1)).await;
640        assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
641        advance(Duration::from_secs(1)).await;
642        assert_eq!(limits.try_acquire_exit(), Ok(()));
643        advance(Duration::from_secs(1)).await;
644        assert_eq!(limits.try_acquire_exit(), Ok(()));
645        assert_eq!(limits.try_acquire_exit(), Err(Duration::from_secs(6)));
646        assert_eq!(
647            limits.try_acquire(RateLimitKind::General),
648            Err(Duration::from_secs(8))
649        );
650        advance(Duration::from_secs(6)).await;
651        assert_eq!(
652            limits.try_acquire(RateLimitKind::General),
653            Err(Duration::from_secs(2))
654        );
655        advance(Duration::from_secs(2)).await;
656        assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
657        assert_eq!(
658            limits.try_acquire(RateLimitKind::General),
659            Err(Duration::from_secs(1))
660        );
661    }
662
663    #[tokio::test(start_paused = true)]
664    async fn concurrent_exit_admission_never_exceeds_shared_capacity() {
665        let limits = reserved_limits();
666        for _ in 0..3 {
667            assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
668        }
669        let mut tasks = tokio::task::JoinSet::new();
670        for _ in 0..20 {
671            let clone = Arc::clone(&limits);
672            tasks.spawn(async move { clone.try_acquire_exit() });
673        }
674        let mut accepted = 0;
675        let mut refused = 0;
676        while let Some(result) = tasks.join_next().await {
677            match result.unwrap_or_else(|error| panic!("{error}")) {
678                Ok(()) => accepted += 1,
679                Err(delay) => {
680                    assert_eq!(delay, Duration::from_secs(10));
681                    refused += 1;
682                }
683            }
684        }
685        assert_eq!(accepted, 2);
686        assert_eq!(refused, 18);
687    }
688
689    #[tokio::test(start_paused = true)]
690    async fn provider_cooldown_blocks_ordinary_and_reserved_capacity() {
691        let limits = reserved_limits();
692        limits.cool_down(RateLimitKind::General, Duration::from_secs(15));
693        assert_eq!(limits.try_acquire_exit(), Err(Duration::from_secs(15)));
694        assert_eq!(
695            limits.try_acquire(RateLimitKind::General),
696            Err(Duration::from_secs(15))
697        );
698        advance(Duration::from_secs(14)).await;
699        assert_eq!(limits.try_acquire_exit(), Err(Duration::from_secs(1)));
700        assert_eq!(
701            limits.try_acquire(RateLimitKind::General),
702            Err(Duration::from_secs(1))
703        );
704        advance(Duration::from_secs(1)).await;
705        assert_eq!(limits.try_acquire_exit(), Ok(()));
706        assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
707    }
708
709    #[test]
710    fn exit_reserve_retains_ordinary_capacity_and_defaults_to_zero() {
711        let config = RateLimitConfig::default();
712        assert_eq!(config.exit_reserve(), 0);
713        assert_eq!(
714            RateLimitConfig::new(config.history(), config.general()).exit_reserve(),
715            0
716        );
717        assert_eq!(
718            config
719                .with_exit_reserve(199)
720                .unwrap_or_else(|error| panic!("{error}"))
721                .exit_reserve(),
722            199
723        );
724        assert!(matches!(
725            config.with_exit_reserve(200),
726            Err(Error::Configuration(_))
727        ));
728        assert!(matches!(
729            config.with_exit_reserve(usize::MAX),
730            Err(Error::Configuration(_))
731        ));
732    }
733
734    #[tokio::test(start_paused = true)]
735    async fn runtime_release_wakes_all_registered_waiters_without_advancing_time() {
736        let limits = reserved_limits();
737        for _ in 0..3 {
738            assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
739        }
740        let first = limits.wait(RateLimitKind::General);
741        let second = limits.wait(RateLimitKind::General);
742        tokio::pin!(first, second);
743        assert!(futures_util::poll!(first.as_mut()).is_pending());
744        assert!(futures_util::poll!(second.as_mut()).is_pending());
745        let started = Instant::now();
746        let clone = Arc::clone(&limits);
747        clone
748            .set_exit_reserve(0)
749            .unwrap_or_else(|error| panic!("{error}"));
750        assert!(futures_util::poll!(first.as_mut()).is_ready());
751        assert!(futures_util::poll!(second.as_mut()).is_ready());
752        assert_eq!(Instant::now(), started);
753        assert_eq!(limits.config().map(RateLimitConfig::exit_reserve), Some(0));
754        assert_eq!(limits.try_acquire_exit(), Err(Duration::from_secs(10)));
755    }
756
757    #[tokio::test(start_paused = true)]
758    async fn runtime_growth_rechecks_waiters_and_preserves_spent_capacity() {
759        let limits = reserved_limits();
760        for _ in 0..3 {
761            assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
762        }
763        let read = limits.wait(RateLimitKind::General);
764        tokio::pin!(read);
765        assert!(futures_util::poll!(read.as_mut()).is_pending());
766        limits
767            .set_exit_reserve(0)
768            .unwrap_or_else(|error| panic!("{error}"));
769        limits
770            .set_exit_reserve(4)
771            .unwrap_or_else(|error| panic!("{error}"));
772        assert!(futures_util::poll!(read.as_mut()).is_pending());
773        assert_eq!(limits.config().map(RateLimitConfig::exit_reserve), Some(4));
774        assert_eq!(
775            limits.try_acquire(RateLimitKind::General),
776            Err(Duration::from_secs(10))
777        );
778        assert_eq!(limits.try_acquire_exit(), Ok(()));
779        assert_eq!(limits.try_acquire_exit(), Ok(()));
780        assert_eq!(limits.try_acquire_exit(), Err(Duration::from_secs(10)));
781        limits
782            .set_exit_reserve(0)
783            .unwrap_or_else(|error| panic!("{error}"));
784        limits
785            .set_exit_reserve(4)
786            .unwrap_or_else(|error| panic!("{error}"));
787        assert_eq!(limits.try_acquire_exit(), Err(Duration::from_secs(10)));
788        limits
789            .set_exit_reserve(0)
790            .unwrap_or_else(|error| panic!("{error}"));
791        assert!(futures_util::poll!(read.as_mut()).is_pending());
792        advance(Duration::from_secs(10)).await;
793        assert!(futures_util::poll!(read.as_mut()).is_ready());
794    }
795
796    #[tokio::test(start_paused = true)]
797    async fn runtime_changes_preserve_timestamps_cooldown_and_invalid_policy_state() {
798        let limits = reserved_limits();
799        assert_eq!(limits.try_acquire_exit(), Ok(()));
800        advance(Duration::from_secs(2)).await;
801        assert_eq!(limits.try_acquire_exit(), Ok(()));
802        limits.cool_down(RateLimitKind::General, Duration::from_secs(3));
803        for reserve in [4, 0, 2] {
804            limits
805                .set_exit_reserve(reserve)
806                .unwrap_or_else(|error| panic!("{error}"));
807            assert_eq!(limits.try_acquire_exit(), Err(Duration::from_secs(3)));
808        }
809        for reserve in [5, usize::MAX] {
810            assert!(matches!(
811                limits.set_exit_reserve(reserve),
812                Err(Error::Configuration(_))
813            ));
814            assert_eq!(limits.config().map(RateLimitConfig::exit_reserve), Some(2));
815            assert_eq!(limits.try_acquire_exit(), Err(Duration::from_secs(3)));
816        }
817        advance(Duration::from_secs(3)).await;
818        for _ in 0..3 {
819            assert_eq!(limits.try_acquire_exit(), Ok(()));
820        }
821        assert_eq!(limits.try_acquire_exit(), Err(Duration::from_secs(5)));
822        advance(Duration::from_secs(5)).await;
823        assert_eq!(limits.try_acquire_exit(), Ok(()));
824        assert_eq!(limits.try_acquire_exit(), Err(Duration::from_secs(2)));
825    }
826
827    #[tokio::test(start_paused = true)]
828    async fn runtime_waiter_rechecks_each_replacement_including_release_then_growth() {
829        let limits = reserved_limits();
830        limits
831            .set_exit_reserve(4)
832            .unwrap_or_else(|error| panic!("{error}"));
833        assert_eq!(limits.try_acquire(RateLimitKind::General), Ok(()));
834        let read = limits.wait(RateLimitKind::General);
835        tokio::pin!(read);
836        assert!(futures_util::poll!(read.as_mut()).is_pending());
837        limits
838            .set_exit_reserve(0)
839            .unwrap_or_else(|error| panic!("{error}"));
840        limits
841            .set_exit_reserve(4)
842            .unwrap_or_else(|error| panic!("{error}"));
843        assert!(futures_util::poll!(read.as_mut()).is_pending());
844        limits
845            .set_exit_reserve(3)
846            .unwrap_or_else(|error| panic!("{error}"));
847        assert!(futures_util::poll!(read.as_mut()).is_ready());
848        assert_eq!(
849            limits.try_acquire(RateLimitKind::General),
850            Err(Duration::from_secs(10))
851        );
852    }
853}