shared_framework/middleware/
rate.rs1use std::collections::HashMap;
14use std::sync::{Arc, Mutex};
15use std::time::{Duration, Instant};
16
17use crate::data::cache::RedisStorage;
18
19#[async_trait::async_trait]
22pub trait RateLimiter: Send + Sync {
23 async fn is_allowed(&self, key: &str, limit: u32) -> bool;
25}
26
27pub struct InMemoryRateLimiter {
32 inner: Mutex<HashMap<String, (u32, Instant)>>,
33 window: Duration,
34}
35
36impl InMemoryRateLimiter {
37 pub fn new() -> Self {
39 Self {
40 inner: Mutex::new(HashMap::new()),
41 window: Duration::from_secs(60),
42 }
43 }
44
45 pub fn new_with_cleanup() -> Arc<Self> {
48 let arc = Arc::new(Self {
49 inner: Mutex::new(HashMap::new()),
50 window: Duration::from_secs(60),
51 });
52 let weak = Arc::downgrade(&arc);
53 tokio::spawn(async move {
54 let mut interval = tokio::time::interval(Duration::from_secs(60));
55 loop {
56 interval.tick().await;
57 if let Some(limiter) = weak.upgrade() {
58 limiter.cleanup();
59 } else {
60 break;
61 }
62 }
63 });
64 arc
65 }
66
67 pub fn is_allowed_sync(&self, key: &str, limit: u32) -> bool {
69 let minute = chrono::Utc::now().timestamp() / 60;
70 let bucket_key = format!("{}:{}", key, minute);
71 let mut map = self.inner.lock().unwrap();
72 let now = Instant::now();
73 let entry = map.entry(bucket_key).or_insert((0, now + self.window));
74 if now > entry.1 {
75 *entry = (1, now + self.window);
76 return true;
77 }
78 if entry.0 < limit {
79 entry.0 += 1;
80 true
81 } else {
82 false
83 }
84 }
85
86 fn cleanup(&self) {
87 let now = Instant::now();
88 let mut map = self.inner.lock().unwrap();
89 map.retain(|_, (_, expiry)| *expiry > now);
90 }
91}
92
93impl Default for InMemoryRateLimiter {
94 fn default() -> Self {
95 Self::new()
96 }
97}
98
99#[async_trait::async_trait]
100impl RateLimiter for InMemoryRateLimiter {
101 async fn is_allowed(&self, key: &str, limit: u32) -> bool {
102 self.is_allowed_sync(key, limit)
103 }
104}
105
106pub struct RedisBackedRateLimiter {
111 storage: Option<RedisStorage>,
112 fallback: Arc<InMemoryRateLimiter>,
113}
114
115impl RedisBackedRateLimiter {
116 pub fn new() -> Self {
119 let storage = RedisStorage::from_env().ok();
120 Self {
121 storage,
122 fallback: InMemoryRateLimiter::new_with_cleanup(),
123 }
124 }
125
126 pub fn with_storage(storage: RedisStorage) -> Self {
128 Self {
129 storage: Some(storage),
130 fallback: InMemoryRateLimiter::new_with_cleanup(),
131 }
132 }
133
134 pub fn from_url(url: &str) -> Self {
136 let storage = RedisStorage::new(url).ok();
137 Self {
138 storage,
139 fallback: InMemoryRateLimiter::new_with_cleanup(),
140 }
141 }
142}
143
144impl Default for RedisBackedRateLimiter {
145 fn default() -> Self {
146 Self::new()
147 }
148}
149
150#[async_trait::async_trait]
151impl RateLimiter for RedisBackedRateLimiter {
152 async fn is_allowed(&self, key: &str, limit: u32) -> bool {
153 let Some(storage) = &self.storage else {
154 return self.fallback.is_allowed(key, limit).await;
155 };
156
157 let minute = chrono::Utc::now().timestamp() / 60;
158 let redis_key = format!("rate_limit:{}:{}", key, minute);
159
160 match storage.increment_value(&redis_key).await {
162 Ok(count) => {
163 if count == 1 {
164 let _ = storage.set_expiration(&redis_key, 60).await;
166 return true;
167 }
168 count <= limit as i64
169 }
170 Err(e) => {
171 tracing::warn!(backend = "redis", fallback_backend = "in_memory", error = %e, "Rate limiter failed; using fallback");
172 self.fallback.is_allowed(key, limit).await
173 }
174 }
175 }
176}
177
178pub enum LimiterKind {
182 InMemory,
184 Redis,
186}
187
188pub fn create(kind: LimiterKind) -> Arc<dyn RateLimiter> {
190 match kind {
191 LimiterKind::InMemory => InMemoryRateLimiter::new_with_cleanup(),
192 LimiterKind::Redis => Arc::new(RedisBackedRateLimiter::new()),
193 }
194}
195
196pub fn create_from_env() -> Arc<dyn RateLimiter> {
199 if std::env::var("REDIS_URL").is_ok() || std::env::var("REDIS_URI").is_ok() {
200 tracing::info!(component = "rate_limiter", backend = "redis", "Configured rate limiter");
201 Arc::new(RedisBackedRateLimiter::new())
202 } else {
203 tracing::info!(component = "rate_limiter", backend = "in_memory", "Configured rate limiter");
204 InMemoryRateLimiter::new_with_cleanup()
205 }
206}