ig_client/application/
rate_limiter.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 19/10/25
5******************************************************************************/
6
7//! Rate limiter module for controlling API request rates
8//!
9//! This module provides rate limiting functionality using the `governor` crate
10//! to ensure compliance with IG Markets API rate limits.
11
12use crate::application::config::RateLimiterConfig;
13use governor::{
14    Quota, RateLimiter as GovernorRateLimiter,
15    clock::QuantaClock,
16    state::{InMemoryState, NotKeyed},
17};
18use std::num::NonZeroU32;
19use std::sync::Arc;
20use std::time::Duration;
21
22/// Rate limiter for controlling API request rates
23///
24/// Uses the `governor` crate to implement a token bucket algorithm
25/// for rate limiting API requests.
26#[derive(Clone)]
27pub struct RateLimiter {
28    limiter: Arc<GovernorRateLimiter<NotKeyed, InMemoryState, QuantaClock>>,
29}
30
31impl RateLimiter {
32    /// Creates a new rate limiter from configuration
33    ///
34    /// # Arguments
35    ///
36    /// * `config` - Rate limiter configuration containing max requests, period, and burst size
37    ///
38    /// # Returns
39    ///
40    /// A new `RateLimiter` instance
41    ///
42    /// # Example
43    ///
44    /// ```ignore
45    /// use ig_client::application::config::RateLimiterConfig;
46    /// use ig_client::application::rate_limiter::RateLimiter;
47    ///
48    /// let config = RateLimiterConfig {
49    ///     max_requests: 60,
50    ///     period_seconds: 60,
51    ///     burst_size: 10,
52    /// };
53    ///
54    /// let limiter = RateLimiter::new(&config);
55    /// ```
56    #[must_use]
57    pub fn new(config: &RateLimiterConfig) -> Self {
58        let period = Duration::from_secs(config.period_seconds);
59
60        let burst_size = NonZeroU32::new(config.burst_size)
61            .unwrap_or_else(|| NonZeroU32::new(10).expect("10 is non-zero"));
62
63        let quota = Quota::with_period(period)
64            .expect("Valid period")
65            .allow_burst(burst_size);
66
67        let limiter = GovernorRateLimiter::direct(quota);
68
69        Self {
70            limiter: Arc::new(limiter),
71        }
72    }
73
74    /// Waits until a request can be made according to the rate limit
75    ///
76    /// This method blocks until the rate limiter allows the request to proceed.
77    /// It uses an async-friendly waiting mechanism.
78    ///
79    /// # Example
80    ///
81    /// ```ignore
82    /// limiter.wait().await;
83    /// // Make API request here
84    /// ```
85    pub async fn wait(&self) {
86        while self.limiter.check().is_err() {
87            tokio::time::sleep(Duration::from_millis(10)).await;
88        }
89    }
90
91    /// Checks if a request can be made immediately without waiting
92    ///
93    /// # Returns
94    ///
95    /// * `true` if a request can be made immediately
96    /// * `false` if the rate limit has been reached
97    ///
98    /// # Example
99    ///
100    /// ```ignore
101    /// if limiter.check() {
102    ///     // Make API request
103    /// } else {
104    ///     // Wait or handle rate limit
105    /// }
106    /// ```
107    #[must_use]
108    pub fn check(&self) -> bool {
109        self.limiter.check().is_ok()
110    }
111}
112
113impl std::fmt::Debug for RateLimiter {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        f.debug_struct("RateLimiter")
116            .field("limiter", &"GovernorRateLimiter")
117            .finish()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[tokio::test]
126    async fn test_rate_limiter_allows_requests() {
127        let config = RateLimiterConfig {
128            max_requests: 10,
129            period_seconds: 1,
130            burst_size: 5,
131        };
132
133        let limiter = RateLimiter::new(&config);
134
135        // Should allow first few requests immediately
136        for _ in 0..5 {
137            assert!(limiter.check());
138        }
139    }
140
141    #[tokio::test]
142    async fn test_rate_limiter_wait() {
143        let config = RateLimiterConfig {
144            max_requests: 2,
145            period_seconds: 1,
146            burst_size: 2,
147        };
148
149        let limiter = RateLimiter::new(&config);
150
151        // First two requests should succeed immediately
152        limiter.wait().await;
153        limiter.wait().await;
154
155        // Third request should wait
156        let start = std::time::Instant::now();
157        limiter.wait().await;
158        let elapsed = start.elapsed();
159
160        // Should have waited some time (but not too long for the test)
161        assert!(elapsed.as_millis() > 0);
162    }
163}