Skip to main content

zentinel_proxy/
rate_limit.rs

1//! Rate limiting using pingora-limits
2//!
3//! This module provides efficient per-route, per-client rate limiting using
4//! Pingora's optimized rate limiting primitives. Supports both local (single-instance)
5//! and distributed (Redis-backed) rate limiting.
6//!
7//! # Local Rate Limiting
8//!
9//! Uses `pingora-limits::Rate` for efficient in-memory rate limiting.
10//! Suitable for single-instance deployments.
11//!
12//! # Distributed Rate Limiting
13//!
14//! Uses Redis sorted sets for sliding window rate limiting across multiple instances.
15//! Requires the `distributed-rate-limit` feature.
16
17use 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/// Rate limiter outcome
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum RateLimitOutcome {
34    /// Request is allowed
35    Allowed,
36    /// Request is rate limited
37    Limited,
38}
39
40/// Detailed rate limit check result from a pool
41#[derive(Debug, Clone)]
42pub struct RateLimitCheckInfo {
43    /// Whether the request is allowed or limited
44    pub outcome: RateLimitOutcome,
45    /// Current request count in the window
46    pub current_count: i64,
47    /// Maximum requests allowed per window
48    pub limit: u32,
49    /// Remaining requests in current window (0 if over limit)
50    pub remaining: u32,
51    /// Unix timestamp (seconds) when the window resets
52    pub reset_at: u64,
53}
54
55/// Rate limiter configuration
56#[derive(Debug, Clone)]
57pub struct RateLimitConfig {
58    /// Maximum requests per second
59    pub max_rps: u32,
60    /// Burst size
61    pub burst: u32,
62    /// Key type for bucketing
63    pub key: RateLimitKey,
64    /// Action when limited
65    pub action: RateLimitAction,
66    /// HTTP status code to return when limited
67    pub status_code: u16,
68    /// Custom message
69    pub message: Option<String>,
70    /// Backend for rate limiting (local or distributed)
71    pub backend: RateLimitBackend,
72    /// Maximum delay in milliseconds for Delay action
73    pub max_delay_ms: u64,
74    /// Maximum number of distinct keys tracked in memory
75    pub max_keys: usize,
76}
77
78/// Default bound on distinct rate-limit keys tracked per pool.
79pub const DEFAULT_MAX_RATE_LIMIT_KEYS: usize = 100_000;
80
81/// Keys not seen within this many seconds are considered idle and evictable.
82/// Rate windows are 1 second, so any state older than this is meaningless.
83const 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
101/// Per-key rate limiter using pingora-limits Rate
102///
103/// Uses a sliding window algorithm with 1-second granularity.
104struct KeyRateLimiter {
105    /// The rate limiter instance (tracks requests in current window)
106    rate: Rate,
107    /// Maximum requests per window
108    max_requests: isize,
109    /// Unix timestamp (seconds) of the last check, for idle eviction
110    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    /// Check if a request should be allowed
132    fn check(&self) -> RateLimitOutcome {
133        // Rate::observe() returns the current count and whether it was a new window
134        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
144/// Backend type for rate limiting
145pub enum RateLimitBackendType {
146    /// Local in-memory backend
147    Local {
148        /// Rate limiters by key (e.g., client IP -> limiter)
149        limiters: DashMap<String, Arc<KeyRateLimiter>>,
150    },
151    /// Distributed Redis backend
152    #[cfg(feature = "distributed-rate-limit")]
153    Distributed {
154        /// Redis rate limiter
155        redis: Arc<RedisRateLimiter>,
156        /// Local fallback
157        local_fallback: DashMap<String, Arc<KeyRateLimiter>>,
158    },
159}
160
161/// Thread-safe rate limiter pool managing multiple rate limiters by key
162pub struct RateLimiterPool {
163    /// Backend for rate limiting
164    backend: RateLimitBackendType,
165    /// Configuration
166    config: RwLock<RateLimitConfig>,
167    /// Scope label for metrics (route ID or "global")
168    scope: String,
169    /// Guard so only one task sweeps the key map at a time
170    sweeping: AtomicBool,
171}
172
173/// Prometheus metrics for rate limiter key maps.
174struct RateLimitPoolMetrics {
175    /// Distinct keys currently tracked, per scope
176    keys: IntGaugeVec,
177    /// Keys evicted to enforce the max-keys bound, per scope
178    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
197/// Get current unix timestamp in seconds
198fn current_unix_timestamp() -> u64 {
199    SystemTime::now()
200        .duration_since(UNIX_EPOCH)
201        .unwrap_or(Duration::ZERO)
202        .as_secs()
203}
204
205/// Calculate window reset timestamp (next second boundary for 1-second windows)
206fn calculate_reset_timestamp() -> u64 {
207    current_unix_timestamp() + 1
208}
209
210impl RateLimiterPool {
211    /// Create a new rate limiter pool with the given configuration (local backend)
212    pub fn new(config: RateLimitConfig) -> Self {
213        Self::with_scope(config, "default")
214    }
215
216    /// Create a new rate limiter pool with a metrics scope (route ID or "global")
217    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    /// Create a new rate limiter pool with a distributed Redis backend
229    #[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    /// Check if a request should be rate limited (synchronous, local only)
243    ///
244    /// Returns detailed rate limit information including remaining quota.
245    /// For distributed backends, this falls back to local limiting.
246    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        // Get or create limiter for this key, enforcing the key-map bound
259        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); // Get current count without incrementing
264        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    /// Check if a request should be rate limited (async, supports distributed backends)
280    ///
281    /// Returns detailed rate limit information including remaining quota.
282    #[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                // Try Redis first
293                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                        // Fallback to local
317                        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                            // Fail open if no fallback
339                            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    /// Check if this pool uses a distributed backend
354    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    /// Get the rate limit key from request context
363    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    /// Get the action to take when rate limited
383    pub fn action(&self) -> RateLimitAction {
384        self.config.read().action.clone()
385    }
386
387    /// Get the HTTP status code for rate limit responses
388    pub fn status_code(&self) -> u16 {
389        self.config.read().status_code
390    }
391
392    /// Get the custom message for rate limit responses
393    pub fn message(&self) -> Option<String> {
394        self.config.read().message.clone()
395    }
396
397    /// Get the maximum delay in milliseconds for Delay action
398    pub fn max_delay_ms(&self) -> u64 {
399        self.config.read().max_delay_ms
400    }
401
402    /// Update the configuration
403    pub fn update_config(&self, config: RateLimitConfig) {
404        *self.config.write() = config;
405        // Clear existing limiters so they get recreated with new config
406        self.clear_local_limiters();
407    }
408
409    /// Clear local limiters (for config updates)
410    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    /// Get the number of local limiter entries
419    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    /// Get or create the limiter for a key, enforcing the `max_keys` bound.
428    ///
429    /// When the map is at capacity and a new key arrives, idle entries (not
430    /// seen within [`IDLE_KEY_TTL_SECS`]) are swept first; if the map is still
431    /// at capacity, the longest-idle entries are evicted down to 90% of the
432    /// bound and an eviction metric is incremented.
433    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    /// Evict entries so a new key can be admitted without exceeding `max_keys`.
464    fn evict_keys(&self, limiters: &DashMap<String, Arc<KeyRateLimiter>>, max_keys: usize) {
465        // Only one task sweeps at a time; concurrent inserts may briefly
466        // overshoot the bound by the number of racing requests.
467        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        // First pass: drop idle entries (their 1s window state is stale anyway)
479        limiters.retain(|_, limiter| limiter.idle_secs(now) <= IDLE_KEY_TTL_SECS);
480
481        // Second pass: still at capacity means all keys are active; evict the
482        // longest-idle entries down to 90% so inserts don't thrash the sweep.
483        if limiters.len() >= max_keys {
484            // Floor and stay below the cap so the pending insert never pushes
485            // the map past max_keys (div_ceil would leave target == max_keys
486            // for small caps and evict nothing).
487            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            // Most idle first
493            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    /// Clean up idle entries (call periodically)
524    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
553/// Trait for accessing headers (allows abstracting over different header types)
554pub trait HeaderAccessor {
555    fn get_header(&self, name: &str) -> Option<String>;
556}
557
558/// Route-level rate limiter manager
559pub struct RateLimitManager {
560    /// Per-route rate limiter pools
561    route_limiters: DashMap<String, Arc<RateLimiterPool>>,
562    /// Global rate limiter (optional)
563    global_limiter: Option<Arc<RateLimiterPool>>,
564}
565
566impl RateLimitManager {
567    /// Create a new rate limit manager
568    pub fn new() -> Self {
569        Self {
570            route_limiters: DashMap::new(),
571            global_limiter: None,
572        }
573    }
574
575    /// Create a new rate limit manager with a global rate limit
576    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    /// Register a rate limiter for a route
595    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    /// Check if a request should be rate limited
611    ///
612    /// Checks both global and route-specific limits.
613    /// Returns detailed rate limit information for response headers.
614    pub fn check(
615        &self,
616        route_id: &str,
617        client_ip: &str,
618        path: &str,
619        headers: Option<&impl HeaderAccessor>,
620    ) -> RateLimitResult {
621        // Track the most restrictive limit info for headers
622        let mut best_limit_info: Option<RateLimitCheckInfo> = None;
623
624        // Check global limit first
625        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                // Calculate suggested delay based on how far over limit
638                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        // Check route-specific limit
662        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                // Calculate suggested delay based on how far over limit
675                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            // Use the more restrictive limit info (lower remaining)
704            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        // Return allowed with rate limit info for headers
714        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, // Default max delay for allowed requests (unused)
729        }
730    }
731
732    /// Perform periodic cleanup
733    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    /// Get the number of registered route limiters
743    pub fn route_count(&self) -> usize {
744        self.route_limiters.len()
745    }
746
747    /// Check if any rate limiting is configured (fast path)
748    ///
749    /// Returns true if there's a global limiter or any route-specific limiters.
750    /// Use this to skip rate limit checks entirely when no limiting is configured.
751    #[inline]
752    pub fn is_enabled(&self) -> bool {
753        self.global_limiter.is_some() || !self.route_limiters.is_empty()
754    }
755
756    /// Check if a specific route has rate limiting configured (fast path)
757    #[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/// Result of a rate limit check
770#[derive(Debug, Clone)]
771pub struct RateLimitResult {
772    /// Whether the request is allowed
773    pub allowed: bool,
774    /// Action to take if limited
775    pub action: RateLimitAction,
776    /// HTTP status code for rejection
777    pub status_code: u16,
778    /// Custom message
779    pub message: Option<String>,
780    /// Which limiter triggered (for logging)
781    pub limiter: String,
782    /// Maximum requests allowed per window
783    pub limit: u32,
784    /// Remaining requests in current window
785    pub remaining: u32,
786    /// Unix timestamp (seconds) when the window resets
787    pub reset_at: u64,
788    /// Suggested delay in milliseconds (for Delay action)
789    pub suggested_delay_ms: Option<u64>,
790    /// Maximum delay in milliseconds (configured cap for Delay action)
791    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        // Should allow first 10 requests
809        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        // Should allow first 5 requests
828        for _ in 0..5 {
829            let info = pool.check("127.0.0.1");
830            assert_eq!(info.outcome, RateLimitOutcome::Allowed);
831        }
832
833        // 6th request should be limited
834        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        // Each IP gets its own bucket
850        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        // Both should hit limit now
861        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); // 5 - 1 = 4
881        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        // Route without limiter should always pass (no rate limit info)
900        let result = manager.check("web", "127.0.0.1", "/", Option::<&NoHeaders>::None);
901        assert!(result.allowed);
902        assert_eq!(result.limit, 0); // No limiter configured
903
904        // Route with limiter should enforce limits and return rate limit info
905        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        // Use up the limit
935        manager.check("api", "127.0.0.1", "/", Option::<&NoHeaders>::None);
936        manager.check("api", "127.0.0.1", "/", Option::<&NoHeaders>::None);
937
938        // Third request should be limited with suggested delay
939        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    // Helper type for tests that don't need header access
945    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        // Global limiter should apply to all routes
957        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        // 4th request should be blocked by global limiter
965        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        // Register a more restrictive route limiter
980        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        // Route limiter should trigger first (more restrictive)
991        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        // 3rd request should be blocked by route limiter
997        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        // Different route should still work (global not exhausted)
1002        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        // Exhaust the limit
1021        for _ in 0..10 {
1022            manager.check("api", "127.0.0.1", "/", Option::<&NoHeaders>::None);
1023        }
1024
1025        // Requests over limit should have suggested delay
1026        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        // Delay should be proportional to how far over limit
1031        // Formula: (excess * 1000) / limit
1032        // With 1 excess request and limit of 10: (1 * 1000) / 10 = 100ms
1033        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        // Reset timestamp should be in the future (within the next second)
1055        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        // Exhaust the limit
1073        pool.check("10.0.0.1");
1074        pool.check("10.0.0.1");
1075
1076        // Over-limit requests should show remaining as 0, not negative
1077        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        // Create a result by checking a rate limited request
1085        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        // First request allowed
1097        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        // Second request should be blocked
1103        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        // Global limiter should be enabled
1131        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        // Active keys force second-pass eviction down to ~90% of the cap, so
1173        // the map may hold at most max_keys entries (cap + the new insert - evictions)
1174        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        // Saturate one key to its limit
1191        pool.check("hot");
1192        pool.check("hot");
1193        assert_eq!(pool.check("hot").outcome, RateLimitOutcome::Limited);
1194
1195        // Flood with new keys to force eviction; limiter must keep functioning
1196        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        // Key was just seen, so periodic cleanup must not remove it
1216        assert_eq!(pool_key_count(&pool), 1);
1217    }
1218}