1use std::time::{Duration, Instant};
2
3use crate::metrics::RateLimitResult;
4use crate::quota::Quota;
5
6#[async_trait::async_trait]
8pub trait RateLimitBackend: Send + Sync + 'static {
9 async fn check(&self, key: &str, quota: &Quota) -> RateLimitResult;
11}
12
13#[derive(Clone)]
18pub struct InMemoryBackend {
19 entries: dashmap::DashMap<String, Entry>,
20}
21
22#[derive(Clone)]
23struct Entry {
24 last_check: Instant,
25 remaining: u32,
26}
27
28impl InMemoryBackend {
29 pub fn new() -> Self {
31 Self {
32 entries: dashmap::DashMap::new(),
33 }
34 }
35}
36
37impl Default for InMemoryBackend {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43#[async_trait::async_trait]
44impl RateLimitBackend for InMemoryBackend {
45 async fn check(&self, key: &str, quota: &Quota) -> RateLimitResult {
46 let interval = quota.interval();
47 let burst = quota.burst;
48
49 let mut entry = self
50 .entries
51 .entry(key.to_string())
52 .or_insert_with(|| Entry {
53 last_check: Instant::now(),
54 remaining: burst,
55 });
56
57 let now = Instant::now();
58 let elapsed = now.duration_since(entry.last_check);
59
60 let replenished = (elapsed.as_nanos() as u64 / interval.as_nanos() as u64) as u32;
62 if replenished > 0 {
63 entry.remaining = (entry.remaining + replenished).min(burst);
64 let consumption = Duration::from_nanos(replenished as u64 * interval.as_nanos() as u64);
65 entry.last_check += consumption;
66 }
67
68 let allowed = entry.remaining > 0;
69 if allowed {
70 entry.remaining -= 1;
71 }
72
73 let remaining = entry.remaining;
74 let reset_at = entry.last_check + interval;
75
76 #[cfg(feature = "metrics")]
77 {
78 let utilization = (burst - remaining) as f64 / burst as f64;
79 metrics::histogram!("ratelimit_utilization").record(utilization);
80 if allowed {
81 metrics::counter!("ratelimit_allowed_total", "key" => key.to_string()).increment(1);
82 } else {
83 metrics::counter!("ratelimit_rejected_total", "key" => key.to_string())
84 .increment(1);
85 }
86 }
87
88 RateLimitResult {
89 allowed,
90 remaining: remaining as u64,
91 reset_at,
92 limit: burst as u64,
93 retry_after: if allowed { None } else { Some(interval) },
94 }
95 }
96}