use crate::utils::clock::Instant;
use anyhow::{Context, Result};
use std::collections::VecDeque;
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::task::{Context as TaskContext, Poll, Waker};
use std::time::Duration;
use tokio::sync::{Mutex, RwLock};
use tracing::{debug, info, warn};
pub const DEFAULT_QUEUE_TIMEOUT_SECS: u64 =
crate::llm::ollama_client::DEFAULT_REQUEST_TIMEOUT.as_secs();
pub const DEFAULT_MAX_QUEUED: usize = 128;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestSource {
User,
Network,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RateLimitError {
QueueTimeout {
waited_secs: u64,
},
QueueFull {
max_queued: usize,
},
TokenLimit {
limit: u64,
window_secs: u64,
},
}
impl std::fmt::Display for RateLimitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::QueueTimeout { waited_secs } => write!(
f,
"LLM backend overloaded: waited {}s for a concurrency permit without one becoming available",
waited_secs
),
Self::QueueFull { max_queued } => write!(
f,
"LLM backend overloaded: {} requests already queued for a concurrency permit",
max_queued
),
Self::TokenLimit { limit, window_secs } => write!(
f,
"LLM token budget exhausted: {} tokens per {}s window",
limit, window_secs
),
}
}
}
impl std::error::Error for RateLimitError {}
pub fn is_overload_error(err: &anyhow::Error) -> bool {
err.downcast_ref::<RateLimitError>().is_some()
}
#[derive(Debug, Clone)]
struct TokenUsage {
timestamp: Instant,
input_tokens: u64,
output_tokens: u64,
}
#[derive(Debug, Clone)]
pub struct RateLimiterConfig {
pub max_concurrent: usize,
pub token_limit: Option<u64>,
pub token_window_secs: u64,
pub queue_timeout_secs: u64,
pub max_queued: usize,
}
impl Default for RateLimiterConfig {
fn default() -> Self {
Self {
max_concurrent: 1,
token_limit: None,
token_window_secs: 60,
queue_timeout_secs: DEFAULT_QUEUE_TIMEOUT_SECS,
max_queued: DEFAULT_MAX_QUEUED,
}
}
}
#[derive(Clone)]
pub struct RateLimiter {
semaphore: Arc<ConcurrencyGate>,
config: Arc<RwLock<RateLimiterConfig>>,
token_usage: Arc<Mutex<Vec<TokenUsage>>>,
queued_network: Arc<AtomicUsize>,
waiting_users: Arc<AtomicUsize>,
stats: Arc<Mutex<RateLimiterStats>>,
}
struct QueueSlot(Arc<AtomicUsize>);
impl Drop for QueueSlot {
fn drop(&mut self) {
self.0.fetch_sub(1, Ordering::AcqRel);
}
}
#[derive(Debug, Default, Clone)]
pub struct RateLimiterStats {
pub total_requests: u64,
pub requests_completed: u64,
pub requests_discarded: u64,
pub requests_waiting: u64,
pub requests_queued: u64,
pub requests_queue_timed_out: u64,
pub currently_queued: u64,
pub total_input_tokens: u64,
pub total_output_tokens: u64,
pub current_window_tokens: u64,
}
impl RateLimiter {
pub fn new(config: RateLimiterConfig) -> Self {
let max_concurrent = config.max_concurrent;
Self {
semaphore: Arc::new(ConcurrencyGate::new(max_concurrent)),
config: Arc::new(RwLock::new(config)),
token_usage: Arc::new(Mutex::new(Vec::new())),
queued_network: Arc::new(AtomicUsize::new(0)),
waiting_users: Arc::new(AtomicUsize::new(0)),
stats: Arc::new(Mutex::new(RateLimiterStats::default())),
}
}
pub async fn update_config(&self, config: RateLimiterConfig) -> Result<()> {
let mut current_config = self.config.write().await;
self.semaphore.resize(config.max_concurrent);
*current_config = config;
info!(
"Rate limiter config updated: max_concurrent={}, token_limit={:?}, window={}s, queue_timeout={}s, max_queued={}",
current_config.max_concurrent,
current_config.token_limit,
current_config.token_window_secs,
current_config.queue_timeout_secs,
current_config.max_queued
);
Ok(())
}
pub async fn get_config(&self) -> RateLimiterConfig {
self.config.read().await.clone()
}
pub async fn get_stats(&self) -> RateLimiterStats {
let window = Duration::from_secs(self.config.read().await.token_window_secs);
let token_usage = self.token_usage.lock().await;
let window_tokens: u64 = token_usage
.iter()
.filter(|usage| usage.timestamp.elapsed() <= window)
.fold(0u64, |total, usage| {
total.saturating_add(usage.input_tokens.saturating_add(usage.output_tokens))
});
drop(token_usage);
let mut stats = self.stats.lock().await;
stats.current_window_tokens = window_tokens;
stats.currently_queued = self.queued_network.load(Ordering::Acquire) as u64;
stats.requests_waiting = self.waiting_users.load(Ordering::Acquire) as u64;
stats.clone()
}
async fn prune_token_usage(&self, window_secs: u64) {
let window = Duration::from_secs(window_secs);
self.token_usage
.lock()
.await
.retain(|usage| usage.timestamp.elapsed() <= window);
}
async fn check_token_capacity(&self) -> Result<bool> {
let config = self.config.read().await.clone();
let window_secs = config.token_window_secs;
self.prune_token_usage(window_secs).await;
let Some(token_limit) = config.token_limit else {
return Ok(true);
};
let token_usage = self.token_usage.lock().await;
let window_tokens: u64 = token_usage.iter().fold(0u64, |total, usage| {
total.saturating_add(usage.input_tokens.saturating_add(usage.output_tokens))
});
debug!(
"Token usage check: {}/{} tokens in {}s window",
window_tokens, token_limit, config.token_window_secs
);
Ok(window_tokens < token_limit)
}
pub async fn record_token_usage(&self, input_tokens: u64, output_tokens: u64) {
let usage = TokenUsage {
timestamp: Instant::now(),
input_tokens,
output_tokens,
};
let window_secs = self.config.read().await.token_window_secs;
self.token_usage.lock().await.push(usage);
self.prune_token_usage(window_secs).await;
let mut stats = self.stats.lock().await;
stats.total_input_tokens = stats.total_input_tokens.saturating_add(input_tokens);
stats.total_output_tokens = stats.total_output_tokens.saturating_add(output_tokens);
stats.requests_completed += 1;
debug!(
"Recorded token usage: input={}, output={}, total_requests={}",
input_tokens, output_tokens, stats.requests_completed
);
}
pub async fn token_history_len(&self) -> usize {
self.token_usage.lock().await.len()
}
async fn wait_for_token_capacity(&self, source: RequestSource) -> Result<()> {
let has_token_capacity = self.check_token_capacity().await?;
if !has_token_capacity {
match source {
RequestSource::User => {
info!("Token limit reached, waiting for capacity (user request)");
let mut delay = Duration::from_millis(100);
while !self.check_token_capacity().await? {
tokio::time::sleep(delay).await;
delay = std::cmp::min(delay * 2, Duration::from_secs(5));
}
info!("Token capacity available, proceeding with user request");
}
RequestSource::Network => {
self.stats.lock().await.requests_discarded += 1;
let config = self.config.read().await.clone();
warn!(
"Discarding network event: Token limit ({}/{} tokens in {}s window)",
self.token_usage
.lock()
.await
.iter()
.fold(0u64, |total, usage| {
total.saturating_add(
usage.input_tokens.saturating_add(usage.output_tokens),
)
}),
config.token_limit.unwrap_or(0),
config.token_window_secs
);
return Err(anyhow::Error::new(RateLimitError::TokenLimit {
limit: config.token_limit.unwrap_or(0),
window_secs: config.token_window_secs,
}));
}
}
}
Ok(())
}
pub async fn acquire_permit(&self, source: RequestSource) -> Result<RateLimiterPermit> {
{
let mut stats = self.stats.lock().await;
stats.total_requests += 1;
}
let _user_wait = (source == RequestSource::User).then(|| {
self.waiting_users.fetch_add(1, Ordering::AcqRel);
QueueSlot(Arc::clone(&self.waiting_users))
});
debug!("Acquiring rate limiter permit for {:?} request", source);
self.wait_for_token_capacity(source).await?;
let semaphore = Arc::clone(&self.semaphore);
let (queue_timeout_secs, max_queued, max_concurrent) = {
let config = self.config.read().await;
(
config.queue_timeout_secs,
config.max_queued,
config.max_concurrent,
)
};
let permit = match source {
RequestSource::User => {
debug!("Waiting for concurrency permit (user request)");
semaphore
.acquire_owned()
.await
.context("Failed to acquire semaphore permit")?
}
RequestSource::Network => {
match Arc::clone(&semaphore).try_acquire_owned() {
Ok(permit) => permit,
Err(_) => {
let depth = self.queued_network.fetch_add(1, Ordering::AcqRel) + 1;
let _slot = QueueSlot(Arc::clone(&self.queued_network));
if max_queued > 0 && depth > max_queued {
let mut stats = self.stats.lock().await;
stats.requests_discarded += 1;
warn!(
"Refusing network event: {} requests already waiting for one of {} concurrency permits",
max_queued, max_concurrent
);
return Err(anyhow::Error::new(RateLimitError::QueueFull {
max_queued,
}));
}
{
let mut stats = self.stats.lock().await;
stats.requests_queued += 1;
}
debug!(
"Network event queued for a concurrency permit ({} waiting, max_concurrent={})",
depth, max_concurrent
);
let acquire = semaphore.acquire_owned();
let acquired = if queue_timeout_secs == 0 {
acquire.await.map(Some).map_err(anyhow::Error::from)
} else {
match tokio::time::timeout(
Duration::from_secs(queue_timeout_secs),
acquire,
)
.await
{
Ok(result) => result.map(Some).map_err(anyhow::Error::from),
Err(_elapsed) => Ok(None),
}
};
match acquired.context("Failed to acquire semaphore permit")? {
Some(permit) => permit,
None => {
let mut stats = self.stats.lock().await;
stats.requests_queue_timed_out += 1;
warn!(
"Refusing network event: waited {}s for one of {} concurrency permits",
queue_timeout_secs, max_concurrent
);
return Err(anyhow::Error::new(RateLimitError::QueueTimeout {
waited_secs: queue_timeout_secs,
}));
}
}
}
}
}
};
self.wait_for_token_capacity(source).await?;
debug!("Rate limiter permit acquired for {:?} request", source);
Ok(RateLimiterPermit {
_permit: permit,
rate_limiter: self.clone(),
})
}
}
pub struct RateLimiterPermit {
_permit: ConcurrencyPermit,
rate_limiter: RateLimiter,
}
impl RateLimiterPermit {
pub async fn record_usage(&self, input_tokens: u64, output_tokens: u64) {
self.rate_limiter
.record_token_usage(input_tokens, output_tokens)
.await;
}
}
struct ConcurrencyGate {
state: std::sync::Mutex<ConcurrencyState>,
}
struct ConcurrencyState {
limit: usize,
active: usize,
next_id: u64,
queue: VecDeque<(u64, Waker)>,
}
impl ConcurrencyGate {
fn new(limit: usize) -> Self {
Self {
state: std::sync::Mutex::new(ConcurrencyState {
limit,
active: 0,
next_id: 0,
queue: VecDeque::new(),
}),
}
}
fn lock(&self) -> std::sync::MutexGuard<'_, ConcurrencyState> {
self.state.lock().unwrap_or_else(|e| e.into_inner())
}
fn resize(&self, limit: usize) {
let wake = {
let mut state = self.lock();
state.limit = limit;
state.queue.front().map(|(_, w)| w.clone())
};
if let Some(w) = wake {
w.wake();
}
}
fn try_acquire_owned(self: Arc<Self>) -> Result<ConcurrencyPermit, ()> {
{
let mut state = self.lock();
if state.active >= state.limit || !state.queue.is_empty() {
return Err(());
}
state.active += 1;
}
Ok(ConcurrencyPermit(self))
}
fn acquire_owned(self: Arc<Self>) -> ConcurrencyWait {
ConcurrencyWait {
gate: self,
id: None,
}
}
}
struct ConcurrencyWait {
gate: Arc<ConcurrencyGate>,
id: Option<u64>,
}
impl Future for ConcurrencyWait {
type Output = Result<ConcurrencyPermit>;
fn poll(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
let gate = Arc::clone(&this.gate);
let mut state = gate.lock();
let first = this.id.map_or(state.queue.is_empty(), |id| {
state.queue.front().is_some_and(|(front, _)| *front == id)
});
if first && state.active < state.limit {
if this.id.take().is_some() {
state.queue.pop_front();
}
state.active += 1;
let wake = if state.active < state.limit {
state.queue.front().map(|(_, w)| w.clone())
} else {
None
};
drop(state);
if let Some(w) = wake {
w.wake();
}
return Poll::Ready(Ok(ConcurrencyPermit(gate)));
}
if let Some(id) = this.id {
if let Some((_, w)) = state.queue.iter_mut().find(|(entry, _)| *entry == id) {
w.clone_from(cx.waker());
}
} else {
let id = state.next_id;
state.next_id = state.next_id.wrapping_add(1);
state.queue.push_back((id, cx.waker().clone()));
this.id = Some(id);
}
Poll::Pending
}
}
impl Drop for ConcurrencyWait {
fn drop(&mut self) {
if let Some(id) = self.id {
let wake = {
let mut state = self.gate.lock();
state.queue.retain(|(entry, _)| *entry != id);
state.queue.front().map(|(_, w)| w.clone())
};
if let Some(w) = wake {
w.wake();
}
}
}
}
struct ConcurrencyPermit(Arc<ConcurrencyGate>);
impl Drop for ConcurrencyPermit {
fn drop(&mut self) {
let wake = {
let mut state = self.0.lock();
state.active -= 1;
state.queue.front().map(|(_, w)| w.clone())
};
if let Some(w) = wake {
w.wake();
}
}
}