use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
use super::RouterError;
const DEFAULT_WAIT_TIMEOUT: Duration = Duration::from_secs(60);
#[derive(Debug, Clone)]
pub struct ModelRateLimit {
pub(crate) requests_per_window: usize,
pub(crate) window: Duration,
pub(crate) max_concurrent: usize,
pub(crate) max_queue: usize,
pub(crate) wait_timeout: Duration,
}
impl ModelRateLimit {
pub fn per_minute(requests_per_minute: usize) -> Self {
Self {
requests_per_window: requests_per_minute,
window: Duration::from_secs(60),
max_concurrent: 0,
max_queue: 0,
wait_timeout: DEFAULT_WAIT_TIMEOUT,
}
}
pub fn per_second(requests_per_second: usize) -> Self {
Self {
requests_per_window: requests_per_second,
window: Duration::from_secs(1),
max_concurrent: 0,
max_queue: 0,
wait_timeout: Duration::from_secs(1),
}
}
pub fn with_window(mut self, window: Duration) -> Self {
self.window = window;
self
}
pub fn with_max_concurrent(mut self, max_concurrent: usize) -> Self {
self.max_concurrent = max_concurrent;
self
}
pub fn with_max_queue(mut self, max_queue: usize) -> Self {
self.max_queue = max_queue;
self
}
pub fn with_wait_timeout(mut self, wait_timeout: Duration) -> Self {
self.wait_timeout = wait_timeout;
self
}
pub fn requests_per_window(&self) -> usize {
self.requests_per_window
}
pub fn window(&self) -> Duration {
self.window
}
pub fn max_concurrent(&self) -> usize {
self.max_concurrent
}
pub fn max_queue(&self) -> usize {
self.max_queue
}
pub fn wait_timeout(&self) -> Duration {
self.wait_timeout
}
}
pub(super) struct ModelGate {
rate: Option<RateGate>,
concurrency: Arc<Semaphore>,
wait_timeout: Duration,
}
impl ModelGate {
pub(super) fn new(config: &ModelRateLimit) -> Arc<Self> {
let concurrency = if config.max_concurrent == 0 {
Semaphore::MAX_PERMITS
} else {
config.max_concurrent
};
Arc::new(Self {
rate: if config.requests_per_window > 0 {
Some(RateGate::new(config))
} else {
None
},
concurrency: Arc::new(Semaphore::new(concurrency)),
wait_timeout: config.wait_timeout,
})
}
pub(super) async fn acquire(&self, model: &str) -> Result<GatePermit, RouterError> {
if let Some(rate) = &self.rate {
rate.acquire(model).await?;
}
let acquired =
tokio::time::timeout(self.wait_timeout, self.concurrency.clone().acquire_owned()).await;
let concurrency = match acquired {
Ok(Ok(permit)) => permit,
Ok(Err(_closed)) => panic!("router concurrency semaphore is never closed"),
Err(_elapsed) => {
return Err(RouterError::RateLimited {
model: model.to_string(),
reason: RateLimitReason::Timeout {
waited: self.wait_timeout,
},
});
}
};
Ok(GatePermit {
_concurrency: concurrency,
})
}
}
pub(super) struct GatePermit {
_concurrency: OwnedSemaphorePermit,
}
impl std::fmt::Debug for GatePermit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GatePermit").finish_non_exhaustive()
}
}
struct RateGate {
semaphore: Arc<Semaphore>,
window: Duration,
queued: AtomicUsize,
max_queue: usize,
wait_timeout: Duration,
}
impl RateGate {
fn new(config: &ModelRateLimit) -> Self {
Self {
semaphore: Arc::new(Semaphore::new(config.requests_per_window)),
window: config.window,
queued: AtomicUsize::new(0),
max_queue: config.max_queue,
wait_timeout: config.wait_timeout,
}
}
async fn acquire(&self, model: &str) -> Result<(), RouterError> {
let position = self.queued.fetch_add(1, Ordering::AcqRel);
if self.max_queue > 0 && position >= self.max_queue {
self.queued.fetch_sub(1, Ordering::AcqRel);
return Err(RouterError::RateLimited {
model: model.to_string(),
reason: RateLimitReason::QueueFull {
max_queue: self.max_queue,
},
});
}
let acquired =
tokio::time::timeout(self.wait_timeout, self.semaphore.clone().acquire_owned()).await;
self.queued.fetch_sub(1, Ordering::AcqRel);
let permit = match acquired {
Ok(Ok(permit)) => permit,
Ok(Err(_closed)) => panic!("router rate semaphore is never closed"),
Err(_elapsed) => {
return Err(RouterError::RateLimited {
model: model.to_string(),
reason: RateLimitReason::Timeout {
waited: self.wait_timeout,
},
});
}
};
let window = self.window;
tokio::spawn(async move {
tokio::time::sleep(window).await;
drop(permit);
});
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RateLimitReason {
QueueFull {
max_queue: usize,
},
Timeout {
waited: Duration,
},
}
impl std::fmt::Display for RateLimitReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RateLimitReason::QueueFull { max_queue } => {
write!(f, "admission queue full (max_queue={max_queue})")
}
RateLimitReason::Timeout { waited } => {
write!(f, "no permit within {} ms", waited.as_millis())
}
}
}
}
impl std::error::Error for RateLimitReason {}
#[cfg(test)]
mod tests {
use super::*;
fn gate(rl: &ModelRateLimit) -> Arc<ModelGate> {
ModelGate::new(rl)
}
#[tokio::test(start_paused = true)]
async fn admits_burst_then_queues_fifo_across_window() {
let rl = ModelRateLimit::per_second(1)
.with_window(Duration::from_millis(100))
.with_max_concurrent(1);
let g = gate(&rl);
let finished = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let mut handles = Vec::new();
for id in 0..3u8 {
let g = g.clone();
let finished = finished.clone();
handles.push(tokio::spawn(async move {
let _permit = g.acquire("m").await.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
finished.lock().await.push(id);
}));
}
for h in handles {
h.await.unwrap();
}
assert_eq!(*finished.lock().await, vec![0, 1, 2]);
}
#[tokio::test(start_paused = true)]
async fn full_queue_rejects_instead_of_waiting() {
let rl = ModelRateLimit::per_minute(1).with_max_queue(1);
let g = gate(&rl);
let _p0 = g.acquire("m").await.unwrap();
let g1 = g.clone();
let waiter = tokio::spawn(async move { g1.acquire("m").await });
tokio::task::yield_now().await;
tokio::task::yield_now().await;
let err = g.acquire("m").await.unwrap_err();
assert!(
matches!(
err,
RouterError::RateLimited {
reason: RateLimitReason::QueueFull { max_queue: 1 },
..
}
),
"got {err:?}"
);
tokio::time::sleep(Duration::from_secs(60)).await;
assert!(waiter.await.unwrap().is_ok());
}
#[tokio::test(start_paused = true)]
async fn wait_timeout_falls_through() {
let rl = ModelRateLimit::per_minute(1).with_wait_timeout(Duration::from_secs(5));
let g = gate(&rl);
let _p0 = g.acquire("m").await.unwrap();
let started = tokio::time::Instant::now();
let err = g.acquire("primary").await.unwrap_err();
assert_eq!(started.elapsed(), Duration::from_secs(5));
assert!(
matches!(
err,
RouterError::RateLimited {
reason: RateLimitReason::Timeout { .. },
..
}
),
"got {err:?}"
);
}
#[tokio::test(start_paused = true)]
async fn permit_returns_after_window_and_keeps_rate_stable() {
let rl = ModelRateLimit::per_second(2).with_window(Duration::from_millis(100));
let g = gate(&rl);
let _p1 = g.acquire("m").await.unwrap();
let _p2 = g.acquire("m").await.unwrap();
let g2 = g.clone();
let h = tokio::spawn(async move { g2.acquire("m").await });
tokio::time::sleep(Duration::from_millis(101)).await;
assert!(h.await.unwrap().is_ok());
}
#[test]
fn disabled_dimensions_are_represented_as_zero() {
let rl = ModelRateLimit::per_minute(10);
assert_eq!(rl.max_concurrent(), 0);
assert_eq!(rl.max_queue(), 0);
assert_eq!(rl.requests_per_window(), 10);
let rl2 = rl
.with_window(Duration::from_secs(30))
.with_wait_timeout(Duration::from_secs(30));
assert_eq!(rl2.window(), Duration::from_secs(30));
assert_eq!(rl2.wait_timeout(), Duration::from_secs(30));
}
}