1use std::collections::HashMap;
4use tokio::sync::Mutex;
5use tokio::time::Instant;
6
7#[derive(Debug, Clone)]
9pub struct RateLimit {
10 pub max_calls: u32,
11 pub interval_secs: f64,
12}
13
14pub struct RateLimiter {
19 limits: Mutex<HashMap<String, RateLimit>>,
20 buckets: Mutex<HashMap<String, TokenBucket>>,
21}
22
23struct TokenBucket {
24 tokens: f64,
25 max_tokens: f64,
26 refill_rate: f64, last_refill: Instant,
28}
29
30impl TokenBucket {
31 fn new(max_tokens: f64, refill_rate: f64) -> Self {
32 Self {
33 tokens: max_tokens,
34 max_tokens,
35 refill_rate,
36 last_refill: Instant::now(),
37 }
38 }
39
40 fn refill(&mut self) {
42 let now = Instant::now();
43 let elapsed = now.duration_since(self.last_refill).as_secs_f64();
44 self.tokens = (self.tokens + elapsed * self.refill_rate).min(self.max_tokens);
45 self.last_refill = now;
46 }
47
48 fn try_consume(&mut self) -> bool {
50 self.refill();
51 if self.tokens >= 1.0 {
52 self.tokens -= 1.0;
53 true
54 } else {
55 false
56 }
57 }
58
59 fn time_until_available(&mut self) -> f64 {
61 self.refill();
62 if self.tokens >= 1.0 {
63 return 0.0;
64 }
65 let deficit = 1.0 - self.tokens;
66 deficit / self.refill_rate
67 }
68}
69
70impl RateLimiter {
71 pub fn new() -> Self {
73 Self {
74 limits: Mutex::new(HashMap::new()),
75 buckets: Mutex::new(HashMap::new()),
76 }
77 }
78
79 pub async fn set_limit(&self, tool: &str, limit: RateLimit) {
84 let max_tokens = limit.max_calls as f64;
85 let refill_rate = max_tokens / limit.interval_secs;
86
87 self.limits.lock().await.insert(tool.to_string(), limit);
88
89 self.buckets
90 .lock()
91 .await
92 .insert(tool.to_string(), TokenBucket::new(max_tokens, refill_rate));
93 }
94
95 pub fn try_set_limit(&self, tool: &str, limit: RateLimit) -> bool {
104 let max_tokens = limit.max_calls as f64;
105 let refill_rate = max_tokens / limit.interval_secs;
106
107 let (Ok(mut limits), Ok(mut buckets)) = (self.limits.try_lock(), self.buckets.try_lock())
108 else {
109 return false;
110 };
111 limits.insert(tool.to_string(), limit);
112 buckets.insert(tool.to_string(), TokenBucket::new(max_tokens, refill_rate));
113 true
114 }
115
116 pub async fn acquire(&self, tool: &str) {
121 loop {
122 let wait_time = {
123 let mut buckets = self.buckets.lock().await;
124 let bucket = match buckets.get_mut(tool) {
125 Some(b) => b,
126 None => return, };
128
129 if bucket.try_consume() {
130 return;
131 }
132
133 bucket.time_until_available()
134 };
135
136 tokio::time::sleep(std::time::Duration::from_secs_f64(wait_time)).await;
138 }
139 }
140
141 pub async fn try_acquire(&self, tool: &str) -> bool {
146 let mut buckets = self.buckets.lock().await;
147 match buckets.get_mut(tool) {
148 Some(bucket) => bucket.try_consume(),
149 None => true, }
151 }
152}
153
154impl Default for RateLimiter {
155 fn default() -> Self {
156 Self::new()
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[tokio::test(start_paused = true)]
165 async fn test_token_bucket_refills_correctly() {
166 let limiter = RateLimiter::new();
167 limiter
168 .set_limit(
169 "tool_a",
170 RateLimit {
171 max_calls: 2,
172 interval_secs: 1.0,
173 },
174 )
175 .await;
176
177 assert!(limiter.try_acquire("tool_a").await);
179 assert!(limiter.try_acquire("tool_a").await);
180 assert!(!limiter.try_acquire("tool_a").await);
182
183 tokio::time::advance(std::time::Duration::from_millis(500)).await;
185 assert!(limiter.try_acquire("tool_a").await);
186 assert!(!limiter.try_acquire("tool_a").await);
187 }
188
189 #[tokio::test(start_paused = true)]
190 async fn test_acquire_blocks_when_empty() {
191 let limiter = Arc::new(RateLimiter::new());
192 limiter
193 .set_limit(
194 "tool_b",
195 RateLimit {
196 max_calls: 1,
197 interval_secs: 0.2,
198 },
199 )
200 .await;
201
202 assert!(limiter.try_acquire("tool_b").await);
204 assert!(!limiter.try_acquire("tool_b").await);
205
206 let waiting = {
208 let limiter = limiter.clone();
209 tokio::spawn(async move { limiter.acquire("tool_b").await })
210 };
211 tokio::task::yield_now().await;
212 assert!(
213 !waiting.is_finished(),
214 "empty bucket must block acquisition"
215 );
216 tokio::time::advance(std::time::Duration::from_millis(200)).await;
217 waiting
218 .await
219 .expect("acquire task must complete after refill");
220 }
221
222 #[tokio::test]
223 async fn test_independent_tool_limits() {
224 let limiter = RateLimiter::new();
225 limiter
226 .set_limit(
227 "fast",
228 RateLimit {
229 max_calls: 10,
230 interval_secs: 1.0,
231 },
232 )
233 .await;
234 limiter
235 .set_limit(
236 "slow",
237 RateLimit {
238 max_calls: 1,
239 interval_secs: 1.0,
240 },
241 )
242 .await;
243
244 assert!(limiter.try_acquire("slow").await);
246 assert!(!limiter.try_acquire("slow").await);
247
248 for _ in 0..10 {
250 assert!(limiter.try_acquire("fast").await);
251 }
252 assert!(!limiter.try_acquire("fast").await);
253 }
254
255 #[tokio::test]
256 async fn test_no_limit_always_passes() {
257 let limiter = RateLimiter::new();
258 assert!(limiter.try_acquire("unconfigured").await);
260 limiter.acquire("unconfigured").await; }
262
263 use std::sync::Arc;
264
265 #[tokio::test(start_paused = true)]
266 async fn test_max_tokens_cap() {
267 let limiter = RateLimiter::new();
268 limiter
269 .set_limit(
270 "capped",
271 RateLimit {
272 max_calls: 2,
273 interval_secs: 1.0,
274 },
275 )
276 .await;
277
278 assert!(limiter.try_acquire("capped").await);
280 assert!(limiter.try_acquire("capped").await);
281 assert!(!limiter.try_acquire("capped").await);
282 tokio::time::advance(std::time::Duration::from_secs(10)).await;
283
284 assert!(limiter.try_acquire("capped").await);
286 assert!(limiter.try_acquire("capped").await);
287 assert!(!limiter.try_acquire("capped").await);
288 }
289}