postrust_proxy/ratelimit/
limiter.rs1use crate::config::RateLimitDefaults;
4use crate::ratelimit::TokenBucket;
5use dashmap::DashMap;
6use std::net::IpAddr;
7use std::sync::Arc;
8use std::time::{Duration, Instant};
9use tokio_util::sync::CancellationToken;
10use tracing::{debug, info};
11
12#[derive(Clone, Debug, Hash, PartialEq, Eq)]
14pub enum RateLimitKey {
15 Ip(IpAddr),
17 Header(String),
19 Route(uuid::Uuid),
21 Global,
23}
24
25struct RateLimitEntry {
27 bucket: TokenBucket,
28 last_access: Instant,
29}
30
31pub struct RateLimiter {
33 buckets: DashMap<RateLimitKey, RateLimitEntry>,
35 defaults: RateLimitDefaults,
37 entry_ttl: Duration,
39}
40
41impl RateLimiter {
42 pub fn new(defaults: RateLimitDefaults) -> Self {
44 Self {
45 buckets: DashMap::new(),
46 defaults,
47 entry_ttl: Duration::from_secs(3600), }
49 }
50
51 pub fn check(&self, key: RateLimitKey) -> bool {
55 let rps = self
57 .defaults
58 .requests
59 .checked_div(self.defaults.window_secs)
60 .unwrap_or(self.defaults.requests);
61 self.check_with_config(key, rps, self.defaults.burst)
62 }
63
64 pub fn check_with_config(&self, key: RateLimitKey, rps: u32, burst: u32) -> bool {
66 let mut entry = self.buckets.entry(key).or_insert_with(|| RateLimitEntry {
67 bucket: TokenBucket::new(burst as u64, rps as f64),
68 last_access: Instant::now(),
69 });
70
71 entry.last_access = Instant::now();
72 entry.bucket.try_acquire()
73 }
74
75 pub fn remaining(&self, key: &RateLimitKey) -> Option<u64> {
77 self.buckets.get(key).map(|entry| entry.bucket.available())
78 }
79
80 pub async fn start_cleanup(self: Arc<Self>, cancel_token: CancellationToken) {
82 let cleanup_interval = Duration::from_secs(300); info!("Rate limiter cleanup task started");
85
86 loop {
87 tokio::select! {
88 _ = cancel_token.cancelled() => {
89 info!("Rate limiter cleanup task stopped");
90 break;
91 }
92 _ = tokio::time::sleep(cleanup_interval) => {
93 self.cleanup_expired();
94 }
95 }
96 }
97 }
98
99 fn cleanup_expired(&self) {
101 let now = Instant::now();
102 let before = self.buckets.len();
103
104 self.buckets
105 .retain(|_, entry| now.duration_since(entry.last_access) < self.entry_ttl);
106
107 let removed = before - self.buckets.len();
108 if removed > 0 {
109 debug!("Rate limiter cleanup: removed {} expired entries", removed);
110 }
111 }
112}
113
114impl Default for RateLimiter {
115 fn default() -> Self {
116 Self::new(RateLimitDefaults::default())
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use std::net::Ipv4Addr;
124
125 #[test]
126 fn test_rate_limiter_by_ip() {
127 let limiter = RateLimiter::new(RateLimitDefaults {
128 requests: 600, window_secs: 60,
130 burst: 5,
131 });
132
133 let ip1 = RateLimitKey::Ip(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)));
134 let ip2 = RateLimitKey::Ip(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)));
135
136 for _ in 0..5 {
138 assert!(limiter.check(ip1.clone()));
139 assert!(limiter.check(ip2.clone()));
140 }
141
142 assert!(!limiter.check(ip1.clone()));
144 assert!(!limiter.check(ip2.clone()));
145 }
146
147 #[test]
148 fn test_rate_limiter_remaining() {
149 let limiter = RateLimiter::new(RateLimitDefaults {
150 requests: 600, window_secs: 60,
152 burst: 10,
153 });
154
155 let key = RateLimitKey::Global;
156 assert!(limiter.check(key.clone()));
157
158 let remaining = limiter.remaining(&key).unwrap();
159 assert!(remaining < 10);
160 }
161}