1#![allow(
2 clippy::cast_possible_truncation,
3 clippy::cast_sign_loss,
4 clippy::cast_precision_loss,
5 clippy::cast_possible_wrap
6)]
7use crate::rate_limit::RateLimitConfig;
55use async_trait::async_trait;
56use std::collections::HashMap;
57use std::sync::Arc;
58use std::time::Duration;
59use tokio::sync::Mutex;
60use tokio::time::Instant;
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum DistributedAlgorithm {
69 TokenBucket,
71 SlidingWindow,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq)]
80pub struct RateLimitParams {
81 pub algorithm: DistributedAlgorithm,
83 pub rate: f64,
85 pub burst: f64,
87 pub window_secs: u64,
89}
90
91impl RateLimitParams {
92 #[must_use]
96 pub fn from_config(config: &RateLimitConfig) -> Self {
97 if config.sliding_window {
98 Self {
99 algorithm: DistributedAlgorithm::SlidingWindow,
100 rate: config.rate,
101 burst: f64::from(config.effective_burst()),
102 window_secs: config.window_size.max(1),
103 }
104 } else {
105 Self {
106 algorithm: DistributedAlgorithm::TokenBucket,
107 rate: config.rate,
108 burst: f64::from(config.effective_burst()),
109 window_secs: config.window_size.max(1),
110 }
111 }
112 }
113
114 #[inline]
116 #[must_use]
117 pub fn max_window_events(&self) -> u64 {
118 (self.rate * self.window_secs as f64).ceil().max(0.0) as u64
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq)]
124pub struct AcquireOutcome {
125 pub allowed: bool,
127 pub remaining: f64,
129 pub retry_after: Duration,
131}
132
133impl AcquireOutcome {
134 #[inline]
136 #[must_use]
137 pub fn allowed(remaining: f64) -> Self {
138 Self {
139 allowed: true,
140 remaining,
141 retry_after: Duration::ZERO,
142 }
143 }
144
145 #[inline]
147 #[must_use]
148 pub fn denied(remaining: f64, retry_after: Duration) -> Self {
149 Self {
150 allowed: false,
151 remaining,
152 retry_after,
153 }
154 }
155}
156
157#[async_trait]
169pub trait DistributedRateLimitBackend: Send + Sync {
170 async fn acquire(
178 &self,
179 key: &str,
180 cost: f64,
181 params: RateLimitParams,
182 ) -> crate::Result<AcquireOutcome>;
183
184 async fn available(&self, key: &str, params: RateLimitParams) -> crate::Result<f64>;
190
191 async fn time_until_available(
197 &self,
198 key: &str,
199 cost: f64,
200 params: RateLimitParams,
201 ) -> crate::Result<Duration>;
202
203 async fn reset(&self, key: &str) -> crate::Result<()>;
209
210 fn backend_name(&self) -> &str;
212}
213
214#[derive(Debug, Clone)]
216enum KeyState {
217 Bucket { tokens: f64, last_refill: Instant },
219 Window { stamps: Vec<Instant> },
221}
222
223#[derive(Debug, Default)]
234pub struct InMemoryDistributedBackend {
235 state: Mutex<HashMap<String, KeyState>>,
236}
237
238impl InMemoryDistributedBackend {
239 #[must_use]
241 pub fn new() -> Self {
242 Self {
243 state: Mutex::new(HashMap::new()),
244 }
245 }
246
247 pub async fn key_count(&self) -> usize {
249 self.state.lock().await.len()
250 }
251
252 #[inline]
254 fn refill_bucket(
255 tokens: f64,
256 last_refill: Instant,
257 now: Instant,
258 params: RateLimitParams,
259 ) -> f64 {
260 let elapsed = now.saturating_duration_since(last_refill).as_secs_f64();
261 (tokens + elapsed * params.rate).min(params.burst)
262 }
263
264 #[inline]
266 fn prune_window(stamps: &mut Vec<Instant>, now: Instant, params: RateLimitParams) {
267 let window = Duration::from_secs(params.window_secs);
268 if let Some(cutoff) = now.checked_sub(window) {
269 stamps.retain(|&t| t > cutoff);
270 }
271 }
272}
273
274#[async_trait]
275impl DistributedRateLimitBackend for InMemoryDistributedBackend {
276 async fn acquire(
277 &self,
278 key: &str,
279 cost: f64,
280 params: RateLimitParams,
281 ) -> crate::Result<AcquireOutcome> {
282 if cost < 0.0 {
283 return Err(crate::CelersError::Configuration(
284 "rate limit acquire cost must be non-negative".to_string(),
285 ));
286 }
287 let now = Instant::now();
288 let mut guard = self.state.lock().await;
289
290 match params.algorithm {
291 DistributedAlgorithm::TokenBucket => {
292 let entry = guard
293 .entry(key.to_string())
294 .or_insert_with(|| KeyState::Bucket {
295 tokens: params.burst,
296 last_refill: now,
297 });
298 if !matches!(entry, KeyState::Bucket { .. }) {
300 *entry = KeyState::Bucket {
301 tokens: params.burst,
302 last_refill: now,
303 };
304 }
305 let KeyState::Bucket {
306 tokens,
307 last_refill,
308 } = entry
309 else {
310 unreachable!("entry coerced to Bucket above")
311 };
312 *tokens = Self::refill_bucket(*tokens, *last_refill, now, params);
313 *last_refill = now;
314 if *tokens + f64::EPSILON >= cost {
315 *tokens -= cost;
316 Ok(AcquireOutcome::allowed(*tokens))
317 } else {
318 let deficit = cost - *tokens;
319 let retry_after = if params.rate > 0.0 {
320 Duration::from_secs_f64(deficit / params.rate)
321 } else {
322 Duration::MAX
323 };
324 Ok(AcquireOutcome::denied(*tokens, retry_after))
325 }
326 }
327 DistributedAlgorithm::SlidingWindow => {
328 let entry = guard
329 .entry(key.to_string())
330 .or_insert_with(|| KeyState::Window { stamps: Vec::new() });
331 if !matches!(entry, KeyState::Window { .. }) {
332 *entry = KeyState::Window { stamps: Vec::new() };
333 }
334 let KeyState::Window { stamps } = entry else {
335 unreachable!("entry coerced to Window above")
336 };
337 Self::prune_window(stamps, now, params);
338 let max = params.max_window_events();
339 let needed = cost.ceil() as u64;
340 let used = stamps.len() as u64;
341 if used + needed <= max {
342 for _ in 0..needed {
343 stamps.push(now);
344 }
345 let remaining = max.saturating_sub(used + needed) as f64;
346 Ok(AcquireOutcome::allowed(remaining))
347 } else {
348 let remaining = max.saturating_sub(used) as f64;
349 let retry_after = stamps.first().map_or(Duration::ZERO, |&oldest| {
350 let expires = oldest + Duration::from_secs(params.window_secs);
351 expires.saturating_duration_since(now)
352 });
353 Ok(AcquireOutcome::denied(remaining, retry_after))
354 }
355 }
356 }
357 }
358
359 async fn available(&self, key: &str, params: RateLimitParams) -> crate::Result<f64> {
360 let now = Instant::now();
361 let mut guard = self.state.lock().await;
362 match params.algorithm {
363 DistributedAlgorithm::TokenBucket => match guard.get_mut(key) {
364 Some(KeyState::Bucket {
365 tokens,
366 last_refill,
367 }) => {
368 *tokens = Self::refill_bucket(*tokens, *last_refill, now, params);
369 *last_refill = now;
370 Ok(*tokens)
371 }
372 _ => Ok(params.burst),
373 },
374 DistributedAlgorithm::SlidingWindow => {
375 let max = params.max_window_events() as f64;
376 match guard.get_mut(key) {
377 Some(KeyState::Window { stamps }) => {
378 Self::prune_window(stamps, now, params);
379 Ok(max - stamps.len() as f64)
380 }
381 _ => Ok(max),
382 }
383 }
384 }
385 }
386
387 async fn time_until_available(
388 &self,
389 key: &str,
390 cost: f64,
391 params: RateLimitParams,
392 ) -> crate::Result<Duration> {
393 let now = Instant::now();
394 let mut guard = self.state.lock().await;
395 match params.algorithm {
396 DistributedAlgorithm::TokenBucket => {
397 let tokens = match guard.get_mut(key) {
398 Some(KeyState::Bucket {
399 tokens,
400 last_refill,
401 }) => {
402 *tokens = Self::refill_bucket(*tokens, *last_refill, now, params);
403 *last_refill = now;
404 *tokens
405 }
406 _ => params.burst,
407 };
408 if tokens + f64::EPSILON >= cost {
409 Ok(Duration::ZERO)
410 } else if params.rate > 0.0 {
411 Ok(Duration::from_secs_f64((cost - tokens) / params.rate))
412 } else {
413 Ok(Duration::MAX)
414 }
415 }
416 DistributedAlgorithm::SlidingWindow => {
417 let max = params.max_window_events();
418 let needed = cost.ceil() as u64;
419 match guard.get_mut(key) {
420 Some(KeyState::Window { stamps }) => {
421 Self::prune_window(stamps, now, params);
422 if stamps.len() as u64 + needed <= max {
423 Ok(Duration::ZERO)
424 } else {
425 Ok(stamps.first().map_or(Duration::ZERO, |&oldest| {
426 let expires = oldest + Duration::from_secs(params.window_secs);
427 expires.saturating_duration_since(now)
428 }))
429 }
430 }
431 _ => Ok(Duration::ZERO),
432 }
433 }
434 }
435 }
436
437 async fn reset(&self, key: &str) -> crate::Result<()> {
438 self.state.lock().await.remove(key);
439 Ok(())
440 }
441
442 fn backend_name(&self) -> &str {
443 "in-memory"
444 }
445}
446
447#[derive(Clone)]
456pub struct DistributedRateLimiter {
457 backend: Arc<dyn DistributedRateLimitBackend>,
458 key: String,
459 config: RateLimitConfig,
460 params: RateLimitParams,
461}
462
463impl std::fmt::Debug for DistributedRateLimiter {
464 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
465 f.debug_struct("DistributedRateLimiter")
466 .field("key", &self.key)
467 .field("backend", &self.backend.backend_name())
468 .field("config", &self.config)
469 .finish()
470 }
471}
472
473impl DistributedRateLimiter {
474 pub fn new(
482 backend: Arc<dyn DistributedRateLimitBackend>,
483 key: impl Into<String>,
484 config: RateLimitConfig,
485 ) -> Self {
486 let params = RateLimitParams::from_config(&config);
487 Self {
488 backend,
489 key: key.into(),
490 config,
491 params,
492 }
493 }
494
495 #[inline]
497 #[must_use]
498 pub fn key(&self) -> &str {
499 &self.key
500 }
501
502 #[inline]
504 #[must_use]
505 pub fn config(&self) -> &RateLimitConfig {
506 &self.config
507 }
508
509 #[inline]
511 #[must_use]
512 pub fn algorithm(&self) -> DistributedAlgorithm {
513 self.params.algorithm
514 }
515
516 pub async fn try_acquire(&self) -> crate::Result<bool> {
524 Ok(self
525 .backend
526 .acquire(&self.key, 1.0, self.params)
527 .await?
528 .allowed)
529 }
530
531 pub async fn try_acquire_n(&self, cost: f64) -> crate::Result<AcquireOutcome> {
540 self.backend.acquire(&self.key, cost, self.params).await
541 }
542
543 pub async fn acquire(&self, max_sleep: Duration) -> crate::Result<Duration> {
553 let start = Instant::now();
554 loop {
555 let outcome = self.backend.acquire(&self.key, 1.0, self.params).await?;
556 if outcome.allowed {
557 return Ok(start.elapsed());
558 }
559 let sleep_for = outcome
560 .retry_after
561 .min(max_sleep)
562 .max(Duration::from_millis(1));
563 tokio::time::sleep(sleep_for).await;
564 }
565 }
566
567 pub async fn available(&self) -> crate::Result<f64> {
573 self.backend.available(&self.key, self.params).await
574 }
575
576 pub async fn time_until_available(&self) -> crate::Result<Duration> {
582 self.backend
583 .time_until_available(&self.key, 1.0, self.params)
584 .await
585 }
586
587 pub async fn reset(&self) -> crate::Result<()> {
593 self.backend.reset(&self.key).await
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600
601 #[tokio::test]
602 async fn test_token_bucket_burst_then_deny() {
603 let backend = Arc::new(InMemoryDistributedBackend::new());
604 let config = RateLimitConfig::new(10.0).with_burst(5);
605 let limiter = DistributedRateLimiter::new(backend, "task", config);
606
607 for _ in 0..5 {
608 assert!(limiter.try_acquire().await.unwrap());
609 }
610 assert!(!limiter.try_acquire().await.unwrap());
611 assert_eq!(limiter.algorithm(), DistributedAlgorithm::TokenBucket);
612 }
613
614 #[tokio::test]
615 async fn test_token_bucket_refill() {
616 let backend = Arc::new(InMemoryDistributedBackend::new());
617 let config = RateLimitConfig::new(100.0).with_burst(10);
619 let limiter = DistributedRateLimiter::new(backend, "task", config);
620
621 for _ in 0..10 {
622 assert!(limiter.try_acquire().await.unwrap());
623 }
624 assert!(!limiter.try_acquire().await.unwrap());
625
626 tokio::time::sleep(Duration::from_millis(30)).await;
627 assert!(limiter.try_acquire().await.unwrap());
629 }
630
631 #[tokio::test]
632 async fn test_sliding_window_basic_and_deny() {
633 let backend = Arc::new(InMemoryDistributedBackend::new());
634 let config = RateLimitConfig::new(5.0).with_sliding_window(1);
635 let limiter = DistributedRateLimiter::new(backend, "task", config);
636 assert_eq!(limiter.algorithm(), DistributedAlgorithm::SlidingWindow);
637
638 for _ in 0..5 {
639 assert!(limiter.try_acquire().await.unwrap());
640 }
641 assert!(!limiter.try_acquire().await.unwrap());
642 let wait = limiter.time_until_available().await.unwrap();
643 assert!(wait > Duration::ZERO);
644 }
645
646 #[tokio::test]
647 async fn test_cost_n_acquire() {
648 let backend = Arc::new(InMemoryDistributedBackend::new());
649 let config = RateLimitConfig::new(10.0).with_burst(10);
650 let limiter = DistributedRateLimiter::new(backend, "task", config);
651
652 let outcome = limiter.try_acquire_n(4.0).await.unwrap();
653 assert!(outcome.allowed);
654 assert!((outcome.remaining - 6.0).abs() < 1e-6);
655
656 let denied = limiter.try_acquire_n(7.0).await.unwrap();
658 assert!(!denied.allowed);
659 assert!(denied.retry_after > Duration::ZERO);
660 }
661
662 #[tokio::test]
663 async fn test_available_and_reset() {
664 let backend = Arc::new(InMemoryDistributedBackend::new());
667 let config = RateLimitConfig::new(0.0).with_burst(8);
668 let limiter = DistributedRateLimiter::new(backend.clone(), "task", config);
669
670 assert!((limiter.available().await.unwrap() - 8.0).abs() < 1e-6);
671 for _ in 0..3 {
672 assert!(limiter.try_acquire().await.unwrap());
673 }
674 let avail = limiter.available().await.unwrap();
675 assert!((avail - 5.0).abs() < 1e-6, "expected 5 tokens, got {avail}");
676
677 limiter.reset().await.unwrap();
678 assert!((limiter.available().await.unwrap() - 8.0).abs() < 1e-6);
679 }
680
681 #[tokio::test]
682 async fn test_concurrent_acquire_does_not_exceed_burst() {
683 let backend = Arc::new(InMemoryDistributedBackend::new());
685 let config = RateLimitConfig::new(0.0).with_burst(20);
686 let limiter = DistributedRateLimiter::new(backend, "task", config);
687
688 let mut handles = Vec::new();
689 for _ in 0..8 {
690 let l = limiter.clone();
691 handles.push(tokio::spawn(async move {
692 let mut count = 0u32;
693 for _ in 0..10 {
694 if l.try_acquire().await.unwrap_or(false) {
695 count += 1;
696 }
697 }
698 count
699 }));
700 }
701
702 let mut total = 0u32;
703 for h in handles {
704 total += h.await.unwrap();
705 }
706 assert_eq!(total, 20, "exactly the burst capacity should be granted");
708 }
709
710 #[tokio::test]
711 async fn test_concurrent_sliding_window_cap() {
712 let backend = Arc::new(InMemoryDistributedBackend::new());
713 let config = RateLimitConfig::new(3.0).with_sliding_window(100);
715 let limiter = DistributedRateLimiter::new(backend, "task", config);
716 let max = limiter.params.max_window_events();
717
718 let mut handles = Vec::new();
719 for _ in 0..6 {
720 let l = limiter.clone();
721 handles.push(tokio::spawn(async move {
722 let mut count = 0u32;
723 for _ in 0..200 {
724 if l.try_acquire().await.unwrap_or(false) {
725 count += 1;
726 }
727 }
728 count
729 }));
730 }
731 let mut total = 0u64;
732 for h in handles {
733 total += u64::from(h.await.unwrap());
734 }
735 assert_eq!(total, max, "sliding window must not exceed max events");
736 }
737
738 #[tokio::test]
739 async fn test_acquire_waits_then_succeeds() {
740 let backend = Arc::new(InMemoryDistributedBackend::new());
741 let config = RateLimitConfig::new(50.0).with_burst(1);
742 let limiter = DistributedRateLimiter::new(backend, "task", config);
743
744 assert!(limiter.try_acquire().await.unwrap());
745 let waited = limiter.acquire(Duration::from_millis(100)).await.unwrap();
747 assert!(waited > Duration::ZERO);
748 }
749
750 #[tokio::test]
751 async fn test_shared_backend_across_keys() {
752 let backend = Arc::new(InMemoryDistributedBackend::new());
753 let a = DistributedRateLimiter::new(
754 backend.clone(),
755 "a",
756 RateLimitConfig::new(0.0).with_burst(2),
757 );
758 let b = DistributedRateLimiter::new(
759 backend.clone(),
760 "b",
761 RateLimitConfig::new(0.0).with_burst(2),
762 );
763
764 assert!(a.try_acquire().await.unwrap());
765 assert!(a.try_acquire().await.unwrap());
766 assert!(!a.try_acquire().await.unwrap());
767 assert!(b.try_acquire().await.unwrap());
769 assert!(b.try_acquire().await.unwrap());
770 assert!(!b.try_acquire().await.unwrap());
771 assert_eq!(backend.key_count().await, 2);
772 }
773
774 #[tokio::test]
775 async fn test_negative_cost_rejected() {
776 let backend = Arc::new(InMemoryDistributedBackend::new());
777 let config = RateLimitConfig::new(10.0).with_burst(5);
778 let limiter = DistributedRateLimiter::new(backend, "task", config);
779 assert!(limiter.try_acquire_n(-1.0).await.is_err());
780 }
781
782 #[tokio::test]
783 async fn test_params_from_config() {
784 let bucket = RateLimitParams::from_config(&RateLimitConfig::new(10.0).with_burst(20));
785 assert_eq!(bucket.algorithm, DistributedAlgorithm::TokenBucket);
786 assert!((bucket.burst - 20.0).abs() < 1e-6);
787
788 let window =
789 RateLimitParams::from_config(&RateLimitConfig::new(4.0).with_sliding_window(5));
790 assert_eq!(window.algorithm, DistributedAlgorithm::SlidingWindow);
791 assert_eq!(window.window_secs, 5);
792 assert_eq!(window.max_window_events(), 20);
793 }
794}