use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
use crate::config::AdmissionConfig;
use crate::telemetry::metrics;
pub const RESOURCE_REQUEST: &str = "request";
pub const RESOURCE_STREAM: &str = "stream";
pub const RESOURCE_TENANT: &str = "tenant";
pub const RESOURCE_QUEUE: &str = "queue";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestKind {
Buffered,
Streamed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum AdmissionRejection {
#[error("tenant concurrency limit exceeded")]
Tenant,
#[error("admission tenant capacity exhausted")]
TenantCapacity,
#[error("concurrent stream limit exceeded")]
Streams,
#[error("gateway is at its concurrent request limit")]
Global,
#[error("admission queue is full")]
QueueFull,
#[error("admission queue wait expired")]
QueueTimeout,
}
impl AdmissionRejection {
pub fn code(self) -> &'static str {
match self {
Self::Tenant => "tenant_concurrency_exceeded",
Self::TenantCapacity => "admission_tenant_capacity_exhausted",
Self::Streams => "stream_capacity_exhausted",
Self::Global => "gateway_overloaded",
Self::QueueFull => "admission_queue_full",
Self::QueueTimeout => "admission_queue_timeout",
}
}
pub fn is_caller_limit(self) -> bool {
matches!(self, Self::Tenant)
}
pub fn retry_after_seconds(self) -> Option<u64> {
match self {
Self::Tenant | Self::Streams | Self::Global | Self::QueueFull | Self::QueueTimeout => {
Some(1)
}
Self::TenantCapacity => None,
}
}
fn scope(self) -> &'static str {
match self {
Self::Tenant | Self::TenantCapacity => RESOURCE_TENANT,
Self::Streams => RESOURCE_STREAM,
Self::Global => RESOURCE_REQUEST,
Self::QueueFull | Self::QueueTimeout => RESOURCE_QUEUE,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AdmissionLimits {
pub max_request_bytes: usize,
pub max_in_flight: Option<usize>,
pub max_in_flight_streams: Option<usize>,
pub max_in_flight_per_tenant: Option<usize>,
pub max_tenants: usize,
pub queue_capacity: Option<usize>,
pub queue_wait: Duration,
pub max_stream_duration: Option<Duration>,
pub max_prompt_tokens: Option<u64>,
pub max_output_tokens: Option<u64>,
pub max_stream_bytes: Option<u64>,
}
impl From<&AdmissionConfig> for AdmissionLimits {
fn from(config: &AdmissionConfig) -> Self {
let bound = |value: usize| (value > 0).then_some(value);
let bound64 = |value: u64| (value > 0).then_some(value);
Self {
max_request_bytes: config.max_request_bytes,
max_in_flight: bound(config.max_in_flight),
max_in_flight_streams: bound(config.max_in_flight_streams),
max_in_flight_per_tenant: bound(config.max_in_flight_per_tenant),
max_tenants: config.max_tenants,
queue_capacity: bound(config.queue_capacity),
queue_wait: Duration::from_millis(config.queue_wait_ms),
max_stream_duration: (config.max_stream_duration_ms > 0)
.then(|| Duration::from_millis(config.max_stream_duration_ms)),
max_prompt_tokens: bound64(config.max_prompt_tokens),
max_output_tokens: bound64(config.max_output_tokens),
max_stream_bytes: bound64(config.max_stream_bytes),
}
}
}
pub struct AdmissionControl {
limits: AdmissionLimits,
global: Option<Arc<Semaphore>>,
streams: Option<Arc<Semaphore>>,
queue: Option<Arc<Semaphore>>,
tenants: Arc<TenantTable>,
}
impl AdmissionControl {
pub fn new(limits: AdmissionLimits) -> Self {
Self {
global: limits.max_in_flight.map(|n| Arc::new(Semaphore::new(n))),
streams: limits
.max_in_flight_streams
.map(|n| Arc::new(Semaphore::new(n))),
queue: limits.queue_capacity.map(|n| Arc::new(Semaphore::new(n))),
tenants: Arc::new(TenantTable {
limit: limits.max_in_flight_per_tenant,
max_tenants: limits.max_tenants,
active: Mutex::new(HashMap::new()),
}),
limits,
}
}
pub fn from_config(config: &AdmissionConfig) -> Self {
Self::new(AdmissionLimits::from(config))
}
pub fn limits(&self) -> AdmissionLimits {
self.limits
}
pub async fn admit(
&self,
tenant: &str,
kind: RequestKind,
) -> Result<AdmissionPermit, AdmissionRejection> {
let mut permit = AdmissionPermit {
global: None,
stream: None,
tenant: self.tenants.reserve(tenant).map_err(reject)?,
};
if let (RequestKind::Streamed, Some(streams)) = (kind, &self.streams) {
permit.stream = Some(
Arc::clone(streams)
.try_acquire_owned()
.map_err(|_| reject(AdmissionRejection::Streams))?,
);
metrics::record_admission_acquired(RESOURCE_STREAM);
}
if let Some(global) = &self.global {
permit.global = Some(self.acquire_global(global).await?);
metrics::record_admission_acquired(RESOURCE_REQUEST);
}
Ok(permit)
}
async fn acquire_global(
&self,
global: &Arc<Semaphore>,
) -> Result<OwnedSemaphorePermit, AdmissionRejection> {
if let Ok(permit) = Arc::clone(global).try_acquire_owned() {
return Ok(permit);
}
let Some(queue) = &self.queue else {
return Err(reject(AdmissionRejection::Global));
};
let Ok(slot) = Arc::clone(queue).try_acquire_owned() else {
return Err(reject(AdmissionRejection::QueueFull));
};
let _queued = QueuedRequest { _slot: slot };
metrics::record_admission_acquired(RESOURCE_QUEUE);
match tokio::time::timeout(self.limits.queue_wait, Arc::clone(global).acquire_owned()).await
{
Ok(Ok(permit)) => Ok(permit),
Ok(Err(_)) => Err(reject(AdmissionRejection::Global)),
Err(_) => Err(reject(AdmissionRejection::QueueTimeout)),
}
}
}
fn reject(rejection: AdmissionRejection) -> AdmissionRejection {
metrics::record_admission_rejection(rejection.scope(), rejection.code());
rejection
}
struct QueuedRequest {
_slot: OwnedSemaphorePermit,
}
impl Drop for QueuedRequest {
fn drop(&mut self) {
metrics::record_admission_released(RESOURCE_QUEUE);
}
}
pub struct AdmissionPermit {
global: Option<OwnedSemaphorePermit>,
stream: Option<OwnedSemaphorePermit>,
tenant: Option<TenantSlot>,
}
impl Drop for AdmissionPermit {
fn drop(&mut self) {
if self.global.take().is_some() {
metrics::record_admission_released(RESOURCE_REQUEST);
}
if self.stream.take().is_some() {
metrics::record_admission_released(RESOURCE_STREAM);
}
drop(self.tenant.take());
}
}
struct TenantTable {
limit: Option<usize>,
max_tenants: usize,
active: Mutex<HashMap<String, usize>>,
}
impl TenantTable {
fn reserve(self: &Arc<Self>, tenant: &str) -> Result<Option<TenantSlot>, AdmissionRejection> {
let Some(limit) = self.limit else {
return Ok(None);
};
let mut active = self
.active
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let in_flight = match active.get_mut(tenant) {
Some(in_flight) => in_flight,
None => {
if active.len() >= self.max_tenants {
return Err(AdmissionRejection::TenantCapacity);
}
active.entry(tenant.to_owned()).or_insert(0)
}
};
if *in_flight >= limit {
if *in_flight == 0 {
active.remove(tenant);
}
return Err(AdmissionRejection::Tenant);
}
*in_flight += 1;
drop(active);
metrics::record_admission_acquired(RESOURCE_TENANT);
Ok(Some(TenantSlot {
table: Arc::clone(self),
tenant: tenant.to_owned(),
}))
}
fn release(&self, tenant: &str) {
let mut active = self
.active
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(in_flight) = active.get_mut(tenant) {
*in_flight = in_flight.saturating_sub(1);
if *in_flight == 0 {
active.remove(tenant);
}
}
}
}
struct TenantSlot {
table: Arc<TenantTable>,
tenant: String,
}
impl Drop for TenantSlot {
fn drop(&mut self) {
self.table.release(&self.tenant);
metrics::record_admission_released(RESOURCE_TENANT);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn shed(result: Result<AdmissionPermit, AdmissionRejection>) -> AdmissionRejection {
result.err().expect("the request is shed")
}
fn limits() -> AdmissionLimits {
AdmissionLimits {
max_request_bytes: 1024,
max_in_flight: None,
max_in_flight_streams: None,
max_in_flight_per_tenant: None,
max_tenants: 8,
queue_capacity: None,
queue_wait: Duration::ZERO,
max_stream_duration: None,
max_prompt_tokens: None,
max_output_tokens: None,
max_stream_bytes: None,
}
}
#[tokio::test]
async fn unbounded_admission_always_admits() {
let control = AdmissionControl::new(limits());
for _ in 0..64 {
control
.admit("tenant", RequestKind::Streamed)
.await
.expect("admit");
}
}
#[tokio::test]
async fn global_saturation_sheds_with_a_typed_rejection_and_recovers() {
let control = AdmissionControl::new(AdmissionLimits {
max_in_flight: Some(1),
..limits()
});
let held = control
.admit("tenant", RequestKind::Buffered)
.await
.expect("first request is admitted");
assert_eq!(
shed(control.admit("tenant", RequestKind::Buffered).await),
AdmissionRejection::Global
);
drop(held);
control
.admit("tenant", RequestKind::Buffered)
.await
.expect("capacity returns when the permit drops");
}
#[tokio::test]
async fn a_saturated_tenant_leaves_other_tenants_their_capacity() {
let control = AdmissionControl::new(AdmissionLimits {
max_in_flight: Some(4),
max_in_flight_per_tenant: Some(1),
..limits()
});
let _noisy = control
.admit("noisy", RequestKind::Buffered)
.await
.expect("first request of the noisy tenant");
assert_eq!(
shed(control.admit("noisy", RequestKind::Buffered).await),
AdmissionRejection::Tenant
);
control
.admit("quiet", RequestKind::Buffered)
.await
.expect("another tenant keeps its own capacity");
}
#[tokio::test]
async fn a_refused_tenant_does_not_consume_the_global_ceiling() {
let control = AdmissionControl::new(AdmissionLimits {
max_in_flight: Some(2),
max_in_flight_per_tenant: Some(1),
..limits()
});
let _first = control
.admit("noisy", RequestKind::Buffered)
.await
.expect("admit");
for _ in 0..8 {
assert_eq!(
shed(control.admit("noisy", RequestKind::Buffered).await),
AdmissionRejection::Tenant
);
}
control
.admit("quiet", RequestKind::Buffered)
.await
.expect("the shed requests never took a global permit");
}
#[tokio::test]
async fn tenant_table_capacity_refuses_new_tenants_rather_than_unbounding() {
let control = AdmissionControl::new(AdmissionLimits {
max_in_flight_per_tenant: Some(1),
max_tenants: 1,
..limits()
});
let held = control
.admit("first", RequestKind::Buffered)
.await
.expect("admit");
assert_eq!(
shed(control.admit("second", RequestKind::Buffered).await),
AdmissionRejection::TenantCapacity
);
drop(held);
control
.admit("second", RequestKind::Buffered)
.await
.expect("an idle tenant leaves no entry behind");
}
#[tokio::test]
async fn streams_have_their_own_ceiling() {
let control = AdmissionControl::new(AdmissionLimits {
max_in_flight: Some(8),
max_in_flight_streams: Some(1),
..limits()
});
let _open = control
.admit("tenant", RequestKind::Streamed)
.await
.expect("admit");
assert_eq!(
shed(control.admit("tenant", RequestKind::Streamed).await),
AdmissionRejection::Streams
);
control
.admit("tenant", RequestKind::Buffered)
.await
.expect("a buffered request is not bound by the stream ceiling");
}
#[tokio::test(start_paused = true)]
async fn a_queued_request_is_admitted_when_capacity_frees() {
let control = Arc::new(AdmissionControl::new(AdmissionLimits {
max_in_flight: Some(1),
queue_capacity: Some(1),
queue_wait: Duration::from_secs(5),
..limits()
}));
let held = control
.admit("tenant", RequestKind::Buffered)
.await
.expect("admit");
let queued = tokio::spawn({
let control = Arc::clone(&control);
async move { control.admit("tenant", RequestKind::Buffered).await }
});
tokio::time::sleep(Duration::from_secs(1)).await;
drop(held);
queued
.await
.expect("task")
.expect("the queued request is admitted");
}
#[tokio::test(start_paused = true)]
async fn a_queued_request_expires_rather_than_waiting_forever() {
let control = AdmissionControl::new(AdmissionLimits {
max_in_flight: Some(1),
queue_capacity: Some(1),
queue_wait: Duration::from_secs(2),
..limits()
});
let _held = control
.admit("tenant", RequestKind::Buffered)
.await
.expect("admit");
let started = tokio::time::Instant::now();
assert_eq!(
shed(control.admit("tenant", RequestKind::Buffered).await),
AdmissionRejection::QueueTimeout
);
assert!(started.elapsed() >= Duration::from_secs(2));
}
#[tokio::test(start_paused = true)]
async fn the_queue_itself_is_bounded() {
let control = Arc::new(AdmissionControl::new(AdmissionLimits {
max_in_flight: Some(1),
queue_capacity: Some(1),
queue_wait: Duration::from_secs(30),
..limits()
}));
let _held = control
.admit("tenant", RequestKind::Buffered)
.await
.expect("admit");
let queued = tokio::spawn({
let control = Arc::clone(&control);
async move { control.admit("tenant", RequestKind::Buffered).await }
});
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(
shed(control.admit("tenant", RequestKind::Buffered).await),
AdmissionRejection::QueueFull
);
queued.abort();
}
#[tokio::test(start_paused = true)]
async fn an_abandoned_queued_request_frees_its_queue_slot() {
let control = Arc::new(AdmissionControl::new(AdmissionLimits {
max_in_flight: Some(1),
queue_capacity: Some(1),
queue_wait: Duration::from_secs(2),
..limits()
}));
let _held = control
.admit("tenant", RequestKind::Buffered)
.await
.expect("admit");
let queued = tokio::spawn({
let control = Arc::clone(&control);
async move { control.admit("tenant", RequestKind::Buffered).await }
});
tokio::time::sleep(Duration::from_millis(50)).await;
queued.abort();
let _ = queued.await;
assert_eq!(
shed(control.admit("tenant", RequestKind::Buffered).await),
AdmissionRejection::QueueTimeout
);
}
#[tokio::test]
async fn concurrent_admission_never_exceeds_the_ceiling() {
let control = Arc::new(AdmissionControl::new(AdmissionLimits {
max_in_flight: Some(3),
..limits()
}));
let mut tasks = Vec::new();
for _ in 0..16 {
let control = Arc::clone(&control);
tasks.push(tokio::spawn(async move {
control.admit("tenant", RequestKind::Buffered).await
}));
}
let mut held = Vec::new();
for task in tasks {
if let Ok(permit) = task.await.expect("task") {
held.push(permit);
}
}
assert_eq!(held.len(), 3);
}
#[test]
fn rejections_separate_caller_limits_from_process_saturation() {
assert!(AdmissionRejection::Tenant.is_caller_limit());
for rejection in [
AdmissionRejection::Global,
AdmissionRejection::QueueFull,
AdmissionRejection::QueueTimeout,
AdmissionRejection::Streams,
AdmissionRejection::TenantCapacity,
] {
assert!(!rejection.is_caller_limit(), "{rejection}");
}
assert_eq!(
AdmissionRejection::TenantCapacity.retry_after_seconds(),
None
);
assert_eq!(AdmissionRejection::Global.retry_after_seconds(), Some(1));
}
#[test]
fn zero_means_unbounded_when_limits_are_resolved() {
let config = AdmissionConfig {
max_in_flight: 0,
max_in_flight_streams: 0,
max_in_flight_per_tenant: 0,
queue_capacity: 0,
max_stream_duration_ms: 0,
max_prompt_tokens: 0,
max_output_tokens: 0,
max_stream_bytes: 0,
..AdmissionConfig::default()
};
let limits = AdmissionLimits::from(&config);
assert_eq!(limits.max_in_flight, None);
assert_eq!(limits.max_in_flight_streams, None);
assert_eq!(limits.max_in_flight_per_tenant, None);
assert_eq!(limits.queue_capacity, None);
assert_eq!(limits.max_stream_duration, None);
assert_eq!(limits.max_prompt_tokens, None);
assert_eq!(limits.max_output_tokens, None);
assert_eq!(limits.max_stream_bytes, None);
}
}