Skip to main content

oxirs_gql/
rate_limiting.rs

1//! # Advanced Rate Limiting for GraphQL
2//!
3//! Production-ready rate limiting system with multiple algorithms and strategies.
4//! Supports per-user, per-API-key, and per-IP rate limiting with distributed coordination.
5//!
6//! ## Features
7//!
8//! - **Token Bucket Algorithm**: Classic token bucket for burst control
9//! - **Sliding Window Counters**: Smooth rate limiting without boundary issues
10//! - **Fixed Window Counters**: Simple time-based windows
11//! - **Query Complexity-based**: Limit by GraphQL query complexity, not just count
12//! - **Distributed Rate Limiting**: Redis-based coordination across servers
13//! - **Adaptive Rate Limiting**: Automatic adjustment based on server load
14//! - **Per-Client Limits**: Different limits for different users/API keys
15//! - **Configurable Policies**: Flexible policy engine
16//!
17//! ## Algorithms
18//!
19//! ### Token Bucket
20//! - Tokens refill at a constant rate
21//! - Each request consumes tokens
22//! - Allows bursts up to bucket capacity
23//!
24//! ### Sliding Window
25//! - Combines previous and current window
26//! - Smooth transition between windows
27//! - No boundary burst issues
28//!
29//! ### Adaptive
30//! - Monitors server CPU, memory, throughput
31//! - Automatically adjusts limits based on load
32//! - Prevents server overload
33
34use anyhow::Result;
35use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37use std::sync::Arc;
38use std::time::{Duration, SystemTime, UNIX_EPOCH};
39use tokio::sync::RwLock;
40
41/// Rate limiting algorithm
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43pub enum RateLimitAlgorithm {
44    /// Token bucket algorithm (allows bursts)
45    TokenBucket,
46    /// Sliding window counters (smooth limiting)
47    SlidingWindow,
48    /// Fixed window counters (simple)
49    FixedWindow,
50    /// Adaptive based on server load
51    Adaptive,
52}
53
54/// Rate limit configuration
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct RateLimitConfig {
57    /// Algorithm to use
58    pub algorithm: RateLimitAlgorithm,
59    /// Maximum requests per window
60    pub max_requests: u32,
61    /// Window duration in seconds
62    pub window_seconds: u64,
63    /// Enable query complexity-based limiting
64    pub complexity_based: bool,
65    /// Maximum complexity per window (if complexity_based)
66    pub max_complexity: Option<u32>,
67    /// Enable distributed rate limiting (Redis)
68    pub distributed: bool,
69    /// Redis URL for distributed limiting
70    pub redis_url: Option<String>,
71    /// Enable adaptive rate limiting
72    pub adaptive: bool,
73    /// Adaptive target CPU percentage (0-100)
74    pub adaptive_cpu_target: Option<f32>,
75    /// Adaptive target memory percentage (0-100)
76    pub adaptive_memory_target: Option<f32>,
77}
78
79impl Default for RateLimitConfig {
80    fn default() -> Self {
81        Self {
82            algorithm: RateLimitAlgorithm::TokenBucket,
83            max_requests: 100,
84            window_seconds: 60,
85            complexity_based: true,
86            max_complexity: Some(10000),
87            distributed: false,
88            redis_url: None,
89            adaptive: false,
90            adaptive_cpu_target: Some(80.0),
91            adaptive_memory_target: Some(90.0),
92        }
93    }
94}
95
96/// Rate limit policy for specific clients
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct RateLimitPolicy {
99    /// Client identifier (user_id, api_key, or IP)
100    pub client_id: String,
101    /// Custom rate limit config
102    pub config: RateLimitConfig,
103    /// Policy priority (higher = more important)
104    pub priority: u32,
105}
106
107impl RateLimitPolicy {
108    pub fn new(client_id: String, config: RateLimitConfig) -> Self {
109        Self {
110            client_id,
111            config,
112            priority: 0,
113        }
114    }
115
116    pub fn with_priority(mut self, priority: u32) -> Self {
117        self.priority = priority;
118        self
119    }
120}
121
122/// Token bucket state
123#[derive(Debug, Clone, Serialize, Deserialize)]
124struct TokenBucket {
125    /// Current number of tokens
126    tokens: f64,
127    /// Maximum bucket capacity
128    capacity: f64,
129    /// Token refill rate (tokens per second)
130    refill_rate: f64,
131    /// Last refill timestamp
132    last_refill: u64,
133}
134
135impl TokenBucket {
136    fn new(capacity: u32, refill_rate: f64) -> Self {
137        Self {
138            tokens: capacity as f64,
139            capacity: capacity as f64,
140            refill_rate,
141            last_refill: Self::now(),
142        }
143    }
144
145    fn now() -> u64 {
146        SystemTime::now()
147            .duration_since(UNIX_EPOCH)
148            .expect("SystemTime should be after UNIX_EPOCH")
149            .as_secs()
150    }
151
152    fn refill(&mut self) {
153        let now = Self::now();
154        let elapsed = now.saturating_sub(self.last_refill) as f64;
155        let new_tokens = elapsed * self.refill_rate;
156        self.tokens = (self.tokens + new_tokens).min(self.capacity);
157        self.last_refill = now;
158    }
159
160    fn consume(&mut self, tokens: f64) -> bool {
161        self.refill();
162        if self.tokens >= tokens {
163            self.tokens -= tokens;
164            true
165        } else {
166            false
167        }
168    }
169
170    fn available(&mut self) -> f64 {
171        self.refill();
172        self.tokens
173    }
174}
175
176/// Sliding window counter
177#[derive(Debug, Clone, Serialize, Deserialize)]
178struct SlidingWindowCounter {
179    /// Current window start time
180    current_window: u64,
181    /// Current window count
182    current_count: u32,
183    /// Previous window count
184    previous_count: u32,
185    /// Window duration
186    window_duration: u64,
187}
188
189impl SlidingWindowCounter {
190    fn new(window_duration: u64) -> Self {
191        Self {
192            current_window: Self::current_window(window_duration),
193            current_count: 0,
194            previous_count: 0,
195            window_duration,
196        }
197    }
198
199    fn current_window(window_duration: u64) -> u64 {
200        SystemTime::now()
201            .duration_since(UNIX_EPOCH)
202            .expect("SystemTime should be after UNIX_EPOCH")
203            .as_secs()
204            / window_duration
205    }
206
207    fn now_seconds() -> u64 {
208        SystemTime::now()
209            .duration_since(UNIX_EPOCH)
210            .expect("SystemTime should be after UNIX_EPOCH")
211            .as_secs()
212    }
213
214    fn increment(&mut self, amount: u32) {
215        let now_window = Self::current_window(self.window_duration);
216
217        if now_window > self.current_window + 1 {
218            // More than one window has passed, reset both
219            self.previous_count = 0;
220            self.current_count = amount;
221            self.current_window = now_window;
222        } else if now_window > self.current_window {
223            // Move to next window
224            self.previous_count = self.current_count;
225            self.current_count = amount;
226            self.current_window = now_window;
227        } else {
228            // Same window
229            self.current_count += amount;
230        }
231    }
232
233    fn count(&self, max_requests: u32) -> Result<(), String> {
234        let now_window = Self::current_window(self.window_duration);
235
236        if now_window > self.current_window + 1 {
237            // More than one window has passed, no requests in current window
238            return Ok(());
239        }
240
241        if now_window > self.current_window {
242            // In transition between windows, use sliding calculation
243            let now = Self::now_seconds();
244            let window_progress = (now % self.window_duration) as f64 / self.window_duration as f64;
245            let weighted_count =
246                (self.previous_count as f64 * (1.0 - window_progress)) + self.current_count as f64;
247
248            if weighted_count >= max_requests as f64 {
249                return Err(format!(
250                    "Rate limit exceeded: {:.0}/{} requests in sliding window",
251                    weighted_count, max_requests
252                ));
253            }
254        } else {
255            // Same window
256            if self.current_count >= max_requests {
257                return Err(format!(
258                    "Rate limit exceeded: {}/{} requests in window",
259                    self.current_count, max_requests
260                ));
261            }
262        }
263
264        Ok(())
265    }
266}
267
268/// Fixed window counter
269#[derive(Debug, Clone, Serialize, Deserialize)]
270struct FixedWindowCounter {
271    /// Window start time
272    window_start: u64,
273    /// Request count in this window
274    count: u32,
275    /// Window duration
276    window_duration: u64,
277}
278
279impl FixedWindowCounter {
280    fn new(window_duration: u64) -> Self {
281        Self {
282            window_start: Self::now(),
283            count: 0,
284            window_duration,
285        }
286    }
287
288    fn now() -> u64 {
289        SystemTime::now()
290            .duration_since(UNIX_EPOCH)
291            .expect("SystemTime should be after UNIX_EPOCH")
292            .as_secs()
293    }
294
295    fn increment(&mut self, amount: u32) {
296        let now = Self::now();
297
298        if now >= self.window_start + self.window_duration {
299            // New window
300            self.window_start = now;
301            self.count = amount;
302        } else {
303            // Same window
304            self.count += amount;
305        }
306    }
307
308    fn check(&self, max_requests: u32) -> Result<(), String> {
309        let now = Self::now();
310
311        if now >= self.window_start + self.window_duration {
312            // New window, no requests yet
313            Ok(())
314        } else if self.count >= max_requests {
315            Err(format!(
316                "Rate limit exceeded: {}/{} requests in window",
317                self.count, max_requests
318            ))
319        } else {
320            Ok(())
321        }
322    }
323}
324
325/// Rate limit state per client
326#[derive(Debug, Clone, Serialize, Deserialize)]
327enum RateLimitState {
328    TokenBucket(TokenBucket),
329    SlidingWindow(SlidingWindowCounter),
330    FixedWindow(FixedWindowCounter),
331}
332
333/// Rate limiter result
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct RateLimitResult {
336    /// Whether the request is allowed
337    pub allowed: bool,
338    /// Current limit
339    pub limit: u32,
340    /// Remaining requests/tokens
341    pub remaining: u32,
342    /// Time until reset (seconds)
343    pub reset_after: u64,
344    /// Retry after (seconds) if not allowed
345    pub retry_after: Option<u64>,
346}
347
348/// Rate limiter
349pub struct RateLimiter {
350    /// Configuration
351    config: Arc<RateLimitConfig>,
352    /// Per-client state
353    states: Arc<RwLock<HashMap<String, RateLimitState>>>,
354    /// Custom policies
355    policies: Arc<RwLock<HashMap<String, RateLimitPolicy>>>,
356    /// System load monitor for adaptive limiting
357    load_monitor: Arc<RwLock<SystemLoad>>,
358}
359
360#[derive(Debug, Clone)]
361struct SystemLoad {
362    cpu_usage: f32,
363    memory_usage: f32,
364    last_update: std::time::Instant,
365}
366
367impl SystemLoad {
368    fn new() -> Self {
369        Self {
370            cpu_usage: 0.0,
371            memory_usage: 0.0,
372            last_update: std::time::Instant::now(),
373        }
374    }
375
376    fn update(&mut self) {
377        // Update only every 5 seconds to avoid overhead
378        if self.last_update.elapsed() < Duration::from_secs(5) {
379            return;
380        }
381
382        use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System};
383
384        let mut sys = System::new_with_specifics(
385            RefreshKind::nothing()
386                .with_cpu(CpuRefreshKind::everything())
387                .with_memory(MemoryRefreshKind::everything()),
388        );
389        sys.refresh_all();
390
391        // Calculate average CPU usage across all cores
392        let cpus = sys.cpus();
393        if !cpus.is_empty() {
394            self.cpu_usage =
395                cpus.iter().map(|cpu| cpu.cpu_usage()).sum::<f32>() / cpus.len() as f32;
396        }
397
398        self.memory_usage = (sys.used_memory() as f64 / sys.total_memory() as f64 * 100.0) as f32;
399        self.last_update = std::time::Instant::now();
400    }
401
402    fn should_reduce_limits(&self, cpu_target: f32, memory_target: f32) -> bool {
403        self.cpu_usage > cpu_target || self.memory_usage > memory_target
404    }
405}
406
407impl RateLimiter {
408    /// Create a new rate limiter
409    pub fn new(config: RateLimitConfig) -> Self {
410        Self {
411            config: Arc::new(config),
412            states: Arc::new(RwLock::new(HashMap::new())),
413            policies: Arc::new(RwLock::new(HashMap::new())),
414            load_monitor: Arc::new(RwLock::new(SystemLoad::new())),
415        }
416    }
417
418    /// Add a custom policy for a specific client
419    pub async fn add_policy(&self, policy: RateLimitPolicy) -> Result<()> {
420        let mut policies = self.policies.write().await;
421        policies.insert(policy.client_id.clone(), policy);
422        Ok(())
423    }
424
425    /// Remove a custom policy
426    pub async fn remove_policy(&self, client_id: &str) -> Result<()> {
427        let mut policies = self.policies.write().await;
428        policies.remove(client_id);
429        Ok(())
430    }
431
432    /// Check rate limit for a client
433    pub async fn check_rate_limit(
434        &self,
435        client_id: &str,
436        cost: Option<u32>,
437    ) -> Result<RateLimitResult> {
438        // Get effective config (policy or default)
439        let effective_config = {
440            let policies = self.policies.read().await;
441            policies
442                .get(client_id)
443                .map(|p| p.config.clone())
444                .unwrap_or_else(|| (*self.config).clone())
445        };
446
447        // Adaptive limiting
448        if effective_config.adaptive {
449            let mut load_monitor = self.load_monitor.write().await;
450            load_monitor.update();
451
452            if load_monitor.should_reduce_limits(
453                effective_config.adaptive_cpu_target.unwrap_or(80.0),
454                effective_config.adaptive_memory_target.unwrap_or(90.0),
455            ) {
456                // Reduce limits by 50% when under high load
457                return Ok(RateLimitResult {
458                    allowed: false,
459                    limit: effective_config.max_requests,
460                    remaining: 0,
461                    reset_after: effective_config.window_seconds,
462                    retry_after: Some(5),
463                });
464            }
465        }
466
467        let request_cost = if effective_config.complexity_based {
468            cost.unwrap_or(1)
469        } else {
470            1
471        };
472
473        let mut states = self.states.write().await;
474        let state = states
475            .entry(client_id.to_string())
476            .or_insert_with(|| Self::create_state(&effective_config));
477
478        match state {
479            RateLimitState::TokenBucket(bucket) => {
480                let allowed = bucket.consume(request_cost as f64);
481                let remaining = bucket.available() as u32;
482
483                Ok(RateLimitResult {
484                    allowed,
485                    limit: effective_config.max_requests,
486                    remaining,
487                    reset_after: effective_config.window_seconds,
488                    retry_after: if !allowed { Some(1) } else { None },
489                })
490            }
491            RateLimitState::SlidingWindow(counter) => {
492                let check_result = counter.count(effective_config.max_requests);
493
494                if check_result.is_ok() {
495                    counter.increment(request_cost);
496                    Ok(RateLimitResult {
497                        allowed: true,
498                        limit: effective_config.max_requests,
499                        remaining: effective_config
500                            .max_requests
501                            .saturating_sub(counter.current_count),
502                        reset_after: effective_config.window_seconds,
503                        retry_after: None,
504                    })
505                } else {
506                    Ok(RateLimitResult {
507                        allowed: false,
508                        limit: effective_config.max_requests,
509                        remaining: 0,
510                        reset_after: effective_config.window_seconds,
511                        retry_after: Some(effective_config.window_seconds),
512                    })
513                }
514            }
515            RateLimitState::FixedWindow(counter) => {
516                let check_result = counter.check(effective_config.max_requests);
517
518                if check_result.is_ok() {
519                    counter.increment(request_cost);
520                    Ok(RateLimitResult {
521                        allowed: true,
522                        limit: effective_config.max_requests,
523                        remaining: effective_config.max_requests.saturating_sub(counter.count),
524                        reset_after: effective_config.window_seconds,
525                        retry_after: None,
526                    })
527                } else {
528                    Ok(RateLimitResult {
529                        allowed: false,
530                        limit: effective_config.max_requests,
531                        remaining: 0,
532                        reset_after: effective_config.window_seconds,
533                        retry_after: Some(effective_config.window_seconds),
534                    })
535                }
536            }
537        }
538    }
539
540    /// Reset rate limit for a client
541    pub async fn reset_client(&self, client_id: &str) -> Result<()> {
542        let mut states = self.states.write().await;
543        states.remove(client_id);
544        Ok(())
545    }
546
547    /// Get statistics for all clients
548    pub async fn get_statistics(&self) -> HashMap<String, String> {
549        let states = self.states.read().await;
550        states
551            .iter()
552            .map(|(id, state)| {
553                let info = match state {
554                    RateLimitState::TokenBucket(b) => {
555                        format!("TokenBucket(tokens: {:.2})", b.tokens)
556                    }
557                    RateLimitState::SlidingWindow(c) => {
558                        format!("SlidingWindow(current: {})", c.current_count)
559                    }
560                    RateLimitState::FixedWindow(c) => {
561                        format!("FixedWindow(count: {})", c.count)
562                    }
563                };
564                (id.clone(), info)
565            })
566            .collect()
567    }
568
569    fn create_state(config: &RateLimitConfig) -> RateLimitState {
570        match config.algorithm {
571            RateLimitAlgorithm::TokenBucket => {
572                let refill_rate = config.max_requests as f64 / config.window_seconds as f64;
573                RateLimitState::TokenBucket(TokenBucket::new(config.max_requests, refill_rate))
574            }
575            RateLimitAlgorithm::SlidingWindow => {
576                RateLimitState::SlidingWindow(SlidingWindowCounter::new(config.window_seconds))
577            }
578            RateLimitAlgorithm::FixedWindow | RateLimitAlgorithm::Adaptive => {
579                RateLimitState::FixedWindow(FixedWindowCounter::new(config.window_seconds))
580            }
581        }
582    }
583}
584
585#[cfg(test)]
586mod tests {
587    use super::*;
588
589    #[tokio::test]
590    async fn test_rate_limit_config_default() {
591        let config = RateLimitConfig::default();
592        assert_eq!(config.algorithm, RateLimitAlgorithm::TokenBucket);
593        assert_eq!(config.max_requests, 100);
594        assert_eq!(config.window_seconds, 60);
595        assert!(config.complexity_based);
596    }
597
598    #[tokio::test]
599    async fn test_token_bucket_creation() {
600        let bucket = TokenBucket::new(100, 10.0);
601        assert_eq!(bucket.capacity, 100.0);
602        assert_eq!(bucket.refill_rate, 10.0);
603    }
604
605    #[tokio::test]
606    async fn test_token_bucket_consume() {
607        let mut bucket = TokenBucket::new(100, 10.0);
608        assert!(bucket.consume(50.0));
609        assert!(bucket.tokens < 51.0 && bucket.tokens > 49.0);
610    }
611
612    #[tokio::test]
613    async fn test_sliding_window_counter() {
614        let mut counter = SlidingWindowCounter::new(60);
615        counter.increment(10);
616        assert_eq!(counter.current_count, 10);
617    }
618
619    #[tokio::test]
620    async fn test_fixed_window_counter() {
621        let mut counter = FixedWindowCounter::new(60);
622        counter.increment(10);
623        assert_eq!(counter.count, 10);
624
625        let result = counter.check(100);
626        assert!(result.is_ok());
627    }
628
629    #[tokio::test]
630    async fn test_rate_limiter_creation() {
631        let config = RateLimitConfig::default();
632        let _limiter = RateLimiter::new(config);
633    }
634
635    #[tokio::test]
636    async fn test_rate_limiter_check_allowed() {
637        let config = RateLimitConfig {
638            max_requests: 10,
639            ..Default::default()
640        };
641        let limiter = RateLimiter::new(config);
642
643        let result = limiter.check_rate_limit("client1", Some(1)).await;
644        assert!(result.is_ok());
645        let result = result.expect("should succeed");
646        assert!(result.allowed);
647    }
648
649    #[tokio::test]
650    async fn test_rate_limiter_check_exceeded() {
651        let config = RateLimitConfig {
652            algorithm: RateLimitAlgorithm::FixedWindow,
653            max_requests: 5,
654            window_seconds: 60,
655            ..Default::default()
656        };
657        let limiter = RateLimiter::new(config);
658
659        // Make 5 requests (should succeed)
660        for _ in 0..5 {
661            let result = limiter
662                .check_rate_limit("client1", Some(1))
663                .await
664                .expect("should succeed");
665            assert!(result.allowed);
666        }
667
668        // 6th request should fail
669        let result = limiter
670            .check_rate_limit("client1", Some(1))
671            .await
672            .expect("should succeed");
673        assert!(!result.allowed);
674        assert!(result.retry_after.is_some());
675    }
676
677    #[tokio::test]
678    async fn test_rate_limiter_custom_policy() {
679        let default_config = RateLimitConfig {
680            max_requests: 10,
681            ..Default::default()
682        };
683        let limiter = RateLimiter::new(default_config);
684
685        // Add custom policy for VIP client
686        let vip_policy = RateLimitPolicy::new(
687            "vip_client".to_string(),
688            RateLimitConfig {
689                max_requests: 1000,
690                ..RateLimitConfig::default()
691            },
692        );
693        limiter
694            .add_policy(vip_policy)
695            .await
696            .expect("should succeed");
697
698        // VIP client should have higher limits
699        let result = limiter
700            .check_rate_limit("vip_client", Some(1))
701            .await
702            .expect("should succeed");
703        assert!(result.allowed);
704        assert_eq!(result.limit, 1000);
705    }
706
707    #[tokio::test]
708    async fn test_complexity_based_limiting() {
709        let config = RateLimitConfig {
710            complexity_based: true,
711            max_requests: 100,
712            max_complexity: Some(1000),
713            ..Default::default()
714        };
715        let limiter = RateLimiter::new(config);
716
717        // High complexity query
718        let result = limiter
719            .check_rate_limit("client1", Some(50))
720            .await
721            .expect("should succeed");
722        assert!(result.allowed);
723    }
724
725    #[tokio::test]
726    async fn test_rate_limiter_reset() {
727        let config = RateLimitConfig {
728            algorithm: RateLimitAlgorithm::FixedWindow,
729            max_requests: 5,
730            ..Default::default()
731        };
732        let limiter = RateLimiter::new(config);
733
734        // Exhaust limit
735        for _ in 0..5 {
736            let _ = limiter.check_rate_limit("client1", Some(1)).await;
737        }
738
739        // Should be rate limited
740        let result = limiter
741            .check_rate_limit("client1", Some(1))
742            .await
743            .expect("should succeed");
744        assert!(!result.allowed);
745
746        // Reset client
747        limiter
748            .reset_client("client1")
749            .await
750            .expect("should succeed");
751
752        // Should be allowed again
753        let result = limiter
754            .check_rate_limit("client1", Some(1))
755            .await
756            .expect("should succeed");
757        assert!(result.allowed);
758    }
759}