skp_ratelimit/algorithm/
gcra.rs

1//! GCRA (Generic Cell Rate Algorithm) implementation.
2//!
3//! GCRA is an efficient rate limiting algorithm that tracks a Theoretical Arrival Time (TAT)
4//! instead of counters. It's known for:
5//! - Low memory usage (only one timestamp per key)
6//! - Precise control over request spacing
7//! - Excellent burst handling with even distribution
8//!
9//! # How It Works
10//!
11//! Instead of counting requests in a window, GCRA tracks when the next request
12//! is theoretically allowed (TAT - Theoretical Arrival Time).
13//!
14//! ```text
15//! Period: 100ms between requests (10/sec)
16//! Burst: 3 requests allowed ahead
17//!
18//! Time 0ms:   Request arrives, TAT = 0 + 100 = 100ms. ALLOWED
19//! Time 10ms:  Request arrives, TAT = 100 (>10), but 100 < 10+300 (burst). ALLOWED, TAT = 200
20//! Time 20ms:  Request arrives, TAT = 200 (>20), but 200 < 20+300. ALLOWED, TAT = 300
21//! Time 30ms:  Request arrives, TAT = 300 (>30), but 300 < 30+300. ALLOWED, TAT = 400
22//! Time 40ms:  Request arrives, TAT = 400 (>40), and 400 > 40+300. DENIED
23//! Time 350ms: Request arrives, TAT = max(350, 400) + 100 = 500. ALLOWED
24//! ```
25
26use std::time::Duration;
27
28use crate::algorithm::{current_timestamp_ms, timestamp_to_instant, Algorithm};
29use crate::decision::{Decision, DecisionMetadata, RateLimitInfo};
30use crate::error::Result;
31use crate::quota::Quota;
32use crate::storage::{Storage, StorageEntry};
33
34/// GCRA (Generic Cell Rate Algorithm) rate limiter.
35///
36/// This is the recommended algorithm for most use cases, offering:
37/// - Precise rate control with even request spacing
38/// - Low memory usage
39/// - Excellent burst handling
40/// - Clear "retry after" semantics
41///
42/// # Example
43///
44/// ```ignore
45/// use oc_ratelimit_advanced::{GCRA, Quota, MemoryStorage};
46/// use std::time::Duration;
47///
48/// let algorithm = GCRA::new();
49/// let storage = MemoryStorage::new();
50/// let quota = Quota::per_second(10).with_burst(15);
51///
52/// let decision = algorithm.check_and_record(&storage, "user:123", &quota).await?;
53/// ```
54#[derive(Debug, Clone, Default)]
55pub struct GCRA;
56
57impl GCRA {
58    /// Create a new GCRA algorithm instance.
59    pub fn new() -> Self {
60        Self
61    }
62
63    /// Calculate the decision based on current TAT and quota.
64    fn calculate_decision(
65        &self,
66        current_tat: Option<u64>,
67        now: u64,
68        quota: &Quota,
69    ) -> (bool, u64) {
70        let period_ms = quota.period().as_millis() as u64;
71        let max_tat_offset_ms = quota.max_tat_offset().as_millis() as u64;
72
73        // Get effective TAT (starts from now if first request)
74        let effective_tat = current_tat.unwrap_or(now);
75
76        // New TAT would be max(now, current_tat) + period
77        let new_tat = effective_tat.max(now) + period_ms;
78
79        // Calculate how far ahead we'd be
80        let tat_offset = new_tat.saturating_sub(now);
81
82        // Check if within burst tolerance
83        if tat_offset <= max_tat_offset_ms + period_ms {
84            // Allowed: update TAT
85            (true, new_tat)
86        } else {
87            // Denied: keep current TAT
88            (false, effective_tat)
89        }
90    }
91
92    /// Build rate limit info from current state.
93    fn build_info(&self, tat: u64, now: u64, quota: &Quota, allowed: bool) -> RateLimitInfo {
94        let period_ms = quota.period().as_millis() as u64;
95        let max_tat_offset_ms = quota.max_tat_offset().as_millis() as u64;
96        let limit = quota.effective_burst();
97
98        // Calculate remaining "tokens" (how many more requests fit in burst)
99        let tat_offset = tat.saturating_sub(now);
100        let remaining = if tat_offset == 0 {
101            limit
102        } else {
103            let used = (tat_offset / period_ms) + 1;
104            limit.saturating_sub(used)
105        };
106
107        // Reset time: when TAT catches up to current time
108        let reset_at = if tat > now {
109            timestamp_to_instant(tat)
110        } else {
111            timestamp_to_instant(now)
112        };
113
114        let mut info = RateLimitInfo::new(limit, remaining, reset_at, timestamp_to_instant(now))
115            .with_algorithm("gcra")
116            .with_metadata(DecisionMetadata::new().with_tat(tat));
117
118        // If denied, calculate retry-after
119        if !allowed {
120            let wait_ms = tat.saturating_sub(now).saturating_sub(max_tat_offset_ms);
121            if wait_ms > 0 {
122                info = info.with_retry_after(Duration::from_millis(wait_ms));
123            }
124        }
125
126        info
127    }
128}
129
130impl Algorithm for GCRA {
131    fn name(&self) -> &'static str {
132        "gcra"
133    }
134
135    async fn check_and_record<S: Storage>(
136        &self,
137        storage: &S,
138        key: &str,
139        quota: &Quota,
140    ) -> Result<Decision> {
141        let now = current_timestamp_ms();
142        let period_ms = quota.period().as_millis() as u64;
143        
144        // TTL based on max TAT offset (how far ahead we can schedule)
145        let ttl = Duration::from_millis(
146            quota.max_tat_offset().as_millis() as u64 + period_ms * 2
147        );
148
149        let decision = storage
150            .execute_atomic(key, ttl, |entry| {
151                let current_tat = entry.and_then(|e| e.tat);
152                let (allowed, new_tat) = self.calculate_decision(current_tat, now, quota);
153                
154                let new_entry = StorageEntry::with_tat(new_tat);
155                let info = self.build_info(new_tat, now, quota, allowed);
156                
157                let decision = if allowed {
158                    Decision::allowed(info)
159                } else {
160                    Decision::denied(info)
161                };
162                
163                (new_entry, decision)
164            })
165            .await?;
166
167        Ok(decision)
168    }
169
170    async fn check<S: Storage>(
171        &self,
172        storage: &S,
173        key: &str,
174        quota: &Quota,
175    ) -> Result<Decision> {
176        let now = current_timestamp_ms();
177
178        let entry = storage.get(key).await?;
179        let current_tat = entry.and_then(|e| e.tat);
180        
181        let (allowed, effective_tat) = self.calculate_decision(current_tat, now, quota);
182        let info = self.build_info(effective_tat, now, quota, allowed);
183
184        Ok(if allowed {
185            Decision::allowed(info)
186        } else {
187            Decision::denied(info)
188        })
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use crate::storage::MemoryStorage;
196
197    #[tokio::test]
198    async fn test_gcra_basic() {
199        let algorithm = GCRA::new();
200        let storage = MemoryStorage::new();
201        let quota = Quota::per_second(10); // 100ms between requests
202
203        // First request should be allowed
204        let decision = algorithm
205            .check_and_record(&storage, "user:1", &quota)
206            .await
207            .unwrap();
208        assert!(decision.is_allowed());
209    }
210
211    #[tokio::test]
212    async fn test_gcra_burst() {
213        let algorithm = GCRA::new();
214        let storage = MemoryStorage::new();
215        // 1 request per second with burst of 5
216        let quota = Quota::per_second(1).with_burst(5);
217
218        // Should allow burst of 5 requests
219        for i in 1..=5 {
220            let decision = algorithm
221                .check_and_record(&storage, "user:1", &quota)
222                .await
223                .unwrap();
224            assert!(decision.is_allowed(), "Request {} should be allowed", i);
225        }
226
227        // 6th request should be denied
228        let decision = algorithm
229            .check_and_record(&storage, "user:1", &quota)
230            .await
231            .unwrap();
232        assert!(decision.is_denied(), "Request 6 should be denied");
233        assert!(decision.info().retry_after.is_some());
234    }
235
236    #[tokio::test]
237    async fn test_gcra_recovery() {
238        let algorithm = GCRA::new();
239        let storage = MemoryStorage::new();
240        // 10 requests per second (100ms period), burst of 2
241        let quota = Quota::per_second(10).with_burst(2);
242
243        // Use up burst
244        algorithm.check_and_record(&storage, "user:1", &quota).await.unwrap();
245        algorithm.check_and_record(&storage, "user:1", &quota).await.unwrap();
246
247        // Should be denied
248        let decision = algorithm
249            .check_and_record(&storage, "user:1", &quota)
250            .await
251            .unwrap();
252        assert!(decision.is_denied());
253
254        // Wait for one period
255        tokio::time::sleep(Duration::from_millis(150)).await;
256
257        // Should be allowed again
258        let decision = algorithm
259            .check_and_record(&storage, "user:1", &quota)
260            .await
261            .unwrap();
262        assert!(decision.is_allowed());
263    }
264
265    #[tokio::test]
266    async fn test_gcra_check_without_record() {
267        let algorithm = GCRA::new();
268        let storage = MemoryStorage::new();
269        let quota = Quota::per_second(10).with_burst(5);
270
271        // Check without recording
272        let decision = algorithm.check(&storage, "user:1", &quota).await.unwrap();
273        assert!(decision.is_allowed());
274
275        // Check again - should still be allowed (no consumption)
276        let decision = algorithm.check(&storage, "user:1", &quota).await.unwrap();
277        assert!(decision.is_allowed());
278
279        // Now record one
280        algorithm.check_and_record(&storage, "user:1", &quota).await.unwrap();
281
282        // Check should show one less remaining
283        let decision = algorithm.check(&storage, "user:1", &quota).await.unwrap();
284        assert!(decision.info().remaining < 5);
285    }
286
287    #[tokio::test]
288    async fn test_gcra_separate_keys() {
289        let algorithm = GCRA::new();
290        let storage = MemoryStorage::new();
291        let quota = Quota::per_second(1).with_burst(1);
292
293        // User 1 uses their quota
294        algorithm.check_and_record(&storage, "user:1", &quota).await.unwrap();
295        let decision = algorithm.check_and_record(&storage, "user:1", &quota).await.unwrap();
296        assert!(decision.is_denied());
297
298        // User 2 should still have quota
299        let decision = algorithm.check_and_record(&storage, "user:2", &quota).await.unwrap();
300        assert!(decision.is_allowed());
301    }
302
303    #[tokio::test]
304    async fn test_gcra_reset() {
305        let algorithm = GCRA::new();
306        let storage = MemoryStorage::new();
307        let quota = Quota::per_second(1).with_burst(1);
308
309        // Use quota
310        algorithm.check_and_record(&storage, "user:1", &quota).await.unwrap();
311        let decision = algorithm.check_and_record(&storage, "user:1", &quota).await.unwrap();
312        assert!(decision.is_denied());
313
314        // Reset
315        algorithm.reset(&storage, "user:1").await.unwrap();
316
317        // Should be allowed again
318        let decision = algorithm.check_and_record(&storage, "user:1", &quota).await.unwrap();
319        assert!(decision.is_allowed());
320    }
321
322    #[test]
323    fn test_algorithm_name() {
324        let algorithm = GCRA::new();
325        assert_eq!(algorithm.name(), "gcra");
326    }
327}