use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use dashmap::DashMap;
use serde::Deserialize;
use crate::error::HsmError;
#[derive(Debug, Clone, Deserialize, Default)]
pub struct RateLimitConfig {
#[serde(default)]
pub max_ops_per_session_per_sec: u64,
#[serde(default)]
pub max_global_ops_per_sec: u64,
#[serde(default)]
pub max_concurrent_ops: u64,
}
const MICROTOKENS_PER_OP: u64 = 1_000_000;
const MAX_SESSION_BUCKETS: usize = 10_000;
#[derive(Debug)]
struct SessionRateState {
tokens: u64,
last_refill: Instant,
rate: u64,
capacity: u64,
}
impl SessionRateState {
fn new(rate: u64) -> Self {
let rate_micro = rate.saturating_mul(MICROTOKENS_PER_OP);
Self {
tokens: rate_micro, last_refill: Instant::now(),
rate: rate_micro,
capacity: rate_micro,
}
}
fn try_consume(&mut self) -> bool {
let now = Instant::now();
let elapsed_micros = now.duration_since(self.last_refill).as_micros() as u64;
let refill = (self.rate as u128).saturating_mul(elapsed_micros as u128) / 1_000_000;
self.tokens = self.tokens.saturating_add(refill as u64).min(self.capacity);
self.last_refill = now;
if self.tokens >= MICROTOKENS_PER_OP {
self.tokens -= MICROTOKENS_PER_OP;
true
} else {
false
}
}
}
#[derive(Debug)]
struct GlobalRateState {
tokens: u64,
last_refill: Instant,
rate: u64,
capacity: u64,
}
impl GlobalRateState {
fn new(rate: u64) -> Self {
let rate_micro = rate.saturating_mul(MICROTOKENS_PER_OP);
Self {
tokens: rate_micro,
last_refill: Instant::now(),
rate: rate_micro,
capacity: rate_micro,
}
}
fn try_consume(&mut self) -> bool {
let now = Instant::now();
let elapsed_micros = now.duration_since(self.last_refill).as_micros() as u64;
let refill = (self.rate as u128).saturating_mul(elapsed_micros as u128) / 1_000_000;
self.tokens = self.tokens.saturating_add(refill as u64).min(self.capacity);
self.last_refill = now;
if self.tokens >= MICROTOKENS_PER_OP {
self.tokens -= MICROTOKENS_PER_OP;
true
} else {
false
}
}
}
pub struct RateLimiter {
config: RateLimitConfig,
session_buckets: DashMap<u64, SessionRateState>,
global_bucket: parking_lot::Mutex<Option<GlobalRateState>>,
concurrent_ops: AtomicU64,
}
impl std::fmt::Debug for RateLimiter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RateLimiter")
.field("config", &self.config)
.field(
"concurrent_ops",
&self.concurrent_ops.load(Ordering::Relaxed),
)
.finish()
}
}
impl RateLimiter {
pub fn new(config: &RateLimitConfig) -> Self {
let global_bucket = if config.max_global_ops_per_sec > 0 {
Some(GlobalRateState::new(config.max_global_ops_per_sec))
} else {
None
};
Self {
config: config.clone(),
session_buckets: DashMap::new(),
global_bucket: parking_lot::Mutex::new(global_bucket),
concurrent_ops: AtomicU64::new(0),
}
}
pub fn disabled() -> Self {
Self::new(&RateLimitConfig::default())
}
pub fn check_rate(&self, session_handle: u64) -> Result<(), HsmError> {
if self.config.max_ops_per_session_per_sec > 0 {
if !self.session_buckets.contains_key(&session_handle)
&& self.session_buckets.len() >= MAX_SESSION_BUCKETS
{
return Err(HsmError::GeneralError);
}
let mut entry = self
.session_buckets
.entry(session_handle)
.or_insert_with(|| SessionRateState::new(self.config.max_ops_per_session_per_sec));
if !entry.value_mut().try_consume() {
return Err(HsmError::GeneralError);
}
}
if self.config.max_global_ops_per_sec > 0 {
let mut guard = self.global_bucket.lock();
if let Some(ref mut bucket) = *guard {
if !bucket.try_consume() {
return Err(HsmError::GeneralError);
}
}
}
Ok(())
}
pub fn begin_operation(&self) -> Result<(), HsmError> {
if self.config.max_concurrent_ops == 0 {
return Ok(());
}
let prev = self.concurrent_ops.fetch_add(1, Ordering::SeqCst);
if prev >= self.config.max_concurrent_ops {
self.concurrent_ops.fetch_sub(1, Ordering::SeqCst);
return Err(HsmError::GeneralError);
}
Ok(())
}
pub fn end_operation(&self) {
if self.config.max_concurrent_ops == 0 {
return;
}
self.concurrent_ops.fetch_sub(1, Ordering::SeqCst);
}
pub fn remove_session(&self, session_handle: u64) {
self.session_buckets.remove(&session_handle);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disabled_limiter_always_passes() {
let rl = RateLimiter::disabled();
for i in 0..1000 {
assert!(rl.check_rate(i).is_ok());
assert!(rl.begin_operation().is_ok());
rl.end_operation();
}
}
#[test]
fn per_session_rate_limit_rejects_after_burst() {
let config = RateLimitConfig {
max_ops_per_session_per_sec: 5,
max_global_ops_per_sec: 0,
max_concurrent_ops: 0,
};
let rl = RateLimiter::new(&config);
let handle = 42u64;
for _ in 0..5 {
assert!(rl.check_rate(handle).is_ok());
}
assert!(rl.check_rate(handle).is_err());
assert!(rl.check_rate(99).is_ok());
}
#[test]
fn concurrent_ops_limit() {
let config = RateLimitConfig {
max_ops_per_session_per_sec: 0,
max_global_ops_per_sec: 0,
max_concurrent_ops: 3,
};
let rl = RateLimiter::new(&config);
assert!(rl.begin_operation().is_ok());
assert!(rl.begin_operation().is_ok());
assert!(rl.begin_operation().is_ok());
assert!(rl.begin_operation().is_err());
rl.end_operation();
assert!(rl.begin_operation().is_ok());
}
#[test]
fn session_cleanup_removes_state() {
let config = RateLimitConfig {
max_ops_per_session_per_sec: 2,
max_global_ops_per_sec: 0,
max_concurrent_ops: 0,
};
let rl = RateLimiter::new(&config);
let handle = 7u64;
assert!(rl.check_rate(handle).is_ok());
assert!(rl.check_rate(handle).is_ok());
assert!(rl.check_rate(handle).is_err());
rl.remove_session(handle);
assert!(rl.check_rate(handle).is_ok());
}
#[test]
fn global_rate_limit_rejects_after_burst() {
let config = RateLimitConfig {
max_ops_per_session_per_sec: 0,
max_global_ops_per_sec: 4,
max_concurrent_ops: 0,
};
let rl = RateLimiter::new(&config);
for i in 0..4 {
assert!(rl.check_rate(i).is_ok());
}
assert!(rl.check_rate(100).is_err());
}
#[test]
fn integer_bucket_does_not_drift_over_many_cycles() {
let rate = 100u64;
let state_capacity = rate.saturating_mul(MICROTOKENS_PER_OP);
let mut tokens: u64 = state_capacity;
for _ in 0..10_000 {
let mut consumed = 0u64;
while tokens >= MICROTOKENS_PER_OP {
tokens -= MICROTOKENS_PER_OP;
consumed += 1;
}
assert_eq!(
consumed, rate,
"bucket should yield exactly `rate` tokens after a full refill"
);
assert_eq!(
tokens, 0,
"residual microtokens should be exactly 0 after drain"
);
tokens = tokens.saturating_add(state_capacity).min(state_capacity);
}
assert_eq!(
tokens, state_capacity,
"bucket should be at full capacity after final refill"
);
}
#[test]
fn session_bucket_count_is_bounded() {
let config = RateLimitConfig {
max_ops_per_session_per_sec: 10,
max_global_ops_per_sec: 0,
max_concurrent_ops: 0,
};
let rl = RateLimiter::new(&config);
for i in 0..(MAX_SESSION_BUCKETS as u64) {
assert!(
rl.check_rate(i).is_ok(),
"session {} should succeed while under limit",
i,
);
}
let overflow_handle = MAX_SESSION_BUCKETS as u64 + 1;
assert!(
rl.check_rate(overflow_handle).is_err(),
"new session beyond MAX_SESSION_BUCKETS must be rejected",
);
assert!(
rl.check_rate(0).is_ok(),
"existing session should still be allowed",
);
}
}