1use std::collections::HashMap;
27use std::sync::Arc;
28use std::time::{Duration, Instant};
29
30use parking_lot::Mutex;
31
32#[derive(Clone, Copy, Debug)]
34pub struct RateLimitConfig {
35 pub rpm: u64,
36 pub tpm: u64,
37 pub burst_rpm: u64,
38 pub burst_tpm: u64,
39}
40
41impl RateLimitConfig {
42 pub fn from_env() -> Result<Self, String> {
49 Self::from_raw(
50 std::env::var("ATLAS_RATE_LIMIT_RPM").ok().as_deref(),
51 std::env::var("ATLAS_RATE_LIMIT_TPM").ok().as_deref(),
52 std::env::var("ATLAS_RATE_LIMIT_BURST_RPM").ok().as_deref(),
53 std::env::var("ATLAS_RATE_LIMIT_BURST_TPM").ok().as_deref(),
54 )
55 }
56
57 pub fn from_raw(
66 rpm: Option<&str>,
67 tpm: Option<&str>,
68 burst_rpm: Option<&str>,
69 burst_tpm: Option<&str>,
70 ) -> Result<Self, String> {
71 use crate::env_config::parse_min;
72 let rpm = parse_min(
73 "ATLAS_RATE_LIMIT_RPM",
74 rpm,
75 0,
76 "requests per minute per client; 0 disables the request-rate limit",
77 )?
78 .unwrap_or(0);
79 let tpm = parse_min(
80 "ATLAS_RATE_LIMIT_TPM",
81 tpm,
82 0,
83 "tokens per minute per client; 0 disables the token-rate limit",
84 )?
85 .unwrap_or(0);
86 let burst_rpm = parse_min(
89 "ATLAS_RATE_LIMIT_BURST_RPM",
90 burst_rpm,
91 0,
92 "request-bucket depth; defaults to ATLAS_RATE_LIMIT_RPM",
93 )?
94 .unwrap_or(rpm);
95 let burst_tpm = parse_min(
96 "ATLAS_RATE_LIMIT_BURST_TPM",
97 burst_tpm,
98 0,
99 "token-bucket depth; defaults to ATLAS_RATE_LIMIT_TPM",
100 )?
101 .unwrap_or(tpm);
102 Ok(Self {
103 rpm,
104 tpm,
105 burst_rpm: burst_rpm.max(1),
106 burst_tpm: burst_tpm.max(1),
107 })
108 }
109
110 pub fn is_enabled(&self) -> bool {
111 self.rpm > 0 || self.tpm > 0
112 }
113
114 fn advertised_tpm(&self) -> u64 {
122 if self.tpm > 0 {
123 self.burst_tpm
124 } else {
125 1_000_000_000
126 }
127 }
128
129 fn advertised_rpm(&self) -> u64 {
131 if self.rpm > 0 {
132 self.burst_rpm
133 } else {
134 1_000_000
135 }
136 }
137
138 fn advertised_remaining_tpm(&self, avail: u64) -> u64 {
143 if self.tpm > 0 { avail } else { 999_999_999 }
144 }
145
146 fn advertised_remaining_rpm(&self, avail: u64) -> u64 {
147 if self.rpm > 0 { avail } else { 999_999 }
148 }
149}
150
151#[derive(Clone, Copy, Debug)]
154pub struct BucketSnapshot {
155 pub limit: u64,
156 pub remaining: u64,
157 pub reset_secs: u64,
159}
160
161#[derive(Clone, Copy, Debug)]
162pub struct RateDecision {
163 pub allowed: bool,
164 pub requests: BucketSnapshot,
165 pub tokens: BucketSnapshot,
166 pub retry_after_secs: u64,
169 pub denied_by: Option<DenialReason>,
171}
172
173#[derive(Clone, Copy, Debug)]
174pub enum DenialReason {
175 Requests,
176 Tokens,
177}
178
179#[derive(Clone, Debug)]
185pub struct RequestContext {
186 pub identity: String,
187 pub reserved_tokens: u64,
189}
190
191struct Bucket {
192 available: f64,
194 last_refill: Instant,
196}
197
198impl Bucket {
199 fn new(burst: u64) -> Self {
200 Self {
201 available: burst as f64,
202 last_refill: Instant::now(),
203 }
204 }
205
206 fn try_consume(&mut self, cost: f64, rate_per_sec: f64, burst: f64, now: Instant) -> bool {
209 let dt = now
210 .saturating_duration_since(self.last_refill)
211 .as_secs_f64();
212 if dt > 0.0 {
213 self.available = (self.available + dt * rate_per_sec).min(burst);
214 self.last_refill = now;
215 }
216 if self.available >= cost {
217 self.available -= cost;
218 true
219 } else {
220 false
221 }
222 }
223
224 fn snapshot(&self, rate_per_sec: f64, burst: f64, now: Instant) -> (f64, u64) {
225 let dt = now
226 .saturating_duration_since(self.last_refill)
227 .as_secs_f64();
228 let available = (self.available + dt * rate_per_sec).min(burst);
229 let deficit = (burst - available).max(0.0);
231 let reset = if rate_per_sec > 0.0 {
232 (deficit / rate_per_sec).ceil() as u64
233 } else {
234 0
235 };
236 (available, reset)
237 }
238
239 fn refund(&mut self, amount: f64, burst: f64) {
242 self.available = (self.available + amount).min(burst);
243 }
244}
245
246struct KeyState {
247 requests: Bucket,
248 tokens: Bucket,
249}
250
251pub struct RateLimiter {
253 cfg: RateLimitConfig,
254 inner: Mutex<HashMap<String, KeyState>>,
255 last_scrub: Mutex<Instant>,
258}
259
260const SCRUB_INTERVAL: Duration = Duration::from_secs(120);
261const IDLE_EVICT: Duration = Duration::from_secs(600);
263const MAX_KEYS: usize = 100_000;
268
269impl RateLimiter {
270 pub fn from_env() -> Result<Arc<Self>, String> {
273 Ok(Arc::new(Self {
274 cfg: RateLimitConfig::from_env()?,
275 inner: Mutex::new(HashMap::new()),
276 last_scrub: Mutex::new(Instant::now()),
277 }))
278 }
279
280 #[cfg(test)]
281 pub fn with_config(cfg: RateLimitConfig) -> Arc<Self> {
282 Arc::new(Self {
283 cfg,
284 inner: Mutex::new(HashMap::new()),
285 last_scrub: Mutex::new(Instant::now()),
286 })
287 }
288
289 pub fn config(&self) -> RateLimitConfig {
290 self.cfg
291 }
292
293 pub fn admit(&self, key: &str, estimated_tokens: u64) -> RateDecision {
299 let now = Instant::now();
300 self.scrub_if_due(now);
301
302 let rpm = self.cfg.rpm;
303 let tpm = self.cfg.tpm;
304
305 if !self.cfg.is_enabled() {
307 return RateDecision {
308 allowed: true,
309 requests: BucketSnapshot {
310 limit: 1_000_000,
311 remaining: 999_999,
312 reset_secs: 0,
313 },
314 tokens: BucketSnapshot {
315 limit: 1_000_000_000,
316 remaining: 999_999_999,
317 reset_secs: 0,
318 },
319 retry_after_secs: 0,
320 denied_by: None,
321 };
322 }
323
324 let req_rate = rpm as f64 / 60.0;
325 let tok_rate = tpm as f64 / 60.0;
326 let req_burst = self.cfg.burst_rpm as f64;
327 let tok_burst = self.cfg.burst_tpm as f64;
328
329 let mut map = self.inner.lock();
330 if map.len() >= MAX_KEYS && !map.contains_key(key) {
334 map.retain(|_, state| {
335 state.requests.last_refill.elapsed() < IDLE_EVICT
336 || state.tokens.last_refill.elapsed() < IDLE_EVICT
337 });
338 }
343 let state = map.entry(key.to_string()).or_insert_with(|| KeyState {
344 requests: Bucket::new(self.cfg.burst_rpm),
345 tokens: Bucket::new(self.cfg.burst_tpm),
346 });
347
348 let req_allowed = if rpm > 0 {
350 state.requests.try_consume(1.0, req_rate, req_burst, now)
351 } else {
352 true
353 };
354 if !req_allowed {
355 let (req_avail, req_reset) = state.requests.snapshot(req_rate, req_burst, now);
356 let (tok_avail, tok_reset) = state.tokens.snapshot(tok_rate, tok_burst, now);
357 return RateDecision {
358 allowed: false,
359 requests: BucketSnapshot {
360 limit: self.cfg.advertised_rpm(),
361 remaining: self.cfg.advertised_remaining_rpm(req_avail.max(0.0) as u64),
362 reset_secs: req_reset,
363 },
364 tokens: BucketSnapshot {
365 limit: self.cfg.advertised_tpm(),
366 remaining: self.cfg.advertised_remaining_tpm(tok_avail.max(0.0) as u64),
367 reset_secs: tok_reset,
368 },
369 retry_after_secs: req_reset.max(1),
370 denied_by: Some(DenialReason::Requests),
371 };
372 }
373
374 let tok_allowed = if tpm > 0 {
376 state
377 .tokens
378 .try_consume(estimated_tokens as f64, tok_rate, tok_burst, now)
379 } else {
380 true
381 };
382 if !tok_allowed {
383 if rpm > 0 {
385 state.requests.refund(1.0, req_burst);
386 }
387 let (req_avail, req_reset) = state.requests.snapshot(req_rate, req_burst, now);
388 let (tok_avail, tok_reset) = state.tokens.snapshot(tok_rate, tok_burst, now);
389 return RateDecision {
390 allowed: false,
391 requests: BucketSnapshot {
392 limit: self.cfg.advertised_rpm(),
393 remaining: self.cfg.advertised_remaining_rpm(req_avail.max(0.0) as u64),
394 reset_secs: req_reset,
395 },
396 tokens: BucketSnapshot {
397 limit: self.cfg.advertised_tpm(),
398 remaining: self.cfg.advertised_remaining_tpm(tok_avail.max(0.0) as u64),
399 reset_secs: tok_reset,
400 },
401 retry_after_secs: tok_reset.max(1),
402 denied_by: Some(DenialReason::Tokens),
403 };
404 }
405
406 let (req_avail, req_reset) = state.requests.snapshot(req_rate, req_burst, now);
407 let (tok_avail, tok_reset) = state.tokens.snapshot(tok_rate, tok_burst, now);
408 RateDecision {
409 allowed: true,
410 requests: BucketSnapshot {
411 limit: self.cfg.advertised_rpm(),
412 remaining: self.cfg.advertised_remaining_rpm(req_avail.max(0.0) as u64),
413 reset_secs: req_reset,
414 },
415 tokens: BucketSnapshot {
416 limit: self.cfg.advertised_tpm(),
417 remaining: self.cfg.advertised_remaining_tpm(tok_avail.max(0.0) as u64),
418 reset_secs: tok_reset,
419 },
420 retry_after_secs: 0,
421 denied_by: None,
422 }
423 }
424
425 pub fn refund_tokens(&self, key: &str, amount: u64) {
428 if amount == 0 || !self.cfg.is_enabled() || self.cfg.tpm == 0 {
429 return;
430 }
431 let mut map = self.inner.lock();
432 if let Some(state) = map.get_mut(key) {
433 state
434 .tokens
435 .refund(amount as f64, self.cfg.burst_tpm as f64);
436 }
437 }
438
439 fn scrub_if_due(&self, now: Instant) {
440 let mut last = self.last_scrub.lock();
441 if now.saturating_duration_since(*last) < SCRUB_INTERVAL {
442 return;
443 }
444 *last = now;
445 drop(last);
446 let mut map = self.inner.lock();
447 map.retain(|_, state| {
448 state.requests.last_refill.elapsed() < IDLE_EVICT
449 || state.tokens.last_refill.elapsed() < IDLE_EVICT
450 });
451 }
452}
453
454#[path = "rate_limiter/identity.rs"]
455mod identity;
456pub use identity::extract_identity;
457
458#[cfg(test)]
459#[path = "rate_limiter/tests.rs"]
460mod tests;
461
462#[cfg(test)]
463#[path = "rate_limiter/advertised_tests.rs"]
464mod advertised_limit_tests;