1use std::hash::Hash;
2use std::num::NonZeroU32;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
5use std::time::Duration;
6
7use parking_lot::RwLock;
8use serde::{Deserialize, Serialize};
9
10use crate::{FastDashMap, FastHashMap, serde_helpers, time};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub struct TrafficLimit {
14 pub rate_per_sec: NonZeroU32,
15 pub burst: NonZeroU32,
16}
17
18impl TrafficLimit {
19 pub const MAX_RATE_PER_SEC: u32 = 1_000;
21
22 pub const fn new(rate_per_sec: NonZeroU32, burst: NonZeroU32) -> Self {
23 Self {
24 rate_per_sec,
25 burst,
26 }
27 }
28
29 fn normalize(&mut self) {
30 if self.rate_per_sec.get() > Self::MAX_RATE_PER_SEC {
31 self.rate_per_sec = NonZeroU32::new(Self::MAX_RATE_PER_SEC).unwrap();
32 }
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(default)]
38pub struct RateLimitConfig {
39 pub rejects_before_cooldown: u8,
40 #[serde(with = "serde_helpers::humantime")]
41 pub cooldown: Duration,
42 #[serde(with = "serde_helpers::humantime")]
43 pub prune_interval: Duration,
44 #[serde(with = "serde_helpers::humantime")]
45 pub state_ttl: Duration,
46}
47
48impl RateLimitConfig {
49 pub const MIN_REJECTS_BEFORE_COOLDOWN: u8 = 1;
50 pub const MAX_REJECTS_BEFORE_COOLDOWN: u8 = u8::MAX - 1;
51 pub const MIN_STATE_TTL: Duration = Duration::from_secs(1);
52 pub const MIN_PRUNE_INTERVAL: Duration = Duration::from_secs(1);
53
54 fn normalize(&mut self) {
55 self.rejects_before_cooldown = self.rejects_before_cooldown.clamp(
56 Self::MIN_REJECTS_BEFORE_COOLDOWN,
57 Self::MAX_REJECTS_BEFORE_COOLDOWN,
58 );
59
60 if self.prune_interval.is_zero() {
61 self.prune_interval = Self::MIN_PRUNE_INTERVAL;
62 }
63
64 if self.state_ttl.is_zero() {
65 self.state_ttl = Self::MIN_STATE_TTL;
66 }
67 }
68}
69
70impl Default for RateLimitConfig {
71 fn default() -> Self {
72 Self {
73 rejects_before_cooldown: 5,
74 cooldown: Duration::from_secs(30),
75 prune_interval: Duration::from_secs(30),
76 state_ttl: Duration::from_secs(300),
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq)]
82pub enum RateLimitVerdict {
83 Allow,
84 Reject { retry_after: Duration },
85}
86
87#[derive(Debug, Clone, Copy)]
88pub struct RateLimitPolicy<C> {
89 pub class: C,
90 pub limit: TrafficLimit,
91}
92
93#[derive(Clone)]
94pub struct RateLimiter<K, C> {
95 inner: Arc<RateLimiterInner<K, C>>,
96}
97
98struct RateLimiterInner<K, C> {
99 config: RateLimitConfig,
100 states: FastDashMap<K, Arc<PeerLimiter<C>>>,
101 last_prune_ms: AtomicU64,
102}
103
104impl<K, C> RateLimiter<K, C>
105where
106 K: Clone + Eq + Hash + Send + Sync + 'static,
107 C: Copy + Eq + Hash + Send + Sync + 'static,
108{
109 pub fn new(mut config: RateLimitConfig) -> Self {
110 config.normalize();
111
112 Self {
113 inner: Arc::new(RateLimiterInner {
114 config,
115 states: FastDashMap::default(),
116 last_prune_ms: AtomicU64::new(time::now_millis()),
117 }),
118 }
119 }
120
121 pub fn check(&self, key: &K, policy: RateLimitPolicy<C>) -> RateLimitVerdict {
122 self.inner.check(key, policy)
123 }
124}
125
126impl<K, C> RateLimiterInner<K, C>
127where
128 K: Clone + Eq + Hash + Send + Sync + 'static,
129 C: Copy + Eq + Hash + Send + Sync + 'static,
130{
131 fn check(&self, key: &K, policy: RateLimitPolicy<C>) -> RateLimitVerdict {
132 let now = time::now_millis();
133
134 self.maybe_prune(now);
135
136 let peer = self.peer(key, now);
137 peer.check(&self.config, policy, now)
138 }
139
140 fn peer(&self, key: &K, now: u64) -> Arc<PeerLimiter<C>> {
141 let state_ttl = self.config.state_ttl.as_millis_u64();
142
143 if let Some(peer) = self.states.get(key)
144 && !peer.is_expired(now, state_ttl)
145 {
146 return peer.clone();
147 }
148
149 let mut entry = self
150 .states
151 .entry(key.clone())
152 .or_insert_with(|| Arc::new(PeerLimiter::new(now)));
153
154 if entry.is_expired(now, state_ttl) {
155 *entry = Arc::new(PeerLimiter::new(now));
156 }
157
158 entry.clone()
159 }
160
161 fn prune_expired(&self, now: u64) {
162 let state_ttl = self.config.state_ttl.as_millis_u64();
163
164 self.states
165 .retain(|_, peer| peer.in_cooldown(now) || !peer.is_expired(now, state_ttl));
166 }
167
168 fn maybe_prune(&self, now: u64) {
169 let last = self.last_prune_ms.load(Ordering::Relaxed);
170
171 if now.saturating_sub(last) < self.config.prune_interval.as_millis_u64() {
172 return;
173 }
174
175 if self
176 .last_prune_ms
177 .compare_exchange(last, now, Ordering::Relaxed, Ordering::Relaxed)
178 .is_ok()
179 {
180 self.prune_expired(now);
181 }
182 }
183}
184
185struct PeerLimiter<C> {
186 buckets: RwLock<FastHashMap<C, Arc<AtomicBucket>>>,
187 rejects: AtomicU8,
188 cooldown_until_ms: AtomicU64,
189 last_seen_ms: AtomicU64,
190}
191
192impl<C> PeerLimiter<C>
193where
194 C: Copy + Eq + Hash + Send + Sync + 'static,
195{
196 fn new(now_ms: u64) -> Self {
197 Self {
198 buckets: RwLock::new(FastHashMap::default()),
199 rejects: AtomicU8::new(0),
200 cooldown_until_ms: AtomicU64::new(0),
201 last_seen_ms: AtomicU64::new(now_ms),
202 }
203 }
204
205 fn check(
206 &self,
207 config: &RateLimitConfig,
208 policy: RateLimitPolicy<C>,
209 now: u64,
210 ) -> RateLimitVerdict {
211 self.last_seen_ms.store(now, Ordering::Relaxed);
212
213 let cooldown_until = self.cooldown_until_ms.load(Ordering::Acquire);
214 if now < cooldown_until {
215 return RateLimitVerdict::Reject {
216 retry_after: Duration::from_millis(cooldown_until - now),
217 };
218 }
219
220 let bucket = self.bucket(policy.class, policy.limit);
221 let BucketResult::TryLater { after } = bucket.try_claim(now) else {
222 self.rejects.store(0, Ordering::Relaxed);
223 return RateLimitVerdict::Allow;
224 };
225
226 self.register_rejection(config, now);
227
228 let cooldown_until = self.cooldown_until_ms.load(Ordering::Acquire);
230 let retry_after = if now < cooldown_until {
231 cooldown_until - now
232 } else {
233 after.saturating_sub(now)
234 };
235
236 RateLimitVerdict::Reject {
237 retry_after: Duration::from_millis(retry_after),
238 }
239 }
240
241 fn bucket(&self, class: C, config: TrafficLimit) -> Arc<AtomicBucket> {
242 if let Some(bucket) = self.buckets.read().get(&class) {
243 return bucket.clone();
244 }
245
246 self.buckets
247 .write()
248 .entry(class)
249 .or_insert_with(|| Arc::new(AtomicBucket::new(config)))
250 .clone()
251 }
252
253 fn register_rejection(&self, config: &RateLimitConfig, now: u64) {
254 let prev_rejects = self.rejects.fetch_add(1, Ordering::AcqRel);
255 debug_assert!(prev_rejects < u8::MAX);
256
257 let rejects = prev_rejects.saturating_add(1);
258 if rejects >= config.rejects_before_cooldown {
259 self.rejects.store(0, Ordering::Release);
260 self.cooldown_until_ms.fetch_max(
261 now.saturating_add(config.cooldown.as_millis_u64()),
262 Ordering::AcqRel,
263 );
264 }
265 }
266
267 fn is_expired(&self, now: u64, state_ttl: u64) -> bool {
268 let last_seen = self.last_seen_ms.load(Ordering::Relaxed);
269 now.saturating_sub(last_seen) >= state_ttl
270 }
271
272 fn in_cooldown(&self, now: u64) -> bool {
273 now < self.cooldown_until_ms.load(Ordering::Acquire)
274 }
275}
276
277#[derive(Debug, PartialEq, Eq)]
278enum BucketResult {
279 Ok,
280 TryLater { after: u64 },
281}
282
283struct AtomicBucket {
285 tat_ms: AtomicU64,
287 interval_ms: u64,
289 tolerance_ms: u64,
291}
292
293impl AtomicBucket {
294 const MILLIS_PER_SEC: u64 = 1_000;
295
296 fn new(mut config: TrafficLimit) -> Self {
297 config.normalize();
298
299 let rate_per_sec = config.rate_per_sec.get() as u64;
300 let interval_ms = Self::MILLIS_PER_SEC.div_ceil(rate_per_sec).max(1);
301 let tolerance_ms = interval_ms.saturating_mul(config.burst.get().saturating_sub(1) as u64);
302
303 Self {
304 tat_ms: AtomicU64::new(0),
305 interval_ms,
306 tolerance_ms,
307 }
308 }
309
310 fn try_claim(&self, now: u64) -> BucketResult {
311 let mut tat = self.tat_ms.load(Ordering::Relaxed);
312
313 loop {
314 let allowed_at = tat.saturating_sub(self.tolerance_ms);
315 if now < allowed_at {
316 return BucketResult::TryLater { after: allowed_at };
317 }
318
319 let new_tat = tat.max(now).saturating_add(self.interval_ms);
320 match self.tat_ms.compare_exchange_weak(
321 tat,
322 new_tat,
323 Ordering::AcqRel,
324 Ordering::Relaxed,
325 ) {
326 Ok(_) => return BucketResult::Ok,
327 Err(next) => tat = next,
328 }
329 }
330 }
331}
332
333trait DurationExt {
334 fn as_millis_u64(&self) -> u64;
335}
336
337impl DurationExt for Duration {
338 fn as_millis_u64(&self) -> u64 {
339 self.as_millis().try_into().unwrap_or(u64::MAX)
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
348 enum Class {
349 A,
350 B,
351 }
352
353 fn bucket_config(rate_per_sec: u32, burst: u32) -> TrafficLimit {
354 TrafficLimit::new(
355 NonZeroU32::new(rate_per_sec).unwrap(),
356 NonZeroU32::new(burst).unwrap(),
357 )
358 }
359
360 fn policy(class: Class) -> RateLimitPolicy<Class> {
361 RateLimitPolicy {
362 class,
363 limit: bucket_config(1, 1),
364 }
365 }
366
367 fn rate_limiter() -> RateLimiter<u32, Class> {
368 RateLimiter::new(RateLimitConfig {
369 rejects_before_cooldown: 2,
370 ..Default::default()
371 })
372 }
373
374 #[test]
375 fn gcra_bucket_burst_and_refills() {
376 let now = time::now_millis();
377
378 let bucket = AtomicBucket::new(bucket_config(10, 2));
379
380 let delay = Duration::from_millis(100).as_millis_u64();
382
383 assert_eq!(bucket.try_claim(now), BucketResult::Ok);
385 assert_eq!(bucket.try_claim(now), BucketResult::Ok);
386
387 assert_eq!(bucket.try_claim(now), BucketResult::TryLater {
388 after: now + delay,
389 });
390
391 assert_eq!(bucket.try_claim(now + delay), BucketResult::Ok);
393 }
394
395 #[test]
396 fn rate_limiter_cooldown() {
397 let limiter = rate_limiter();
398
399 let key = 1;
400
401 assert_eq!(
403 limiter.check(&key, policy(Class::A)),
404 RateLimitVerdict::Allow
405 );
406
407 assert!(matches!(
409 limiter.check(&key, policy(Class::A)),
410 RateLimitVerdict::Reject { retry_after }
411 if retry_after <= Duration::from_secs(1)
412 ));
413
414 assert!(matches!(
416 limiter.check(&key, policy(Class::A)),
417 RateLimitVerdict::Reject { retry_after }
418 if retry_after > Duration::from_secs(29)
419 ));
420 }
421
422 #[test]
423 fn rate_limiter_keep_buckets_per_class() {
424 let limiter = rate_limiter();
425
426 let key = 1;
427
428 assert_eq!(
430 limiter.check(&key, policy(Class::A)),
431 RateLimitVerdict::Allow
432 );
433
434 assert_eq!(
436 limiter.check(&key, policy(Class::B)),
437 RateLimitVerdict::Allow
438 );
439
440 assert!(matches!(
441 limiter.check(&key, policy(Class::A)),
442 RateLimitVerdict::Reject { .. },
443 ));
444
445 assert!(matches!(
446 limiter.check(&key, policy(Class::B)),
447 RateLimitVerdict::Reject { .. },
448 ));
449 }
450}