use std::future::Future;
use std::pin::Pin;
#[cfg(feature = "postgres")]
use a2a_protocol_types::error::A2aError;
use a2a_protocol_types::error::A2aResult;
pub trait RateLimitCounter: Send + Sync + 'static {
fn count<'a>(
&'a self,
key: &'a str,
window: u64,
window_secs: u64,
) -> Pin<Box<dyn Future<Output = A2aResult<u64>> + Send + 'a>>;
}
#[cfg(feature = "postgres")]
mod postgres {
use super::{A2aError, A2aResult, Future, Pin, RateLimitCounter};
use std::sync::atomic::{AtomicU64, Ordering};
const CREATE_TABLE_SQL: &str = "CREATE TABLE IF NOT EXISTS a2a_rate_limit (
caller TEXT NOT NULL,
window_no BIGINT NOT NULL,
request_count BIGINT NOT NULL,
PRIMARY KEY (caller, window_no)
)";
const COUNT_SQL: &str = "INSERT INTO a2a_rate_limit (caller, window_no, request_count) \
VALUES ($1, $2, 1) \
ON CONFLICT (caller, window_no) \
DO UPDATE SET request_count = a2a_rate_limit.request_count + 1 \
RETURNING request_count";
const SWEEP_SQL: &str = "DELETE FROM a2a_rate_limit WHERE window_no < $1";
const SWEEP_INTERVAL: u64 = 1_000;
pub struct PostgresRateLimitCounter {
pool: sqlx::PgPool,
counted: AtomicU64,
}
impl std::fmt::Debug for PostgresRateLimitCounter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PostgresRateLimitCounter")
.finish_non_exhaustive()
}
}
impl PostgresRateLimitCounter {
pub async fn new(url: &str) -> A2aResult<Self> {
let pool = sqlx::postgres::PgPoolOptions::new()
.after_connect(|conn, _meta| {
Box::pin(async move {
sqlx::query("SET synchronous_commit = off")
.execute(&mut *conn)
.await
.map(|_| ())
})
})
.connect(url)
.await
.map_err(|e| A2aError::internal(format!("rate-limit counter connect: {e}")))?;
Self::from_pool(pool).await
}
pub async fn from_pool(pool: sqlx::PgPool) -> A2aResult<Self> {
sqlx::query(CREATE_TABLE_SQL)
.execute(&pool)
.await
.map_err(|e| A2aError::internal(format!("rate-limit counter migrate: {e}")))?;
Ok(Self {
pool,
counted: AtomicU64::new(0),
})
}
async fn sweep(&self, current_window: u64) {
let cutoff = i64::try_from(current_window).unwrap_or(i64::MAX);
if let Err(_e) = sqlx::query(SWEEP_SQL)
.bind(cutoff)
.execute(&self.pool)
.await
{
trace_warn!(error = %_e, "rate-limit counter sweep failed");
}
}
}
impl RateLimitCounter for PostgresRateLimitCounter {
fn count<'a>(
&'a self,
key: &'a str,
window: u64,
_window_secs: u64,
) -> Pin<Box<dyn Future<Output = A2aResult<u64>> + Send + 'a>> {
Box::pin(async move {
let window_no = i64::try_from(window)
.map_err(|_| A2aError::internal("rate-limit window out of range"))?;
let (count,): (i64,) = sqlx::query_as(COUNT_SQL)
.bind(key)
.bind(window_no)
.fetch_one(&self.pool)
.await
.map_err(|e| A2aError::internal(format!("rate-limit counter: {e}")))?;
let n = self.counted.fetch_add(1, Ordering::Relaxed);
if n > 0 && n.is_multiple_of(SWEEP_INTERVAL) {
self.sweep(window).await;
}
Ok(u64::try_from(count).unwrap_or(u64::MAX))
})
}
}
}
#[cfg(feature = "postgres")]
pub use postgres::PostgresRateLimitCounter;