use std::{
collections::VecDeque,
sync::atomic::{AtomicU64, Ordering},
time::{Duration, Instant},
};
use dashmap::{DashMap, mapref::entry::Entry};
use crate::{
ConditionalSetOutcome, HistoryPreservation, RateLimitComparator,
builder::ProviderConfig,
common::{
Bucket, BucketSize, HistoryUpdateMode, RandomState, RateLimit, RateLimitDecision,
WindowSize, duration_from_milliseconds,
},
};
#[derive(Debug)]
pub(crate) struct RateLimitSeries {
pub window_limit: f64,
pub buckets: VecDeque<Bucket>,
pub total_count: AtomicU64,
}
impl RateLimitSeries {
pub fn new(window_limit: f64) -> Self {
Self {
window_limit,
buckets: VecDeque::new(),
total_count: AtomicU64::new(0),
}
}
}
#[derive(Debug)]
pub struct AbsoluteLocalRateLimiter {
window_size: WindowSize,
window_size_ms: u128,
window_duration: Duration,
bucket_size: BucketSize,
series: DashMap<String, RateLimitSeries, RandomState>,
}
impl AbsoluteLocalRateLimiter {
pub(crate) fn new(options: ProviderConfig) -> Self {
Self {
window_size_ms: options.window_size.as_milliseconds(),
window_duration: Duration::from_secs(options.window_size.as_seconds()),
window_size: options.window_size,
bucket_size: options.bucket_size,
series: DashMap::default(),
}
}
#[cfg(test)]
pub(crate) fn series(&self) -> &DashMap<String, RateLimitSeries, RandomState> {
&self.series
}
pub fn inc(&self, key: &str, rate_limit: &RateLimit, count: u64) -> RateLimitDecision {
let decision = self.is_allowed(key);
if !matches!(decision, RateLimitDecision::Allowed) {
return decision;
}
let series = match self.series.get(key) {
Some(series) => series,
None => self
.series
.entry(key.to_string())
.or_insert_with(|| {
RateLimitSeries::new(
rate_limit.as_per_second() * self.window_size.as_seconds() as f64,
)
})
.downgrade(),
};
if let Some(latest_bucket) = series.buckets.back()
&& latest_bucket.timestamp.elapsed().as_millis()
<= self.bucket_size.as_milliseconds() as u128
{
latest_bucket.count.fetch_add(count, Ordering::AcqRel);
series.total_count.fetch_add(count, Ordering::AcqRel);
} else {
drop(series);
let mut series = self.series.entry(key.to_string()).or_insert_with(|| {
RateLimitSeries::new(
rate_limit.as_per_second() * self.window_size.as_seconds() as f64,
)
});
series.buckets.push_back(Bucket {
count: count.into(),
timestamp: Instant::now(),
declined_count: AtomicU64::new(0),
});
series.total_count.fetch_add(count, Ordering::AcqRel);
}
RateLimitDecision::Allowed
}
pub fn is_allowed(&self, key: &str) -> RateLimitDecision {
let Some(series) = self.series.get(key) else {
return RateLimitDecision::Allowed;
};
let total_count = series.total_count.load(Ordering::Acquire);
if total_count < series.window_limit as u64 {
return RateLimitDecision::Allowed;
}
let (retry_after_ms, remaining_after_waiting) = match series.buckets.front() {
None => (0, 0),
Some(oldest_bucket)
if oldest_bucket.timestamp.elapsed().as_millis() <= self.window_size_ms =>
{
let elapsed_ms = oldest_bucket.timestamp.elapsed().as_millis();
(
self.window_size_ms.saturating_sub(elapsed_ms),
oldest_bucket.count.load(Ordering::Acquire),
)
}
Some(_) => {
drop(series);
let Some(mut series) = self.series.get_mut(key) else {
return RateLimitDecision::Allowed;
};
let total_count = Self::evict_expired(&mut series, self.window_duration);
if total_count < series.window_limit as u64 {
return RateLimitDecision::Allowed;
}
let (elapsed_ms, count) = series
.buckets
.front()
.map(|bucket| {
(
self.window_size_ms
.saturating_sub(bucket.timestamp.elapsed().as_millis()),
bucket.count.load(Ordering::Acquire),
)
})
.unwrap_or((0, 0));
(elapsed_ms, count)
}
};
RateLimitDecision::Rejected {
window_size: self.window_size,
retry_after: duration_from_milliseconds(retry_after_ms),
remaining_after_waiting,
}
}
fn evict_expired(series: &mut RateLimitSeries, window_duration: Duration) -> u64 {
let now = Instant::now();
let split = series
.buckets
.partition_point(|bucket| now.duration_since(bucket.timestamp) > window_duration);
if split == 0 {
return series.total_count.load(Ordering::Acquire);
}
let drained = series
.buckets
.drain(..split)
.map(|bucket| bucket.count.load(Ordering::Acquire))
.sum::<u64>();
let prev = series.total_count.fetch_sub(drained, Ordering::AcqRel);
prev - drained
}
fn inspect_live_total(series: &RateLimitSeries, window_duration: Duration) -> (u64, bool) {
let now = Instant::now();
let split = series
.buckets
.partition_point(|bucket| now.duration_since(bucket.timestamp) > window_duration);
if split == 0 {
return (series.total_count.load(Ordering::Acquire), false);
}
let total_count = series
.buckets
.iter()
.skip(split)
.map(|bucket| bucket.count.load(Ordering::Acquire))
.sum();
(total_count, true)
}
fn live_total(&self, key: &str) -> u64 {
let Some(series) = self.series.get(key) else {
return 0;
};
let (total, contains_expired) = Self::inspect_live_total(&series, self.window_duration);
if !contains_expired {
return total;
}
drop(series);
let Some(mut series) = self.series.get_mut(key) else {
return 0;
};
Self::evict_expired(&mut series, self.window_duration)
}
fn set_if_with_history_mode(
&self,
key: &str,
rate_limit: &RateLimit,
comparator: RateLimitComparator,
count: u64,
mode: HistoryUpdateMode,
) -> (u64, u64) {
let existing = match self.series.get(key) {
Some(series) => {
let (old_total, contains_expired) =
Self::inspect_live_total(&series, self.window_duration);
if !comparator.matches(old_total) {
return (old_total, old_total);
}
let window_limit =
rate_limit.as_per_second() * self.window_size.as_seconds() as f64;
let unchanged = count > 0
&& matches!(mode, HistoryUpdateMode::Preserve(_))
&& !contains_expired
&& old_total == count
&& series.window_limit == window_limit;
if unchanged {
return (old_total, old_total);
}
true
}
None => false,
};
if !existing && (!comparator.matches(0) || count == 0) {
return (0, 0);
}
match self.series.entry(key.to_string()) {
Entry::Occupied(mut entry) => {
let (old_total, contains_expired) =
Self::inspect_live_total(entry.get(), self.window_duration);
if !comparator.matches(old_total) {
return (old_total, old_total);
}
if count == 0 {
entry.remove();
return (0, old_total);
}
let series = entry.get_mut();
if contains_expired {
Self::evict_expired(series, self.window_duration);
}
series.window_limit =
rate_limit.as_per_second() * self.window_size.as_seconds() as f64;
if old_total == count && matches!(mode, HistoryUpdateMode::Preserve(_)) {
return (old_total, old_total);
}
Self::apply_history_update(series, count, old_total, mode);
(count, old_total)
}
Entry::Vacant(entry) => {
if !comparator.matches(0) || count == 0 {
return (0, 0);
}
let mut series = RateLimitSeries::new(
rate_limit.as_per_second() * self.window_size.as_seconds() as f64,
);
Self::apply_history_update(&mut series, count, 0, mode);
entry.insert(series);
(count, 0)
}
}
}
fn apply_history_update(
series: &mut RateLimitSeries,
count: u64,
old_total: u64,
mode: HistoryUpdateMode,
) {
match mode {
HistoryUpdateMode::Replace => {
series.buckets.clear();
series.buckets.push_back(Bucket {
count: count.into(),
timestamp: Instant::now(),
declined_count: AtomicU64::new(0),
});
}
HistoryUpdateMode::Preserve(preservation) if count > old_total => {
let delta = count - old_total;
let bucket = match preservation {
HistoryPreservation::PreserveNewest => series.buckets.back(),
HistoryPreservation::PreserveOldest => series.buckets.front(),
};
if let Some(bucket) = bucket {
bucket.count.fetch_add(delta, Ordering::AcqRel);
} else {
series.buckets.push_back(Bucket {
count: delta.into(),
timestamp: Instant::now(),
declined_count: AtomicU64::new(0),
});
}
}
HistoryUpdateMode::Preserve(preservation) if count < old_total => {
let mut to_remove = old_total - count;
let mut bucket;
let mut bucket_count;
while to_remove > 0 {
bucket = match preservation {
HistoryPreservation::PreserveNewest => series.buckets.front(),
HistoryPreservation::PreserveOldest => series.buckets.back(),
};
let Some(bucket) = bucket else {
break;
};
bucket_count = bucket.count.load(Ordering::Acquire);
if bucket_count <= to_remove {
to_remove -= bucket_count;
match preservation {
HistoryPreservation::PreserveNewest => {
series.buckets.pop_front();
}
HistoryPreservation::PreserveOldest => {
series.buckets.pop_back();
}
}
} else {
bucket.count.fetch_sub(to_remove, Ordering::AcqRel);
to_remove = 0;
}
}
}
HistoryUpdateMode::Preserve(_) => {}
}
series.total_count.store(count, Ordering::Release);
}
pub fn get(&self, key: &str) -> u64 {
self.live_total(key)
}
pub fn set_rate_limit(&self, key: &str, rate_limit: &RateLimit) -> Option<RateLimit> {
let series = self.series.get(key)?;
let previous_rate_limit =
RateLimit::from_stored_window_limit(series.window_limit, self.window_size, 1.0)
.expect("locally stored window limit must represent a valid rate limit");
let window_limit = rate_limit.as_per_second() * self.window_size.as_seconds() as f64;
if series.window_limit == window_limit {
return Some(previous_rate_limit);
}
drop(series);
let mut series = self.series.get_mut(key)?;
let previous_rate_limit =
RateLimit::from_stored_window_limit(series.window_limit, self.window_size, 1.0)
.expect("locally stored window limit must represent a valid rate limit");
if series.window_limit != window_limit {
series.window_limit = window_limit;
}
Some(previous_rate_limit)
}
pub fn delete(&self, key: &str) -> Option<u64> {
let (_, mut series) = self.series.remove(key)?;
Some(Self::evict_expired(&mut series, self.window_duration))
}
pub fn clear(&self) {
self.series.clear();
}
pub fn set_if(
&self,
key: &str,
rate_limit: &RateLimit,
comparator: RateLimitComparator,
count: u64,
) -> ConditionalSetOutcome {
let (current_total, previous_total) = self.set_if_with_history_mode(
key,
rate_limit,
comparator,
count,
HistoryUpdateMode::Replace,
);
ConditionalSetOutcome {
matched: comparator.matches(previous_total),
previous_total,
current_total,
}
}
pub fn set_if_preserve_history(
&self,
key: &str,
rate_limit: &RateLimit,
comparator: RateLimitComparator,
count: u64,
preservation: HistoryPreservation,
) -> ConditionalSetOutcome {
let (current_total, previous_total) = self.set_if_with_history_mode(
key,
rate_limit,
comparator,
count,
HistoryUpdateMode::Preserve(preservation),
);
ConditionalSetOutcome {
matched: comparator.matches(previous_total),
previous_total,
current_total,
}
}
pub(crate) fn cleanup(&self, stale_after_ms: u64) {
self.series.retain(|_, series| match series.buckets.back() {
None => false,
Some(latest_bucket)
if latest_bucket.timestamp.elapsed().as_millis() > stale_after_ms as u128 =>
{
false
}
Some(_) => true,
});
} }