use std::net::IpAddr;
use std::time::Duration;
use async_trait::async_trait;
use nest_rs_throttler::{Decision, Throttle, ThrottlerStore};
use redis::Script;
use crate::QueueConnection;
const WINDOW_SCRIPT: &str = r"
local count = redis.call('INCR', KEYS[1])
local ttl = redis.call('PTTL', KEYS[1])
if ttl < 0 then
redis.call('PEXPIRE', KEYS[1], ARGV[1])
ttl = tonumber(ARGV[1])
end
return {count, ttl}
";
pub struct RedisThrottler {
conn: QueueConnection,
default: Throttle,
trusted_proxies: Vec<IpAddr>,
script: Script,
}
impl RedisThrottler {
pub fn new(conn: QueueConnection, default: Throttle, trusted_proxies: Vec<IpAddr>) -> Self {
Self {
conn,
default,
trusted_proxies,
script: Script::new(WINDOW_SCRIPT),
}
}
async fn run(&self, key: &str, window_ms: u64) -> Result<(i64, i64), redis::RedisError> {
let namespaced = format!("nestrs:throttle:{key}");
let mut conn = self.conn.manager();
self.script
.key(namespaced)
.arg(window_ms)
.invoke_async::<(i64, i64)>(&mut conn)
.await
}
}
#[async_trait]
impl ThrottlerStore for RedisThrottler {
async fn hit(&self, key: &str, limit: Throttle) -> Decision {
let window_ms = limit.window.as_millis().min(u64::MAX as u128) as u64;
match self.run(key, window_ms).await {
Ok((count, ttl_ms)) => {
let allowed = count <= i64::from(limit.limit);
if allowed {
Decision {
allowed: true,
retry_after: Duration::ZERO,
}
} else {
let retry_after = if ttl_ms > 0 {
Duration::from_millis(ttl_ms as u64)
} else {
limit.window
};
Decision {
allowed: false,
retry_after,
}
}
}
Err(error) => {
tracing::warn!(
target: "nest_rs::throttler",
key = %key,
error = %error,
"redis throttler unavailable; denying (fail-closed)",
);
Decision {
allowed: false,
retry_after: limit.window,
}
}
}
}
fn default_limit(&self) -> Throttle {
self.default
}
fn trusted_proxies(&self) -> &[IpAddr] {
&self.trusted_proxies
}
}