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}