1use std::time::Instant;
4
5use dashmap::DashMap;
6
7#[derive(Debug)]
9struct TokenBucket {
10 tokens: f64,
12 capacity: f64,
14 refill_rate: f64,
16 last_refill: Instant,
18}
19
20impl TokenBucket {
21 #[cfg(test)]
23 fn new(rpm: u32) -> Self {
24 Self::new_with_window(rpm, 60)
25 }
26
27 fn new_with_window(rpm: u32, window_secs: u64) -> Self {
29 let capacity = f64::from(rpm);
30 let window = window_secs.max(1) as f64;
31 Self {
32 tokens: capacity,
33 capacity,
34 refill_rate: capacity / window,
35 last_refill: Instant::now(),
36 }
37 }
38
39 fn try_consume(&mut self) -> Result<(), u64> {
43 self.refill();
44 if self.tokens >= 1.0 {
45 self.tokens -= 1.0;
46 Ok(())
47 } else {
48 let deficit = 1.0 - self.tokens;
50 let wait_secs = (deficit / self.refill_rate).ceil() as u64;
51 Err(wait_secs.max(1))
52 }
53 }
54
55 fn refill(&mut self) {
57 let now = Instant::now();
58 let elapsed = now.duration_since(self.last_refill).as_secs_f64();
59 self.tokens = (self.tokens + elapsed * self.refill_rate).min(self.capacity);
60 self.last_refill = now;
61 }
62}
63
64pub struct RateLimiter {
69 buckets: DashMap<String, TokenBucket>,
70 rpm: u32,
71 window_secs: u64,
75}
76
77impl RateLimiter {
78 pub fn new(rpm: u32) -> Self {
83 let window_secs = std::env::var("AA_RATE_LIMIT_WINDOW_SECS")
84 .ok()
85 .and_then(|v| v.parse::<u64>().ok())
86 .unwrap_or(60);
87 Self::new_with_window(rpm, window_secs)
88 }
89
90 pub fn new_with_window(rpm: u32, window_secs: u64) -> Self {
94 Self {
95 buckets: DashMap::new(),
96 rpm,
97 window_secs: window_secs.max(1),
98 }
99 }
100
101 pub fn check(&self, key_id: &str) -> Result<(), u64> {
106 let mut bucket = self
107 .buckets
108 .entry(key_id.to_string())
109 .or_insert_with(|| TokenBucket::new_with_window(self.rpm, self.window_secs));
110 bucket.try_consume()
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn test_token_bucket_allows_within_limit() {
120 let mut bucket = TokenBucket::new(100);
121 for _ in 0..100 {
122 assert!(bucket.try_consume().is_ok());
123 }
124 }
125
126 #[test]
127 fn test_token_bucket_rejects_over_limit() {
128 let mut bucket = TokenBucket::new(10);
129 for _ in 0..10 {
130 bucket.try_consume().unwrap();
131 }
132 let result = bucket.try_consume();
133 assert!(result.is_err(), "should reject when tokens exhausted");
134 let retry_after = result.unwrap_err();
135 assert!(retry_after >= 1, "retry_after should be at least 1 second");
136 }
137
138 #[test]
139 fn test_token_bucket_refills_over_time() {
140 let mut bucket = TokenBucket::new(60); for _ in 0..60 {
143 bucket.try_consume().unwrap();
144 }
145 assert!(bucket.try_consume().is_err());
146
147 bucket.last_refill = Instant::now() - std::time::Duration::from_secs(2);
149 assert!(bucket.try_consume().is_ok(), "should have tokens after refill");
150 }
151
152 #[test]
153 fn test_rate_limiter_per_key_isolation() {
154 let limiter = RateLimiter::new(5);
155
156 for _ in 0..5 {
158 limiter.check("key-a").unwrap();
159 }
160 assert!(limiter.check("key-a").is_err(), "key-a should be exhausted");
161
162 assert!(limiter.check("key-b").is_ok(), "key-b should be independent");
164 }
165}