use parking_lot::Mutex;
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::Arc;
use std::time::Instant;
pub fn canonicalize_ip(ip: IpAddr) -> IpAddr {
match ip {
IpAddr::V6(v6) => {
if let Some(v4) = v6.to_ipv4_mapped() {
IpAddr::V4(v4)
} else {
ip
}
}
_ => ip,
}
}
#[derive(Clone)]
pub struct RateLimiter {
inner: Arc<RateLimiterInner>,
}
struct RateLimiterInner {
shards: Vec<Mutex<HashMap<IpAddr, Bucket>>>,
limit: u32,
}
struct Bucket {
tokens: f64,
last_refill: Instant,
}
const NUM_SHARDS: usize = 64;
impl RateLimiter {
pub fn new(limit: u32) -> Self {
let shards = (0..NUM_SHARDS)
.map(|_| Mutex::new(HashMap::new()))
.collect();
Self {
inner: Arc::new(RateLimiterInner { shards, limit }),
}
}
#[inline]
pub fn check(&self, ip: IpAddr) -> bool {
let ip = canonicalize_ip(ip);
if self.inner.limit == 0 {
return true;
}
let shard_idx = shard_for_ip(&ip);
let mut shard = self.inner.shards[shard_idx].lock();
const MAX_ENTRIES_PER_SHARD: usize = 10_000;
if shard.len() >= MAX_ENTRIES_PER_SHARD && !shard.contains_key(&ip) {
if let Some(oldest_ip) = shard
.iter()
.min_by_key(|(_, b)| b.last_refill)
.map(|(ip, _)| *ip)
{
shard.remove(&oldest_ip);
}
}
let now = Instant::now();
let limit = self.inner.limit as f64;
let bucket = shard.entry(ip).or_insert_with(|| Bucket {
tokens: limit,
last_refill: now,
});
let elapsed = now.duration_since(bucket.last_refill).as_secs_f64();
bucket.tokens = (bucket.tokens + elapsed * limit).min(limit);
bucket.last_refill = now;
if bucket.tokens >= 1.0 {
bucket.tokens -= 1.0;
true
} else {
false
}
}
pub fn evict_stale(&self) {
let cutoff = Instant::now() - std::time::Duration::from_secs(60);
let mut evicted = 0usize;
for shard in &self.inner.shards {
let mut s = shard.lock();
let before = s.len();
s.retain(|_, bucket| bucket.last_refill > cutoff);
evicted += before - s.len();
}
if evicted > 0 {
tracing::debug!(evicted, "Rate limiter: evicted stale entries");
}
}
pub fn spawn_cleanup_task(self) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let mut ticker = tokio::time::interval(std::time::Duration::from_secs(30));
loop {
ticker.tick().await;
self.evict_stale();
}
})
}
}
fn shard_for_ip(ip: &IpAddr) -> usize {
let hash = match ip {
IpAddr::V4(v4) => {
let o = v4.octets();
let mut h: u32 = 2166136261;
for &b in &o {
h ^= b as u32;
h = h.wrapping_mul(16777619);
}
h
}
IpAddr::V6(v6) => {
let o = v6.octets();
let mut h: u32 = 2166136261;
for &b in &o {
h ^= b as u32;
h = h.wrapping_mul(16777619);
}
h
}
};
hash as usize & (NUM_SHARDS - 1)
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
#[test]
fn test_rate_limiter_allows_within_limit() {
let limiter = RateLimiter::new(10);
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
for _ in 0..10 {
assert!(limiter.check(ip));
}
assert!(!limiter.check(ip));
}
#[test]
fn test_rate_limiter_disabled_when_zero() {
let limiter = RateLimiter::new(0);
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
for _ in 0..10000 {
assert!(limiter.check(ip));
}
}
#[test]
fn test_rate_limiter_different_ips_independent() {
let limiter = RateLimiter::new(2);
let ip1 = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
let ip2 = IpAddr::V4(Ipv4Addr::new(2, 2, 2, 2));
assert!(limiter.check(ip1));
assert!(limiter.check(ip1));
assert!(!limiter.check(ip1));
assert!(limiter.check(ip2)); assert!(limiter.check(ip2));
assert!(!limiter.check(ip2)); }
}