use std::collections::VecDeque;
use std::error::Error;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AdmissionLimits {
pub max_in_flight: usize,
pub max_memory_bytes: usize,
pub max_queue_depth: usize,
pub base_retry_after_ms: u64,
}
impl AdmissionLimits {
pub fn new(max_in_flight: usize, max_memory_bytes: usize, max_queue_depth: usize) -> Self {
Self {
max_in_flight: max_in_flight.max(1),
max_memory_bytes: max_memory_bytes.max(1),
max_queue_depth,
base_retry_after_ms: 25,
}
}
pub fn retry_after_ms(mut self, base_retry_after_ms: u64) -> Self {
self.base_retry_after_ms = base_retry_after_ms.max(1);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdmissionController {
limits: AdmissionLimits,
queue: VecDeque<AdmissionQueueTicket>,
in_flight: usize,
memory_bytes: usize,
rejected_total: u64,
}
impl AdmissionController {
pub fn new(limits: AdmissionLimits) -> Self {
Self {
limits,
queue: VecDeque::new(),
in_flight: 0,
memory_bytes: 0,
rejected_total: 0,
}
}
pub fn try_acquire(
&mut self,
request_id: impl Into<String>,
estimated_bytes: usize,
) -> Result<AdmissionPermit, AdmissionError> {
self.ensure_fits(estimated_bytes)?;
if self.in_flight >= self.limits.max_in_flight {
return self.reject(AdmissionRejectionReason::InFlightLimit);
}
if self.memory_bytes.saturating_add(estimated_bytes) > self.limits.max_memory_bytes {
return self.reject(AdmissionRejectionReason::MemoryLimit);
}
self.in_flight += 1;
self.memory_bytes = self.memory_bytes.saturating_add(estimated_bytes);
Ok(AdmissionPermit {
request_id: request_id.into(),
estimated_bytes,
})
}
pub fn enqueue(
&mut self,
request_id: impl Into<String>,
estimated_bytes: usize,
) -> Result<AdmissionQueueTicket, AdmissionError> {
self.ensure_fits(estimated_bytes)?;
if self.queue.len() >= self.limits.max_queue_depth {
return self.reject(AdmissionRejectionReason::QueueFull);
}
let ticket = AdmissionQueueTicket {
request_id: request_id.into(),
estimated_bytes,
};
self.queue.push_back(ticket.clone());
Ok(ticket)
}
pub fn admit_next(&mut self) -> Option<AdmissionPermit> {
let ticket = self.queue.front()?;
if self.in_flight >= self.limits.max_in_flight
|| self.memory_bytes.saturating_add(ticket.estimated_bytes)
> self.limits.max_memory_bytes
{
return None;
}
let ticket = self.queue.pop_front().expect("front was present");
self.in_flight += 1;
self.memory_bytes = self.memory_bytes.saturating_add(ticket.estimated_bytes);
Some(AdmissionPermit {
request_id: ticket.request_id,
estimated_bytes: ticket.estimated_bytes,
})
}
pub fn release(&mut self, permit: AdmissionPermit) {
self.in_flight = self.in_flight.saturating_sub(1);
self.memory_bytes = self.memory_bytes.saturating_sub(permit.estimated_bytes);
}
pub fn snapshot(&self) -> AdmissionSnapshot {
AdmissionSnapshot {
in_flight: self.in_flight,
memory_bytes: self.memory_bytes,
queue_depth: self.queue.len(),
rejected_total: self.rejected_total,
}
}
fn ensure_fits(&mut self, estimated_bytes: usize) -> Result<(), AdmissionError> {
if estimated_bytes > self.limits.max_memory_bytes {
return self.reject(AdmissionRejectionReason::MemoryLimit);
}
Ok(())
}
fn reject<T>(&mut self, reason: AdmissionRejectionReason) -> Result<T, AdmissionError> {
self.rejected_total = self.rejected_total.saturating_add(1);
Err(AdmissionError::Backpressure {
reason,
retry_after_ms: self.retry_after_ms(),
})
}
fn retry_after_ms(&self) -> u64 {
self.limits
.base_retry_after_ms
.saturating_mul((self.queue.len() as u64).saturating_add(1))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdmissionPermit {
pub request_id: String,
pub estimated_bytes: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdmissionQueueTicket {
pub request_id: String,
pub estimated_bytes: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AdmissionSnapshot {
pub in_flight: usize,
pub memory_bytes: usize,
pub queue_depth: usize,
pub rejected_total: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdmissionRejectionReason {
InFlightLimit,
MemoryLimit,
QueueFull,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AdmissionError {
Backpressure {
reason: AdmissionRejectionReason,
retry_after_ms: u64,
},
}
impl fmt::Display for AdmissionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Backpressure {
reason,
retry_after_ms,
} => write!(
formatter,
"admission backpressure: {reason:?}; retry after {retry_after_ms} ms"
),
}
}
}
impl Error for AdmissionError {}