1use crate::{
2 AcquireResult, PeekResult, RateLimitError, RateLimitPolicy, RateLimiter, TryAcquireResult,
3};
4use async_trait::async_trait;
5use std::collections::HashMap;
6use std::sync::Arc;
7use std::time::{Duration, Instant};
8use tokio::sync::Mutex;
9
10#[derive(Debug, Clone)]
11pub struct LocalRateLimiter {
12 interval: Duration,
13 state: Arc<Mutex<HashMap<String, Instant>>>,
14}
15
16impl LocalRateLimiter {
17 pub fn new(policy: RateLimitPolicy) -> Result<Self, RateLimitError> {
18 let interval = policy.interval()?;
19 Ok(Self {
20 interval,
21 state: Arc::new(Mutex::new(HashMap::new())),
22 })
23 }
24
25 async fn reserve_slot(&self, key: &str) -> (Duration, Instant) {
26 let now = Instant::now();
27 let mut state = self.state.lock().await;
28 let next_allowed = state.get(key).copied().unwrap_or(now);
29 let wait_duration = next_allowed.saturating_duration_since(now);
30 let acquired_at = now + wait_duration;
31 state.insert(key.to_owned(), acquired_at + self.interval);
32 (wait_duration, acquired_at)
33 }
34
35 async fn try_reserve_slot(&self, key: &str) -> TryAcquireResult {
36 let now = Instant::now();
37 let mut state = self.state.lock().await;
38 let next_allowed = state.get(key).copied().unwrap_or(now);
39 let wait_duration = next_allowed.saturating_duration_since(now);
40 if !wait_duration.is_zero() {
41 return TryAcquireResult::Limited { wait_duration };
42 }
43
44 state.insert(key.to_owned(), now + self.interval);
45 TryAcquireResult::Acquired
46 }
47
48 async fn inspect(&self, key: &str) -> PeekResult {
49 let now = Instant::now();
50 let state = self.state.lock().await;
51 let wait_duration = state
52 .get(key)
53 .copied()
54 .unwrap_or(now)
55 .saturating_duration_since(now);
56 PeekResult {
57 allowed: wait_duration.is_zero(),
58 wait_duration,
59 }
60 }
61}
62
63#[async_trait]
64impl RateLimiter for LocalRateLimiter {
65 async fn acquire(&self, key: &str) -> Result<AcquireResult, RateLimitError> {
66 let (waited, _) = self.reserve_slot(key).await;
67 if !waited.is_zero() {
68 tokio::time::sleep(waited).await;
69 }
70 Ok(AcquireResult {
71 key: key.to_owned(),
72 waited,
73 })
74 }
75
76 async fn try_acquire(&self, key: &str) -> Result<TryAcquireResult, RateLimitError> {
77 Ok(self.try_reserve_slot(key).await)
78 }
79
80 async fn peek(&self, key: &str) -> Result<PeekResult, RateLimitError> {
81 Ok(self.inspect(key).await)
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::LocalRateLimiter;
88 use crate::{RateLimitPolicy, RateLimiter, TryAcquireResult};
89 use std::time::Duration;
90
91 fn assert_duration_at_least(actual: Duration, expected: Duration) {
92 assert!(actual >= expected.saturating_sub(Duration::from_millis(1)));
93 assert!(actual <= expected);
94 }
95
96 #[tokio::test(start_paused = true)]
97 async fn same_key_waits_on_second_acquire() {
98 let limiter =
99 LocalRateLimiter::new(RateLimitPolicy::min_interval(Duration::from_secs(3)).unwrap())
100 .unwrap();
101
102 let first = limiter.acquire("same").await.unwrap();
103 assert_eq!(first.waited, Duration::ZERO);
104
105 let limiter_clone = limiter.clone();
106 let task = tokio::spawn(async move { limiter_clone.acquire("same").await.unwrap() });
107
108 tokio::task::yield_now().await;
109 assert!(!task.is_finished());
110
111 tokio::time::advance(Duration::from_secs(3)).await;
112 let second = task.await.unwrap();
113 assert_duration_at_least(second.waited, Duration::from_secs(3));
114 }
115
116 #[tokio::test(start_paused = true)]
117 async fn different_keys_do_not_block_each_other() {
118 let limiter =
119 LocalRateLimiter::new(RateLimitPolicy::min_interval(Duration::from_secs(2)).unwrap())
120 .unwrap();
121
122 limiter.acquire("a").await.unwrap();
123 let second = limiter.acquire("b").await.unwrap();
124 assert_eq!(second.waited, Duration::ZERO);
125 }
126
127 #[tokio::test(start_paused = true)]
128 async fn try_acquire_reports_wait_duration() {
129 let limiter =
130 LocalRateLimiter::new(RateLimitPolicy::min_interval(Duration::from_secs(5)).unwrap())
131 .unwrap();
132
133 assert!(matches!(
134 limiter.try_acquire("same").await.unwrap(),
135 TryAcquireResult::Acquired
136 ));
137
138 match limiter.try_acquire("same").await.unwrap() {
139 TryAcquireResult::Limited { wait_duration } => {
140 assert_duration_at_least(wait_duration, Duration::from_secs(5));
141 }
142 TryAcquireResult::Acquired => panic!("expected limited result"),
143 }
144 }
145
146 #[tokio::test(start_paused = true)]
147 async fn peek_does_not_consume_slot() {
148 let limiter =
149 LocalRateLimiter::new(RateLimitPolicy::min_interval(Duration::from_secs(4)).unwrap())
150 .unwrap();
151
152 let peek = limiter.peek("diag").await.unwrap();
153 assert!(peek.allowed);
154 assert_eq!(peek.wait_duration, Duration::ZERO);
155
156 let acquire = limiter.acquire("diag").await.unwrap();
157 assert_eq!(acquire.waited, Duration::ZERO);
158 }
159}