1use dashmap::DashMap;
18use parking_lot::RwLock;
19use pingora_limits::rate::Rate;
20use prometheus::{register_int_counter_vec, register_int_gauge_vec, IntCounterVec, IntGaugeVec};
21use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
22use std::sync::{Arc, LazyLock};
23use std::time::{Duration, SystemTime, UNIX_EPOCH};
24use tracing::{debug, trace, warn};
25
26use zentinel_config::{RateLimitAction, RateLimitBackend, RateLimitKey};
27
28#[cfg(feature = "distributed-rate-limit")]
29use crate::distributed_rate_limit::{create_redis_rate_limiter, RedisRateLimiter};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum RateLimitOutcome {
34 Allowed,
36 Limited,
38}
39
40#[derive(Debug, Clone)]
42pub struct RateLimitCheckInfo {
43 pub outcome: RateLimitOutcome,
45 pub current_count: i64,
47 pub limit: u32,
49 pub remaining: u32,
51 pub reset_at: u64,
53}
54
55#[derive(Debug, Clone)]
57pub struct RateLimitConfig {
58 pub max_rps: u32,
60 pub burst: u32,
62 pub key: RateLimitKey,
64 pub action: RateLimitAction,
66 pub status_code: u16,
68 pub message: Option<String>,
70 pub backend: RateLimitBackend,
72 pub max_delay_ms: u64,
74 pub max_keys: usize,
76}
77
78pub const DEFAULT_MAX_RATE_LIMIT_KEYS: usize = 100_000;
80
81const IDLE_KEY_TTL_SECS: u64 = 10;
84
85impl Default for RateLimitConfig {
86 fn default() -> Self {
87 Self {
88 max_rps: 100,
89 burst: 10,
90 key: RateLimitKey::ClientIp,
91 action: RateLimitAction::Reject,
92 status_code: 429,
93 message: None,
94 backend: RateLimitBackend::Local,
95 max_delay_ms: 5000,
96 max_keys: DEFAULT_MAX_RATE_LIMIT_KEYS,
97 }
98 }
99}
100
101struct KeyRateLimiter {
105 rate: Rate,
107 max_requests: isize,
109 last_seen: AtomicU64,
111}
112
113impl KeyRateLimiter {
114 fn new(max_rps: u32) -> Self {
115 Self {
116 rate: Rate::new(Duration::from_secs(1)),
117 max_requests: max_rps as isize,
118 last_seen: AtomicU64::new(current_unix_timestamp()),
119 }
120 }
121
122 fn touch(&self) {
123 self.last_seen
124 .store(current_unix_timestamp(), Ordering::Relaxed);
125 }
126
127 fn idle_secs(&self, now: u64) -> u64 {
128 now.saturating_sub(self.last_seen.load(Ordering::Relaxed))
129 }
130
131 fn check(&self) -> RateLimitOutcome {
133 let curr_count = self.rate.observe(&(), 1);
135
136 if curr_count > self.max_requests {
137 RateLimitOutcome::Limited
138 } else {
139 RateLimitOutcome::Allowed
140 }
141 }
142}
143
144pub enum RateLimitBackendType {
146 Local {
148 limiters: DashMap<String, Arc<KeyRateLimiter>>,
150 },
151 #[cfg(feature = "distributed-rate-limit")]
153 Distributed {
154 redis: Arc<RedisRateLimiter>,
156 local_fallback: DashMap<String, Arc<KeyRateLimiter>>,
158 },
159}
160
161pub struct RateLimiterPool {
163 backend: RateLimitBackendType,
165 config: RwLock<RateLimitConfig>,
167 scope: String,
169 sweeping: AtomicBool,
171}
172
173struct RateLimitPoolMetrics {
175 keys: IntGaugeVec,
177 evictions: IntCounterVec,
179}
180
181static POOL_METRICS: LazyLock<Option<RateLimitPoolMetrics>> = LazyLock::new(|| {
182 let keys = register_int_gauge_vec!(
183 "zentinel_rate_limit_keys",
184 "Distinct rate-limit keys currently tracked in memory",
185 &["scope"]
186 )
187 .ok()?;
188 let evictions = register_int_counter_vec!(
189 "zentinel_rate_limit_key_evictions_total",
190 "Rate-limit keys evicted to enforce the max-keys bound",
191 &["scope"]
192 )
193 .ok()?;
194 Some(RateLimitPoolMetrics { keys, evictions })
195});
196
197fn current_unix_timestamp() -> u64 {
199 SystemTime::now()
200 .duration_since(UNIX_EPOCH)
201 .unwrap_or(Duration::ZERO)
202 .as_secs()
203}
204
205fn calculate_reset_timestamp() -> u64 {
207 current_unix_timestamp() + 1
208}
209
210impl RateLimiterPool {
211 pub fn new(config: RateLimitConfig) -> Self {
213 Self::with_scope(config, "default")
214 }
215
216 pub fn with_scope(config: RateLimitConfig, scope: impl Into<String>) -> Self {
218 Self {
219 backend: RateLimitBackendType::Local {
220 limiters: DashMap::new(),
221 },
222 config: RwLock::new(config),
223 scope: scope.into(),
224 sweeping: AtomicBool::new(false),
225 }
226 }
227
228 #[cfg(feature = "distributed-rate-limit")]
230 pub fn with_redis(config: RateLimitConfig, redis: Arc<RedisRateLimiter>) -> Self {
231 Self {
232 backend: RateLimitBackendType::Distributed {
233 redis,
234 local_fallback: DashMap::new(),
235 },
236 config: RwLock::new(config),
237 scope: "default".to_string(),
238 sweeping: AtomicBool::new(false),
239 }
240 }
241
242 pub fn check(&self, key: &str) -> RateLimitCheckInfo {
247 let config = self.config.read();
248 let max_rps = config.max_rps;
249 let max_keys = config.max_keys;
250 drop(config);
251
252 let limiters = match &self.backend {
253 RateLimitBackendType::Local { limiters } => limiters,
254 #[cfg(feature = "distributed-rate-limit")]
255 RateLimitBackendType::Distributed { local_fallback, .. } => local_fallback,
256 };
257
258 let limiter = self.get_or_create_limiter(limiters, key, max_rps, max_keys);
260
261 limiter.touch();
262 let outcome = limiter.check();
263 let count = limiter.rate.observe(&(), 0); let remaining = if count >= max_rps as isize {
265 0
266 } else {
267 (max_rps as isize - count) as u32
268 };
269
270 RateLimitCheckInfo {
271 outcome,
272 current_count: count as i64,
273 limit: max_rps,
274 remaining,
275 reset_at: calculate_reset_timestamp(),
276 }
277 }
278
279 #[cfg(feature = "distributed-rate-limit")]
283 pub async fn check_async(&self, key: &str) -> RateLimitCheckInfo {
284 let max_rps = self.config.read().max_rps;
285
286 match &self.backend {
287 RateLimitBackendType::Local { .. } => self.check(key),
288 RateLimitBackendType::Distributed {
289 redis,
290 local_fallback,
291 } => {
292 match redis.check(key).await {
294 Ok((outcome, count)) => {
295 let remaining = if count >= max_rps as i64 {
296 0
297 } else {
298 (max_rps as i64 - count) as u32
299 };
300 RateLimitCheckInfo {
301 outcome,
302 current_count: count,
303 limit: max_rps,
304 remaining,
305 reset_at: calculate_reset_timestamp(),
306 }
307 }
308 Err(e) => {
309 warn!(
310 error = %e,
311 key = key,
312 "Redis rate limit check failed, falling back to local"
313 );
314 redis.mark_unhealthy();
315
316 if redis.fallback_enabled() {
318 let max_keys = self.config.read().max_keys;
319 let limiter =
320 self.get_or_create_limiter(local_fallback, key, max_rps, max_keys);
321
322 limiter.touch();
323 let outcome = limiter.check();
324 let count = limiter.rate.observe(&(), 0);
325 let remaining = if count >= max_rps as isize {
326 0
327 } else {
328 (max_rps as isize - count) as u32
329 };
330 RateLimitCheckInfo {
331 outcome,
332 current_count: count as i64,
333 limit: max_rps,
334 remaining,
335 reset_at: calculate_reset_timestamp(),
336 }
337 } else {
338 RateLimitCheckInfo {
340 outcome: RateLimitOutcome::Allowed,
341 current_count: 0,
342 limit: max_rps,
343 remaining: max_rps,
344 reset_at: calculate_reset_timestamp(),
345 }
346 }
347 }
348 }
349 }
350 }
351 }
352
353 pub fn is_distributed(&self) -> bool {
355 match &self.backend {
356 RateLimitBackendType::Local { .. } => false,
357 #[cfg(feature = "distributed-rate-limit")]
358 RateLimitBackendType::Distributed { .. } => true,
359 }
360 }
361
362 pub fn extract_key(
364 &self,
365 client_ip: &str,
366 path: &str,
367 route_id: &str,
368 headers: Option<&impl HeaderAccessor>,
369 ) -> String {
370 let config = self.config.read();
371 match &config.key {
372 RateLimitKey::ClientIp => client_ip.to_string(),
373 RateLimitKey::Path => path.to_string(),
374 RateLimitKey::Route => route_id.to_string(),
375 RateLimitKey::ClientIpAndPath => format!("{}:{}", client_ip, path),
376 RateLimitKey::Header(header_name) => headers
377 .and_then(|h| h.get_header(header_name))
378 .unwrap_or_else(|| "unknown".to_string()),
379 }
380 }
381
382 pub fn action(&self) -> RateLimitAction {
384 self.config.read().action.clone()
385 }
386
387 pub fn status_code(&self) -> u16 {
389 self.config.read().status_code
390 }
391
392 pub fn message(&self) -> Option<String> {
394 self.config.read().message.clone()
395 }
396
397 pub fn max_delay_ms(&self) -> u64 {
399 self.config.read().max_delay_ms
400 }
401
402 pub fn update_config(&self, config: RateLimitConfig) {
404 *self.config.write() = config;
405 self.clear_local_limiters();
407 }
408
409 fn clear_local_limiters(&self) {
411 match &self.backend {
412 RateLimitBackendType::Local { limiters } => limiters.clear(),
413 #[cfg(feature = "distributed-rate-limit")]
414 RateLimitBackendType::Distributed { local_fallback, .. } => local_fallback.clear(),
415 }
416 }
417
418 fn local_limiter_count(&self) -> usize {
420 match &self.backend {
421 RateLimitBackendType::Local { limiters } => limiters.len(),
422 #[cfg(feature = "distributed-rate-limit")]
423 RateLimitBackendType::Distributed { local_fallback, .. } => local_fallback.len(),
424 }
425 }
426
427 fn get_or_create_limiter(
434 &self,
435 limiters: &DashMap<String, Arc<KeyRateLimiter>>,
436 key: &str,
437 max_rps: u32,
438 max_keys: usize,
439 ) -> Arc<KeyRateLimiter> {
440 if let Some(limiter) = limiters.get(key) {
441 return limiter.clone();
442 }
443
444 if limiters.len() >= max_keys {
445 self.evict_keys(limiters, max_keys);
446 }
447
448 let limiter = limiters
449 .entry(key.to_string())
450 .or_insert_with(|| Arc::new(KeyRateLimiter::new(max_rps)))
451 .clone();
452
453 if let Some(metrics) = POOL_METRICS.as_ref() {
454 metrics
455 .keys
456 .with_label_values(&[&self.scope])
457 .set(limiters.len() as i64);
458 }
459
460 limiter
461 }
462
463 fn evict_keys(&self, limiters: &DashMap<String, Arc<KeyRateLimiter>>, max_keys: usize) {
465 if self
468 .sweeping
469 .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
470 .is_err()
471 {
472 return;
473 }
474
475 let now = current_unix_timestamp();
476 let before = limiters.len();
477
478 limiters.retain(|_, limiter| limiter.idle_secs(now) <= IDLE_KEY_TTL_SECS);
480
481 if limiters.len() >= max_keys {
484 let target = ((max_keys * 9) / 10).min(max_keys.saturating_sub(1));
488 let mut entries: Vec<(String, u64)> = limiters
489 .iter()
490 .map(|e| (e.key().clone(), e.value().idle_secs(now)))
491 .collect();
492 entries.sort_by_key(|e| std::cmp::Reverse(e.1));
494 for (key, _) in entries.iter().take(limiters.len().saturating_sub(target)) {
495 limiters.remove(key);
496 }
497 }
498
499 let evicted = before.saturating_sub(limiters.len());
500 if evicted > 0 {
501 warn!(
502 scope = %self.scope,
503 evicted = evicted,
504 remaining = limiters.len(),
505 max_keys = max_keys,
506 "Rate limiter key map at capacity, evicted entries"
507 );
508 if let Some(metrics) = POOL_METRICS.as_ref() {
509 metrics
510 .evictions
511 .with_label_values(&[&self.scope])
512 .inc_by(evicted as u64);
513 metrics
514 .keys
515 .with_label_values(&[&self.scope])
516 .set(limiters.len() as i64);
517 }
518 }
519
520 self.sweeping.store(false, Ordering::Release);
521 }
522
523 pub fn cleanup(&self) {
525 let limiters = match &self.backend {
526 RateLimitBackendType::Local { limiters } => limiters,
527 #[cfg(feature = "distributed-rate-limit")]
528 RateLimitBackendType::Distributed { local_fallback, .. } => local_fallback,
529 };
530
531 let now = current_unix_timestamp();
532 let before = limiters.len();
533 limiters.retain(|_, limiter| limiter.idle_secs(now) <= IDLE_KEY_TTL_SECS);
534
535 if let Some(metrics) = POOL_METRICS.as_ref() {
536 metrics
537 .keys
538 .with_label_values(&[&self.scope])
539 .set(limiters.len() as i64);
540 }
541
542 if before != limiters.len() {
543 debug!(
544 scope = %self.scope,
545 removed = before - limiters.len(),
546 remaining = limiters.len(),
547 "Rate limiter pool cleanup completed"
548 );
549 }
550 }
551}
552
553pub trait HeaderAccessor {
555 fn get_header(&self, name: &str) -> Option<String>;
556}
557
558pub struct RateLimitManager {
560 route_limiters: DashMap<String, Arc<RateLimiterPool>>,
562 global_limiter: Option<Arc<RateLimiterPool>>,
564}
565
566impl RateLimitManager {
567 pub fn new() -> Self {
569 Self {
570 route_limiters: DashMap::new(),
571 global_limiter: None,
572 }
573 }
574
575 pub fn with_global_limit(max_rps: u32, burst: u32) -> Self {
577 let config = RateLimitConfig {
578 max_rps,
579 burst,
580 key: RateLimitKey::ClientIp,
581 action: RateLimitAction::Reject,
582 status_code: 429,
583 message: None,
584 backend: RateLimitBackend::Local,
585 max_delay_ms: 5000,
586 max_keys: DEFAULT_MAX_RATE_LIMIT_KEYS,
587 };
588 Self {
589 route_limiters: DashMap::new(),
590 global_limiter: Some(Arc::new(RateLimiterPool::with_scope(config, "global"))),
591 }
592 }
593
594 pub fn register_route(&self, route_id: &str, config: RateLimitConfig) {
596 trace!(
597 route_id = route_id,
598 max_rps = config.max_rps,
599 burst = config.burst,
600 key = ?config.key,
601 "Registering rate limiter for route"
602 );
603
604 self.route_limiters.insert(
605 route_id.to_string(),
606 Arc::new(RateLimiterPool::with_scope(config, route_id)),
607 );
608 }
609
610 pub fn check(
615 &self,
616 route_id: &str,
617 client_ip: &str,
618 path: &str,
619 headers: Option<&impl HeaderAccessor>,
620 ) -> RateLimitResult {
621 let mut best_limit_info: Option<RateLimitCheckInfo> = None;
623
624 if let Some(ref global) = self.global_limiter {
626 let key = global.extract_key(client_ip, path, route_id, headers);
627 let check_info = global.check(&key);
628
629 if check_info.outcome == RateLimitOutcome::Limited {
630 warn!(
631 route_id = route_id,
632 client_ip = client_ip,
633 key = key,
634 count = check_info.current_count,
635 "Request rate limited by global limiter"
636 );
637 let suggested_delay_ms = if check_info.current_count > check_info.limit as i64 {
639 let excess = check_info.current_count - check_info.limit as i64;
640 Some((excess as u64 * 1000) / check_info.limit as u64)
641 } else {
642 None
643 };
644 return RateLimitResult {
645 allowed: false,
646 action: global.action(),
647 status_code: global.status_code(),
648 message: global.message(),
649 limiter: "global".to_string(),
650 limit: check_info.limit,
651 remaining: check_info.remaining,
652 reset_at: check_info.reset_at,
653 suggested_delay_ms,
654 max_delay_ms: global.max_delay_ms(),
655 };
656 }
657
658 best_limit_info = Some(check_info);
659 }
660
661 if let Some(pool) = self.route_limiters.get(route_id) {
663 let key = pool.extract_key(client_ip, path, route_id, headers);
664 let check_info = pool.check(&key);
665
666 if check_info.outcome == RateLimitOutcome::Limited {
667 warn!(
668 route_id = route_id,
669 client_ip = client_ip,
670 key = key,
671 count = check_info.current_count,
672 "Request rate limited by route limiter"
673 );
674 let suggested_delay_ms = if check_info.current_count > check_info.limit as i64 {
676 let excess = check_info.current_count - check_info.limit as i64;
677 Some((excess as u64 * 1000) / check_info.limit as u64)
678 } else {
679 None
680 };
681 return RateLimitResult {
682 allowed: false,
683 action: pool.action(),
684 status_code: pool.status_code(),
685 message: pool.message(),
686 limiter: route_id.to_string(),
687 limit: check_info.limit,
688 remaining: check_info.remaining,
689 reset_at: check_info.reset_at,
690 suggested_delay_ms,
691 max_delay_ms: pool.max_delay_ms(),
692 };
693 }
694
695 trace!(
696 route_id = route_id,
697 key = key,
698 count = check_info.current_count,
699 remaining = check_info.remaining,
700 "Request allowed by rate limiter"
701 );
702
703 if let Some(ref existing) = best_limit_info {
705 if check_info.remaining < existing.remaining {
706 best_limit_info = Some(check_info);
707 }
708 } else {
709 best_limit_info = Some(check_info);
710 }
711 }
712
713 let (limit, remaining, reset_at) = best_limit_info
715 .map(|info| (info.limit, info.remaining, info.reset_at))
716 .unwrap_or((0, 0, 0));
717
718 RateLimitResult {
719 allowed: true,
720 action: RateLimitAction::Reject,
721 status_code: 429,
722 message: None,
723 limiter: String::new(),
724 limit,
725 remaining,
726 reset_at,
727 suggested_delay_ms: None,
728 max_delay_ms: 5000, }
730 }
731
732 pub fn cleanup(&self) {
734 if let Some(ref global) = self.global_limiter {
735 global.cleanup();
736 }
737 for entry in self.route_limiters.iter() {
738 entry.value().cleanup();
739 }
740 }
741
742 pub fn route_count(&self) -> usize {
744 self.route_limiters.len()
745 }
746
747 #[inline]
752 pub fn is_enabled(&self) -> bool {
753 self.global_limiter.is_some() || !self.route_limiters.is_empty()
754 }
755
756 #[inline]
758 pub fn has_route_limiter(&self, route_id: &str) -> bool {
759 self.global_limiter.is_some() || self.route_limiters.contains_key(route_id)
760 }
761}
762
763impl Default for RateLimitManager {
764 fn default() -> Self {
765 Self::new()
766 }
767}
768
769#[derive(Debug, Clone)]
771pub struct RateLimitResult {
772 pub allowed: bool,
774 pub action: RateLimitAction,
776 pub status_code: u16,
778 pub message: Option<String>,
780 pub limiter: String,
782 pub limit: u32,
784 pub remaining: u32,
786 pub reset_at: u64,
788 pub suggested_delay_ms: Option<u64>,
790 pub max_delay_ms: u64,
792}
793
794#[cfg(test)]
795mod tests {
796 use super::*;
797
798 #[test]
799 fn test_rate_limiter_allows_under_limit() {
800 let config = RateLimitConfig {
801 max_rps: 10,
802 burst: 5,
803 key: RateLimitKey::ClientIp,
804 ..Default::default()
805 };
806 let pool = RateLimiterPool::new(config);
807
808 for i in 0..10 {
810 let info = pool.check("127.0.0.1");
811 assert_eq!(info.outcome, RateLimitOutcome::Allowed);
812 assert_eq!(info.limit, 10);
813 assert_eq!(info.remaining, 10 - i - 1);
814 }
815 }
816
817 #[test]
818 fn test_rate_limiter_blocks_over_limit() {
819 let config = RateLimitConfig {
820 max_rps: 5,
821 burst: 2,
822 key: RateLimitKey::ClientIp,
823 ..Default::default()
824 };
825 let pool = RateLimiterPool::new(config);
826
827 for _ in 0..5 {
829 let info = pool.check("127.0.0.1");
830 assert_eq!(info.outcome, RateLimitOutcome::Allowed);
831 }
832
833 let info = pool.check("127.0.0.1");
835 assert_eq!(info.outcome, RateLimitOutcome::Limited);
836 assert_eq!(info.remaining, 0);
837 }
838
839 #[test]
840 fn test_rate_limiter_separate_keys() {
841 let config = RateLimitConfig {
842 max_rps: 2,
843 burst: 1,
844 key: RateLimitKey::ClientIp,
845 ..Default::default()
846 };
847 let pool = RateLimiterPool::new(config);
848
849 let info1 = pool.check("192.168.1.1");
851 let info2 = pool.check("192.168.1.2");
852 let info3 = pool.check("192.168.1.1");
853 let info4 = pool.check("192.168.1.2");
854
855 assert_eq!(info1.outcome, RateLimitOutcome::Allowed);
856 assert_eq!(info2.outcome, RateLimitOutcome::Allowed);
857 assert_eq!(info3.outcome, RateLimitOutcome::Allowed);
858 assert_eq!(info4.outcome, RateLimitOutcome::Allowed);
859
860 let info5 = pool.check("192.168.1.1");
862 let info6 = pool.check("192.168.1.2");
863
864 assert_eq!(info5.outcome, RateLimitOutcome::Limited);
865 assert_eq!(info6.outcome, RateLimitOutcome::Limited);
866 }
867
868 #[test]
869 fn test_rate_limit_info_fields() {
870 let config = RateLimitConfig {
871 max_rps: 5,
872 burst: 2,
873 key: RateLimitKey::ClientIp,
874 ..Default::default()
875 };
876 let pool = RateLimiterPool::new(config);
877
878 let info = pool.check("10.0.0.1");
879 assert_eq!(info.limit, 5);
880 assert_eq!(info.remaining, 4); assert!(info.reset_at > 0);
882 assert_eq!(info.outcome, RateLimitOutcome::Allowed);
883 }
884
885 #[test]
886 fn test_rate_limit_manager() {
887 let manager = RateLimitManager::new();
888
889 manager.register_route(
890 "api",
891 RateLimitConfig {
892 max_rps: 5,
893 burst: 2,
894 key: RateLimitKey::ClientIp,
895 ..Default::default()
896 },
897 );
898
899 let result = manager.check("web", "127.0.0.1", "/", Option::<&NoHeaders>::None);
901 assert!(result.allowed);
902 assert_eq!(result.limit, 0); for i in 0..5 {
906 let result = manager.check("api", "127.0.0.1", "/api/test", Option::<&NoHeaders>::None);
907 assert!(result.allowed);
908 assert_eq!(result.limit, 5);
909 assert_eq!(result.remaining, 5 - i as u32 - 1);
910 }
911
912 let result = manager.check("api", "127.0.0.1", "/api/test", Option::<&NoHeaders>::None);
913 assert!(!result.allowed);
914 assert_eq!(result.status_code, 429);
915 assert_eq!(result.limit, 5);
916 assert_eq!(result.remaining, 0);
917 assert!(result.reset_at > 0);
918 }
919
920 #[test]
921 fn test_rate_limit_result_with_delay() {
922 let manager = RateLimitManager::new();
923
924 manager.register_route(
925 "api",
926 RateLimitConfig {
927 max_rps: 2,
928 burst: 1,
929 key: RateLimitKey::ClientIp,
930 ..Default::default()
931 },
932 );
933
934 manager.check("api", "127.0.0.1", "/", Option::<&NoHeaders>::None);
936 manager.check("api", "127.0.0.1", "/", Option::<&NoHeaders>::None);
937
938 let result = manager.check("api", "127.0.0.1", "/", Option::<&NoHeaders>::None);
940 assert!(!result.allowed);
941 assert!(result.suggested_delay_ms.is_some());
942 }
943
944 struct NoHeaders;
946 impl HeaderAccessor for NoHeaders {
947 fn get_header(&self, _name: &str) -> Option<String> {
948 None
949 }
950 }
951
952 #[test]
953 fn test_global_rate_limiter() {
954 let manager = RateLimitManager::with_global_limit(3, 1);
955
956 for i in 0..3 {
958 let result = manager.check("any-route", "127.0.0.1", "/", Option::<&NoHeaders>::None);
959 assert!(result.allowed, "Request {} should be allowed", i);
960 assert_eq!(result.limit, 3);
961 assert_eq!(result.remaining, 3 - i as u32 - 1);
962 }
963
964 let result = manager.check(
966 "different-route",
967 "127.0.0.1",
968 "/",
969 Option::<&NoHeaders>::None,
970 );
971 assert!(!result.allowed);
972 assert_eq!(result.limiter, "global");
973 }
974
975 #[test]
976 fn test_global_and_route_limiters() {
977 let manager = RateLimitManager::with_global_limit(10, 5);
978
979 manager.register_route(
981 "strict-api",
982 RateLimitConfig {
983 max_rps: 2,
984 burst: 1,
985 key: RateLimitKey::ClientIp,
986 ..Default::default()
987 },
988 );
989
990 let result1 = manager.check("strict-api", "127.0.0.1", "/", Option::<&NoHeaders>::None);
992 let result2 = manager.check("strict-api", "127.0.0.1", "/", Option::<&NoHeaders>::None);
993 assert!(result1.allowed);
994 assert!(result2.allowed);
995
996 let result3 = manager.check("strict-api", "127.0.0.1", "/", Option::<&NoHeaders>::None);
998 assert!(!result3.allowed);
999 assert_eq!(result3.limiter, "strict-api");
1000
1001 let result4 = manager.check("other-route", "127.0.0.1", "/", Option::<&NoHeaders>::None);
1003 assert!(result4.allowed);
1004 }
1005
1006 #[test]
1007 fn test_suggested_delay_calculation() {
1008 let manager = RateLimitManager::new();
1009
1010 manager.register_route(
1011 "api",
1012 RateLimitConfig {
1013 max_rps: 10,
1014 burst: 5,
1015 key: RateLimitKey::ClientIp,
1016 ..Default::default()
1017 },
1018 );
1019
1020 for _ in 0..10 {
1022 manager.check("api", "127.0.0.1", "/", Option::<&NoHeaders>::None);
1023 }
1024
1025 let result = manager.check("api", "127.0.0.1", "/", Option::<&NoHeaders>::None);
1027 assert!(!result.allowed);
1028 assert!(result.suggested_delay_ms.is_some());
1029
1030 let delay = result.suggested_delay_ms.unwrap();
1034 assert!(delay > 0, "Delay should be positive");
1035 assert!(delay <= 1000, "Delay should be reasonable");
1036 }
1037
1038 #[test]
1039 fn test_reset_timestamp_is_future() {
1040 let config = RateLimitConfig {
1041 max_rps: 5,
1042 burst: 2,
1043 key: RateLimitKey::ClientIp,
1044 ..Default::default()
1045 };
1046 let pool = RateLimiterPool::new(config);
1047
1048 let info = pool.check("10.0.0.1");
1049 let now = std::time::SystemTime::now()
1050 .duration_since(std::time::UNIX_EPOCH)
1051 .unwrap()
1052 .as_secs();
1053
1054 assert!(info.reset_at >= now, "Reset time should be >= now");
1056 assert!(
1057 info.reset_at <= now + 2,
1058 "Reset time should be within 2 seconds"
1059 );
1060 }
1061
1062 #[test]
1063 fn test_rate_limit_check_info_remaining_clamps_to_zero() {
1064 let config = RateLimitConfig {
1065 max_rps: 2,
1066 burst: 1,
1067 key: RateLimitKey::ClientIp,
1068 ..Default::default()
1069 };
1070 let pool = RateLimiterPool::new(config);
1071
1072 pool.check("10.0.0.1");
1074 pool.check("10.0.0.1");
1075
1076 let info = pool.check("10.0.0.1");
1078 assert_eq!(info.remaining, 0);
1079 assert_eq!(info.outcome, RateLimitOutcome::Limited);
1080 }
1081
1082 #[test]
1083 fn test_rate_limit_result_fields() {
1084 let manager = RateLimitManager::new();
1086 manager.register_route(
1087 "test",
1088 RateLimitConfig {
1089 max_rps: 1,
1090 burst: 1,
1091 key: RateLimitKey::ClientIp,
1092 ..Default::default()
1093 },
1094 );
1095
1096 let allowed_result = manager.check("test", "127.0.0.1", "/", Option::<&NoHeaders>::None);
1098 assert!(allowed_result.allowed);
1099 assert_eq!(allowed_result.limit, 1);
1100 assert!(allowed_result.reset_at > 0);
1101
1102 let blocked_result = manager.check("test", "127.0.0.1", "/", Option::<&NoHeaders>::None);
1104 assert!(!blocked_result.allowed);
1105 assert_eq!(blocked_result.status_code, 429);
1106 assert_eq!(blocked_result.remaining, 0);
1107 }
1108
1109 #[test]
1110 fn test_has_route_limiter() {
1111 let manager = RateLimitManager::new();
1112 assert!(!manager.has_route_limiter("test-route"));
1113
1114 manager.register_route(
1115 "test-route",
1116 RateLimitConfig {
1117 max_rps: 10,
1118 burst: 5,
1119 key: RateLimitKey::ClientIp,
1120 ..Default::default()
1121 },
1122 );
1123 assert!(manager.has_route_limiter("test-route"));
1124 assert!(!manager.has_route_limiter("other-route"));
1125 }
1126
1127 #[test]
1128 fn test_global_limiter_is_enabled() {
1129 let manager = RateLimitManager::with_global_limit(100, 50);
1130 assert!(manager.is_enabled());
1132 }
1133
1134 #[test]
1135 fn test_is_enabled() {
1136 let empty_manager = RateLimitManager::new();
1137 assert!(!empty_manager.is_enabled());
1138
1139 let global_manager = RateLimitManager::with_global_limit(100, 50);
1140 assert!(global_manager.is_enabled());
1141
1142 let route_manager = RateLimitManager::new();
1143 route_manager.register_route(
1144 "test",
1145 RateLimitConfig {
1146 max_rps: 10,
1147 burst: 5,
1148 key: RateLimitKey::ClientIp,
1149 ..Default::default()
1150 },
1151 );
1152 assert!(route_manager.is_enabled());
1153 }
1154
1155 fn pool_key_count(pool: &RateLimiterPool) -> usize {
1156 pool.local_limiter_count()
1157 }
1158
1159 #[test]
1160 fn key_map_never_exceeds_max_keys() {
1161 let config = RateLimitConfig {
1162 max_rps: 100,
1163 max_keys: 10,
1164 ..Default::default()
1165 };
1166 let pool = RateLimiterPool::new(config);
1167
1168 for i in 0..100 {
1169 pool.check(&format!("client-{i}"));
1170 }
1171
1172 assert!(
1175 pool_key_count(&pool) <= 10,
1176 "key map grew past max_keys: {}",
1177 pool_key_count(&pool)
1178 );
1179 }
1180
1181 #[test]
1182 fn eviction_keeps_recently_seen_keys_usable() {
1183 let config = RateLimitConfig {
1184 max_rps: 2,
1185 max_keys: 5,
1186 ..Default::default()
1187 };
1188 let pool = RateLimiterPool::new(config);
1189
1190 pool.check("hot");
1192 pool.check("hot");
1193 assert_eq!(pool.check("hot").outcome, RateLimitOutcome::Limited);
1194
1195 for i in 0..50 {
1197 let info = pool.check(&format!("cold-{i}"));
1198 assert_eq!(info.outcome, RateLimitOutcome::Allowed);
1199 }
1200 assert!(pool_key_count(&pool) <= 5);
1201 }
1202
1203 #[test]
1204 fn cleanup_retains_active_keys() {
1205 let config = RateLimitConfig {
1206 max_rps: 100,
1207 max_keys: 100,
1208 ..Default::default()
1209 };
1210 let pool = RateLimiterPool::new(config);
1211
1212 pool.check("active");
1213 pool.cleanup();
1214
1215 assert_eq!(pool_key_count(&pool), 1);
1217 }
1218}