use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use a2a_protocol_types::error::{A2aError, A2aResult};
use super::{CallerBucket, RateLimitInterceptor, CLEANUP_INTERVAL};
impl RateLimitInterceptor {
pub(super) const fn window_number(&self, now_secs: u64) -> u64 {
now_secs / self.config.window_secs
}
pub(super) fn evict_stale(buckets: &mut HashMap<String, CallerBucket>, current_window: u64) {
buckets.retain(|_, bucket| {
bucket.window_start.load(Ordering::Relaxed) >= current_window.saturating_sub(1)
});
}
pub(super) async fn cleanup_stale_buckets(&self) {
let now_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let current_window = self.window_number(now_secs);
let mut buckets = self.buckets.write().await;
Self::evict_stale(&mut buckets, current_window);
}
#[allow(clippy::too_many_lines)]
pub(super) fn admit_within_window(&self, bucket: &CallerBucket, limit: u64) -> A2aResult<()> {
let count = bucket.count.fetch_add(1, Ordering::Relaxed) + 1;
if count > limit {
return Err(A2aError::internal(format!(
"rate limit exceeded: {limit} requests per {} seconds",
self.config.window_secs
)));
}
Ok(())
}
pub(super) fn admit_or_roll_window(
&self,
bucket: &CallerBucket,
current_window: u64,
limit: u64,
) -> A2aResult<()> {
if bucket.window_start.load(Ordering::Acquire) == current_window {
return self.admit_within_window(bucket, limit);
}
bucket.window_start.store(current_window, Ordering::Release);
bucket.count.store(1, Ordering::Release);
Ok(())
}
pub(super) async fn create_or_join_bucket(
&self,
key: &str,
current_window: u64,
limit: u64,
) -> A2aResult<()> {
let mut buckets = self.buckets.write().await;
if let Some(bucket) = buckets.get(key) {
return self.admit_or_roll_window(bucket, current_window, limit);
}
if buckets.len() >= self.config.max_buckets {
Self::evict_stale(&mut buckets, current_window);
if buckets.len() >= self.config.max_buckets {
return Err(A2aError::internal(format!(
"rate limiter caller capacity exhausted ({} buckets); request rejected",
self.config.max_buckets
)));
}
}
buckets.insert(
key.to_string(),
CallerBucket {
window_start: AtomicU64::new(current_window),
count: AtomicU64::new(1),
},
);
drop(buckets);
Ok(())
}
pub(super) fn admit_shared_count(&self, count: u64, limit: u64) -> A2aResult<()> {
if count > limit {
return Err(A2aError::internal(format!(
"rate limit exceeded: {limit} requests per {} seconds",
self.config.window_secs
)));
}
Ok(())
}
pub(super) async fn check(&self, key: &str, limit: u64) -> A2aResult<()> {
let now_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let current_window = self.window_number(now_secs);
if let Some(counter) = &self.shared {
match counter
.count(key, current_window, self.config.window_secs)
.await
{
Ok(count) => return self.admit_shared_count(count, limit),
Err(_e) => {
trace_warn!(
error = %_e,
"shared rate-limit counter unavailable; counting in this process only"
);
}
}
}
let count = self.check_count.fetch_add(1, Ordering::Relaxed);
if count > 0 && count.is_multiple_of(CLEANUP_INTERVAL) {
self.cleanup_stale_buckets().await;
}
{
let buckets = self.buckets.read().await;
if let Some(bucket) = buckets.get(key) {
loop {
let bucket_window = bucket.window_start.load(Ordering::Acquire);
if bucket_window == current_window {
return self.admit_within_window(bucket, limit);
}
if bucket
.window_start
.compare_exchange(
bucket_window,
current_window,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_ok()
{
bucket.count.store(1, Ordering::Release);
return Ok(());
}
}
}
}
self.create_or_join_bucket(key, current_window, limit).await
}
}