1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3mod backend;
28mod error;
29mod metrics;
30mod quota;
31
32#[cfg(feature = "redis")]
33mod redis;
34
35#[cfg(feature = "sliding-window")]
36mod sliding_window;
37
38#[cfg(feature = "tower")]
39mod tower_layer;
40
41pub use backend::{InMemoryBackend, RateLimitBackend};
42
43#[cfg(feature = "sliding-window")]
44pub use sliding_window::SlidingWindowBackend;
45pub use error::RateLimitError;
46pub use metrics::RateLimitResult;
47pub use quota::Quota;
48
49#[cfg(feature = "redis")]
50pub use redis::RedisBackend;
51
52use std::sync::Arc;
53
54#[derive(Clone)]
56pub struct RateLimiter<B: RateLimitBackend> {
57 quota: Quota,
58 backend: Arc<B>,
59}
60
61impl<B: RateLimitBackend> RateLimiter<B> {
62 pub fn new(quota: Quota, backend: B) -> Self {
64 Self {
65 quota,
66 backend: Arc::new(backend),
67 }
68 }
69
70 pub async fn check(&self, key: &str) -> RateLimitResult {
72 self.backend.check(key, &self.quota).await
73 }
74
75 pub fn check_sync(&self, key: &str) -> RateLimitResult {
80 let rt = tokio::runtime::Builder::new_current_thread()
81 .build()
82 .expect("failed to create tokio runtime for sync check");
83 rt.block_on(self.check(key))
84 }
85}
86
87#[cfg(feature = "in-memory")]
88pub use keyed::KeyedRateLimiter;
89
90#[cfg(feature = "in-memory")]
91mod keyed {
92 use dashmap::DashMap;
93
94 use crate::backend::RateLimitBackend;
95 use crate::metrics::RateLimitResult;
96 use crate::quota::Quota;
97 use crate::RateLimiter;
98
99 pub struct KeyedRateLimiter<B: RateLimitBackend + Clone> {
123 limiters: DashMap<String, RateLimiter<B>>,
124 default_quota: Quota,
125 backend: B,
126 }
127
128 impl<B: RateLimitBackend + Clone> KeyedRateLimiter<B> {
129 pub fn new(default_quota: Quota, backend: B) -> Self {
131 Self {
132 limiters: DashMap::new(),
133 default_quota,
134 backend,
135 }
136 }
137
138 pub fn with_quota_for_key(&self, key: &str, quota: Quota) {
144 self.limiters.insert(
145 key.to_string(),
146 RateLimiter::new(quota, self.backend.clone()),
147 );
148 }
149
150 pub async fn check(&self, key: &str) -> RateLimitResult {
152 let limiter = self
153 .limiters
154 .entry(key.to_string())
155 .or_insert_with(|| {
156 RateLimiter::new(self.default_quota.clone(), self.backend.clone())
157 })
158 .value()
159 .clone();
160 limiter.check(key).await
161 }
162
163 pub fn check_sync(&self, key: &str) -> RateLimitResult {
168 let rt = tokio::runtime::Builder::new_current_thread()
169 .build()
170 .expect("failed to create tokio runtime for sync check");
171 rt.block_on(self.check(key))
172 }
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use std::time::Duration;
180
181 #[test]
182 fn quota_per_second() {
183 let q = Quota::per_second(10);
184 assert_eq!(q.burst, 10);
185 assert_eq!(q.interval(), Duration::from_millis(100));
186 }
187
188 #[test]
189 fn quota_per_minute() {
190 let q = Quota::per_minute(100);
191 assert_eq!(q.burst, 100);
192 assert_eq!(q.interval(), Duration::from_millis(600));
193 }
194
195 #[test]
196 fn quota_per_hour() {
197 let q = Quota::per_hour(3600);
198 assert_eq!(q.burst, 3600);
199 assert_eq!(q.interval(), Duration::from_secs(1));
200 }
201
202 #[test]
203 fn quota_allow_burst() {
204 let q = Quota::per_second(10).allow_burst(20);
205 assert_eq!(q.burst, 20);
206 }
207
208 #[test]
209 fn rate_limit_result_fields() {
210 let r = RateLimitResult {
211 allowed: true,
212 remaining: 5,
213 reset_at: std::time::Instant::now(),
214 limit: 10,
215 retry_after: None,
216 };
217 assert!(r.allowed);
218 assert_eq!(r.remaining, 5);
219 assert_eq!(r.limit, 10);
220 assert!(r.retry_after.is_none());
221 }
222
223 #[test]
224 fn rate_limit_result_headers() {
225 let r = RateLimitResult {
226 allowed: true,
227 remaining: 7,
228 reset_at: std::time::Instant::now() + Duration::from_secs(30),
229 limit: 10,
230 retry_after: None,
231 };
232 let headers = r.headers();
233 assert_eq!(headers.len(), 3);
234 assert_eq!(headers[0].0, "X-RateLimit-Limit");
235 assert_eq!(headers[0].1, "10");
236 assert_eq!(headers[1].0, "X-RateLimit-Remaining");
237 assert_eq!(headers[1].1, "7");
238 assert_eq!(headers[2].0, "X-RateLimit-Reset");
239 }
240
241 #[test]
242 fn rate_limit_result_retry_after() {
243 let r = RateLimitResult {
244 allowed: false,
245 remaining: 0,
246 reset_at: std::time::Instant::now(),
247 limit: 10,
248 retry_after: Some(Duration::from_millis(100)),
249 };
250 assert!(!r.allowed);
251 assert_eq!(r.retry_after, Some(Duration::from_millis(100)));
252 }
253
254 #[test]
255 fn in_memory_backend_new() {
256 let _ = InMemoryBackend::new();
257 }
258
259 #[test]
260 fn in_memory_backend_default() {
261 let _ = InMemoryBackend::default();
262 }
263
264 #[test]
265 fn rate_limit_error_display() {
266 let e = RateLimitError::RateLimited;
267 assert_eq!(e.to_string(), "rate limit exceeded");
268
269 let e = RateLimitError::BackendError("connection refused".into());
270 assert_eq!(
271 e.to_string(),
272 "rate limit backend error: connection refused"
273 );
274 }
275
276 #[tokio::test]
277 async fn first_request_is_allowed() {
278 let backend = InMemoryBackend::new();
279 let limiter = RateLimiter::new(Quota::per_second(10), backend);
280 let result = limiter.check("user-1").await;
281 assert!(result.allowed);
282 assert_eq!(result.remaining, 9);
283 assert_eq!(result.limit, 10);
284 assert!(result.retry_after.is_none());
285 }
286
287 #[tokio::test]
288 async fn rate_limit_exhaustion() {
289 let backend = InMemoryBackend::new();
290 let limiter = RateLimiter::new(Quota::per_second(2), backend);
291 let r1 = limiter.check("user-1").await;
292 assert!(r1.allowed);
293 let r2 = limiter.check("user-1").await;
294 assert!(r2.allowed);
295 let r3 = limiter.check("user-1").await;
296 assert!(!r3.allowed);
297 assert!(r3.retry_after.is_some());
298 }
299
300 #[tokio::test]
301 async fn keyed_limiter_default_quota() {
302 use crate::KeyedRateLimiter;
303 let backend = InMemoryBackend::new();
304 let limiter = KeyedRateLimiter::new(Quota::per_second(5), backend);
305
306 let r = limiter.check("client-1").await;
307 assert!(r.allowed);
308 assert_eq!(r.limit, 5);
309 }
310
311 #[tokio::test]
312 async fn keyed_limiter_custom_quota_per_key() {
313 use crate::KeyedRateLimiter;
314 let backend = InMemoryBackend::new();
315 let limiter = KeyedRateLimiter::new(Quota::per_second(100), backend);
316
317 limiter.with_quota_for_key("login", Quota::per_second(2));
319
320 let r_login = limiter.check("login").await;
321 assert!(r_login.allowed);
322 assert_eq!(r_login.limit, 2);
323
324 let r_api = limiter.check("api").await;
325 assert!(r_api.allowed);
326 assert_eq!(r_api.limit, 100);
327 }
328
329 #[tokio::test]
330 async fn keyed_limiter_isolation() {
331 use crate::KeyedRateLimiter;
332 let backend = InMemoryBackend::new();
333 let limiter = KeyedRateLimiter::new(Quota::per_second(1), backend);
334
335 let r1 = limiter.check("a").await;
336 assert!(r1.allowed);
337
338 let r1_again = limiter.check("a").await;
340 assert!(!r1_again.allowed);
341
342 let r2 = limiter.check("b").await;
343 assert!(r2.allowed);
344 }
345
346 #[test]
347 fn keyed_limiter_sync_check() {
348 use crate::KeyedRateLimiter;
349 let backend = InMemoryBackend::new();
350 let limiter = KeyedRateLimiter::new(Quota::per_second(10), backend);
351
352 let r = limiter.check_sync("client-1");
353 assert!(r.allowed);
354 assert_eq!(r.limit, 10);
355 }
356
357 #[tokio::test]
358 async fn keyed_limiter_headers() {
359 use crate::KeyedRateLimiter;
360 let backend = InMemoryBackend::new();
361 let limiter = KeyedRateLimiter::new(Quota::per_second(10), backend);
362
363 let r = limiter.check("client-1").await;
364 let headers = r.headers();
365 assert!(headers.iter().any(|(k, _)| *k == "X-RateLimit-Limit"));
366 assert!(headers
367 .iter()
368 .any(|(k, v)| *k == "X-RateLimit-Remaining" && v == "9"));
369 }
370
371 #[test]
372 fn sync_check_basic() {
373 let backend = InMemoryBackend::new();
374 let limiter = RateLimiter::new(Quota::per_second(10), backend);
375 let r = limiter.check_sync("user-1");
376 assert!(r.allowed);
377 }
378}