pubmed_client/rate_limit.rs
1//! Rate limiting implementation for NCBI API compliance
2//!
3//! This module provides rate limiting functionality that respects NCBI E-utilities guidelines.
4//! Uses a unified implementation that works across both native and WASM targets.
5
6use crate::time::{Duration, Instant, sleep};
7use std::sync::Arc;
8use std::sync::Mutex;
9use tracing::{debug, instrument};
10
11/// NCBI E-utilities rate limits:
12/// - 3 requests per second without API key
13/// - 10 requests per second with API key
14/// - Violations can result in IP blocking
15///
16/// Token bucket rate limiter for NCBI API compliance
17#[derive(Clone)]
18pub struct RateLimiter {
19 bucket: Arc<Mutex<TokenBucket>>,
20}
21
22struct TokenBucket {
23 tokens: f64,
24 capacity: f64,
25 refill_rate: f64, // tokens per second
26 last_refill: Instant,
27}
28
29impl RateLimiter {
30 /// Create a new rate limiter with the specified rate
31 ///
32 /// # Arguments
33 ///
34 /// * `rate` - Maximum requests per second (e.g., 3.0 for NCBI default)
35 ///
36 /// # Examples
37 ///
38 /// ```
39 /// use pubmed_client::RateLimiter;
40 ///
41 /// // Create rate limiter for NCBI API without key (3 req/sec)
42 /// let limiter_default = RateLimiter::new(3.0);
43 ///
44 /// // Create rate limiter for NCBI API with key (10 req/sec)
45 /// let limiter_with_key = RateLimiter::new(10.0);
46 /// ```
47 pub fn new(rate: f64) -> Self {
48 let capacity = rate.max(1.0); // Ensure minimum capacity
49 let now = Instant::now();
50 Self {
51 bucket: Arc::new(Mutex::new(TokenBucket {
52 tokens: capacity,
53 capacity,
54 refill_rate: rate,
55 last_refill: now,
56 })),
57 }
58 }
59
60 /// Create rate limiter for NCBI API without API key (3 requests/second)
61 pub fn ncbi_default() -> Self {
62 Self::new(3.0)
63 }
64
65 /// Create rate limiter for NCBI API with API key (10 requests/second)
66 pub fn ncbi_with_key() -> Self {
67 Self::new(10.0)
68 }
69
70 /// Acquire a token, waiting if necessary to respect rate limits
71 ///
72 /// This method implements a token bucket algorithm with the following behavior:
73 /// 1. Check if tokens are available in the bucket
74 /// 2. If available, consume one token and return immediately
75 /// 3. If not available, wait for the appropriate interval
76 /// 4. Refill the bucket and consume one token
77 ///
78 /// # Examples
79 ///
80 /// ```no_run
81 /// use pubmed_client::RateLimiter;
82 ///
83 /// #[tokio::main]
84 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
85 /// let limiter = RateLimiter::ncbi_default();
86 ///
87 /// // This will respect the 3 req/sec limit
88 /// for i in 0..5 {
89 /// limiter.acquire().await?;
90 /// println!("Making API call {}", i + 1);
91 /// // Make your API call here
92 /// }
93 ///
94 /// Ok(())
95 /// }
96 /// ```
97 #[instrument(skip(self))]
98 pub async fn acquire(&self) -> crate::Result<()> {
99 let should_wait = {
100 let mut bucket = self
101 .bucket
102 .lock()
103 .unwrap_or_else(|poisoned| poisoned.into_inner());
104 self.refill_bucket(&mut bucket);
105
106 if bucket.tokens >= 1.0 {
107 bucket.tokens -= 1.0;
108 debug!(remaining_tokens = %bucket.tokens, "Token acquired immediately");
109 false
110 } else {
111 debug!("No tokens available, need to wait");
112 true
113 }
114 };
115
116 if should_wait {
117 // Calculate wait time based on rate
118 let wait_duration = Duration::from_secs(1).as_secs_f64() / self.rate();
119 let wait_duration = Duration::from_millis((wait_duration * 1000.0) as u64);
120
121 debug!(
122 wait_duration_ms = wait_duration.as_millis(),
123 "Waiting for rate limit"
124 );
125 sleep(wait_duration).await;
126
127 // After waiting, refill bucket and consume token
128 let mut bucket = self
129 .bucket
130 .lock()
131 .unwrap_or_else(|poisoned| poisoned.into_inner());
132 self.refill_bucket(&mut bucket);
133 bucket.tokens = bucket.tokens.min(bucket.capacity);
134 if bucket.tokens >= 1.0 {
135 bucket.tokens -= 1.0;
136 debug!(remaining_tokens = %bucket.tokens, "Token acquired after waiting");
137 }
138 }
139
140 Ok(())
141 }
142
143 /// Check if a token is available without blocking
144 ///
145 /// Returns `true` if a token is available and can be acquired immediately.
146 /// This method does not consume a token.
147 pub fn check_available(&self) -> bool {
148 let mut bucket = self
149 .bucket
150 .lock()
151 .unwrap_or_else(|poisoned| poisoned.into_inner());
152 self.refill_bucket(&mut bucket);
153 bucket.tokens >= 1.0
154 }
155
156 /// Get current token count (for testing and monitoring)
157 pub fn token_count(&self) -> f64 {
158 let mut bucket = self
159 .bucket
160 .lock()
161 .unwrap_or_else(|poisoned| poisoned.into_inner());
162 self.refill_bucket(&mut bucket);
163 bucket.tokens
164 }
165
166 /// Get the configured rate limit (requests per second)
167 pub fn rate(&self) -> f64 {
168 let bucket = self
169 .bucket
170 .lock()
171 .unwrap_or_else(|poisoned| poisoned.into_inner());
172 bucket.refill_rate
173 }
174
175 /// Refill the token bucket based on elapsed time
176 fn refill_bucket(&self, bucket: &mut TokenBucket) {
177 let now = Instant::now();
178 let elapsed = now.duration_since(bucket.last_refill);
179
180 // Calculate tokens to add based on elapsed time
181 let tokens_to_add = elapsed.as_secs_f64() * bucket.refill_rate;
182 bucket.tokens = (bucket.tokens + tokens_to_add).min(bucket.capacity);
183
184 bucket.last_refill = now;
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[tokio::test]
193 async fn test_basic_functionality() {
194 let limiter = RateLimiter::new(5.0);
195
196 // Should be able to acquire tokens
197 limiter.acquire().await.unwrap();
198
199 // Check rate
200 let rate = limiter.rate();
201 assert!((rate - 5.0).abs() < 0.1);
202 }
203
204 #[tokio::test]
205 async fn test_check_available() {
206 let limiter = RateLimiter::new(2.0);
207
208 // Should have tokens available initially
209 assert!(limiter.check_available());
210 }
211
212 #[tokio::test]
213 async fn test_ncbi_presets() {
214 let default_limiter = RateLimiter::ncbi_default();
215 let with_key_limiter = RateLimiter::ncbi_with_key();
216
217 assert!((default_limiter.rate() - 3.0).abs() < 0.1);
218 assert!((with_key_limiter.rate() - 10.0).abs() < 0.1);
219 }
220
221 #[tokio::test]
222 async fn test_rate_limiting_basic() {
223 let limiter = RateLimiter::new(1.0); // 1 request per second
224
225 // Should be able to acquire tokens
226 limiter.acquire().await.unwrap();
227 limiter.acquire().await.unwrap(); // This should involve a wait
228
229 // Rate limiter should still work
230 let tokens = limiter.token_count();
231 assert!(tokens >= 0.0);
232 }
233}