use std::collections::VecDeque;
use std::fmt;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use tokio::sync::Notify;
use tokio::time::Instant;
use super::budget::BudgetAdmission;
use super::budget::BudgetRejection;
use super::budget::PermitRebindOutcome;
use super::budget::ResourceBudget;
use super::budget::ResourcePermit;
use super::limit::BudgetClass;
use super::limit::BudgetDimension;
use super::limit::FullPolicy;
#[derive(Debug)]
pub enum QueuePushOutcome<T> {
Enqueued,
Coalesced {
replaced: usize,
},
DroppedStale {
dropped: usize,
},
Rejected {
item: T,
rejection: QueuePushRejection,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueuePushRejection {
BudgetExhausted(BudgetRejection),
DeadlineExceeded,
Closed,
SlowConsumerClosed,
}
#[derive(Debug)]
pub struct ForeignPermit<T> {
pub item: T,
pub permit: Box<ResourcePermit>,
}
pub struct BudgetedItem<T> {
item: T,
enqueued_at: Duration,
permit: ResourcePermit,
}
impl<T> BudgetedItem<T> {
#[must_use]
pub fn into_item(self) -> T {
self.item
}
#[must_use]
pub fn into_parts(self) -> (T, ResourcePermit, Duration) {
(self.item, self.permit, self.enqueued_at)
}
#[must_use]
pub const fn enqueued_at(&self) -> Duration {
self.enqueued_at
}
#[must_use]
pub fn retained_bytes(&self) -> usize {
self.permit.bytes()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueueSnapshot {
pub path: Arc<str>,
pub depth: usize,
pub reserved_count: usize,
pub retained_bytes: usize,
pub oldest_age: Option<Duration>,
pub throttled_count: u64,
pub rejected_count: u64,
pub dropped_count: u64,
pub coalesced_count: u64,
pub closed_slow_consumer_count: u64,
pub waiters: usize,
pub wait_count: u64,
pub deadline_exceeded_count: u64,
pub closed: bool,
}
pub struct BudgetedQueue<T> {
inner: Arc<QueueInner<T>>,
}
impl<T> Clone for BudgetedQueue<T> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl<T> fmt::Debug for BudgetedQueue<T> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("BudgetedQueue")
.field("budget", &self.inner.budget)
.finish_non_exhaustive()
}
}
impl<T> BudgetedQueue<T> {
#[must_use]
pub fn new(budget: ResourceBudget) -> Self {
Self {
inner: Arc::new(QueueInner {
budget,
state: Mutex::new(QueueState {
items: VecDeque::new(),
closed: false,
}),
coalesce_push: Mutex::new(()),
notify: Notify::new(),
metrics: QueueMetrics::default(),
}),
}
}
#[must_use]
pub fn is_same_queue(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.inner, &other.inner)
}
pub fn try_push(&self, item: T, retained_bytes: usize, class: BudgetClass) -> QueuePushOutcome<T> {
let mut discarded = Vec::new();
let _coalesce_guard = if self.inner.budget.limit().full_policy == FullPolicy::CoalesceLatest {
Some(
self.inner
.coalesce_push
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
)
} else {
None
};
let admission = match self.inner.budget.admit() {
Ok(admission) => admission,
Err(_) => {
return QueuePushOutcome::Rejected {
item,
rejection: QueuePushRejection::Closed,
}
}
};
let dropped = self.apply_age_policy(&mut discarded);
match admission.try_acquire(retained_bytes, class) {
Ok(permit) => {
if let Some(rejected) = self.enqueue(item, permit) {
return rejected;
}
if dropped == 0 {
QueuePushOutcome::Enqueued
} else {
QueuePushOutcome::DroppedStale { dropped }
}
}
Err(error) => self.handle_full(&admission, item, retained_bytes, class, error, &mut discarded),
}
}
pub fn try_push_budgeted(
&self,
item: T,
mut permit: ResourcePermit,
) -> Result<QueuePushOutcome<T>, ForeignPermit<T>> {
let mut discarded = Vec::new();
if !permit.belongs_to_tree(&self.inner.budget) {
return Err(ForeignPermit {
item,
permit: Box::new(permit),
});
}
let _coalesce_guard = if self.inner.budget.limit().full_policy == FullPolicy::CoalesceLatest {
Some(
self.inner
.coalesce_push
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
)
} else {
None
};
let admission = match self.inner.budget.admit() {
Ok(admission) => admission,
Err(_) => {
return Ok(QueuePushOutcome::Rejected {
item,
rejection: QueuePushRejection::Closed,
})
}
};
let dropped = self.apply_age_policy(&mut discarded);
match permit.try_rebind_admitted(&admission) {
PermitRebindOutcome::Rebound | PermitRebindOutcome::Unchanged => {
if let Some(rejected) = self.enqueue(item, permit) {
return Ok(rejected);
}
Ok(if dropped == 0 {
QueuePushOutcome::Enqueued
} else {
QueuePushOutcome::DroppedStale { dropped }
})
}
PermitRebindOutcome::Rejected(rejection) => Ok(QueuePushOutcome::Rejected {
item,
rejection: QueuePushRejection::BudgetExhausted(rejection),
}),
}
}
pub fn try_push_data(&self, item: T, retained_bytes: usize) -> QueuePushOutcome<T> {
self.try_push(item, retained_bytes, BudgetClass::Data)
}
pub fn try_push_control(&self, item: T, retained_bytes: usize) -> QueuePushOutcome<T> {
self.try_push(item, retained_bytes, BudgetClass::Control)
}
pub async fn push_until(
&self,
item: T,
retained_bytes: usize,
class: BudgetClass,
deadline: Instant,
) -> QueuePushOutcome<T> {
if self.inner.budget.limit().full_policy != FullPolicy::WaitUntilDeadline {
return self.try_push(item, retained_bytes, class);
}
if self.is_closed() {
return QueuePushOutcome::Rejected {
item,
rejection: QueuePushRejection::Closed,
};
}
if let Some(rejection) = self.inner.budget.permanent_acquire_rejection(retained_bytes, class) {
return QueuePushOutcome::Rejected {
item,
rejection: if rejection.is_closed() {
QueuePushRejection::Closed
} else {
QueuePushRejection::BudgetExhausted(rejection)
},
};
}
let mut waiter = None;
loop {
let capacity_notified = self.inner.budget.capacity_notify().notified();
tokio::pin!(capacity_notified);
capacity_notified.as_mut().enable();
let state_notified = self.inner.notify.notified();
tokio::pin!(state_notified);
state_notified.as_mut().enable();
if self.is_closed() {
return QueuePushOutcome::Rejected {
item,
rejection: QueuePushRejection::Closed,
};
}
if Instant::now() >= deadline {
self.inner.metrics.record_deadline_exceeded();
return QueuePushOutcome::Rejected {
item,
rejection: QueuePushRejection::DeadlineExceeded,
};
}
{
let admission = match self.inner.budget.admit() {
Ok(admission) => admission,
Err(_) => {
return QueuePushOutcome::Rejected {
item,
rejection: QueuePushRejection::Closed,
}
}
};
match admission.try_acquire_waiting(retained_bytes, class) {
Ok(permit) => {
return self.enqueue(item, permit).unwrap_or(QueuePushOutcome::Enqueued);
}
Err(error) if error.dimension() == Some(BudgetDimension::Rate) => {
self.inner.budget.record_budget_rejection(&error);
return QueuePushOutcome::Rejected {
item,
rejection: QueuePushRejection::BudgetExhausted(error),
};
}
Err(_) => {}
}
}
if waiter.is_none() {
waiter = Some(self.inner.metrics.begin_wait());
}
let deadline_sleep = tokio::time::sleep_until(deadline);
tokio::pin!(deadline_sleep);
tokio::select! {
biased;
() = &mut deadline_sleep => {
self.inner.metrics.record_deadline_exceeded();
return QueuePushOutcome::Rejected { item, rejection: QueuePushRejection::DeadlineExceeded };
}
() = &mut state_notified => {}
() = &mut capacity_notified => {}
}
}
}
pub fn try_pop(&self) -> Option<T> {
self.try_pop_budgeted().map(BudgetedItem::into_item)
}
pub fn try_pop_budgeted(&self) -> Option<BudgetedItem<T>> {
let mut discarded = Vec::new();
self.apply_age_policy(&mut discarded);
let mut state = self
.inner
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.items.pop_front()
}
pub fn retain(&self, mut keep: impl FnMut(&T) -> bool) -> usize {
let mut state = self
.inner
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let previous_len = state.items.len();
state.items.retain(|item| keep(&item.item));
previous_len - state.items.len()
}
pub async fn recv(&self) -> Option<T> {
self.recv_budgeted().await.map(BudgetedItem::into_item)
}
pub async fn recv_budgeted(&self) -> Option<BudgetedItem<T>> {
loop {
let notified = self.inner.notify.notified();
tokio::pin!(notified);
notified.as_mut().enable();
let budget_changed = self.inner.budget.capacity_notify().notified();
tokio::pin!(budget_changed);
budget_changed.as_mut().enable();
if let Some(item) = self.try_pop_budgeted() {
return Some(item);
}
if self.is_closed() {
return self.try_pop_budgeted();
}
tokio::select! {
_ = &mut notified => {},
_ = &mut budget_changed => {},
}
}
}
pub fn close(&self) {
let mut state = self
.inner
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.closed = true;
drop(state);
self.inner.notify.notify_waiters();
}
#[must_use]
pub fn is_closed(&self) -> bool {
self.inner.budget.is_closed()
|| self
.inner
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.closed
}
#[must_use]
pub fn len(&self) -> usize {
self.inner
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.items
.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn snapshot(&self) -> QueueSnapshot {
let budget_closed = self.inner.budget.is_closed();
let now = self.inner.budget.monotonic_now();
let state = self
.inner
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let budget = self.inner.budget.snapshot();
QueueSnapshot {
path: budget.path,
depth: state.items.len(),
reserved_count: budget.current_count,
retained_bytes: budget.current_bytes,
oldest_age: state.items.front().map(|entry| now.saturating_sub(entry.enqueued_at)),
throttled_count: budget.throttled_count,
rejected_count: budget.rejected_count,
dropped_count: budget.dropped_count,
coalesced_count: budget.coalesced_count,
closed_slow_consumer_count: budget.closed_slow_consumer_count,
waiters: self.inner.metrics.waiters.load(Ordering::Acquire),
wait_count: self.inner.metrics.wait_count.load(Ordering::Relaxed),
deadline_exceeded_count: self.inner.metrics.deadline_exceeded_count.load(Ordering::Relaxed),
closed: state.closed || budget_closed,
}
}
fn enqueue(&self, item: T, permit: ResourcePermit) -> Option<QueuePushOutcome<T>> {
let mut state = self
.inner
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state.closed {
return Some(QueuePushOutcome::Rejected {
item,
rejection: QueuePushRejection::Closed,
});
}
state.items.push_back(BudgetedItem {
item,
enqueued_at: self.inner.budget.monotonic_now(),
permit,
});
drop(state);
self.inner.notify.notify_one();
None
}
fn handle_full(
&self,
admission: &BudgetAdmission<'_>,
item: T,
retained_bytes: usize,
class: BudgetClass,
rejection: BudgetRejection,
discarded: &mut Vec<T>,
) -> QueuePushOutcome<T> {
match self.inner.budget.limit().full_policy {
FullPolicy::Reject | FullPolicy::WaitUntilDeadline | FullPolicy::DropStale => QueuePushOutcome::Rejected {
item,
rejection: QueuePushRejection::BudgetExhausted(rejection),
},
FullPolicy::CoalesceLatest => {
if rejection.dimension() == Some(BudgetDimension::Rate)
|| rejection.exhausted_path() != self.inner.budget.path()
{
return QueuePushOutcome::Rejected {
item,
rejection: QueuePushRejection::BudgetExhausted(rejection),
};
}
if !self.item_can_fit(retained_bytes, class) {
return QueuePushOutcome::Rejected {
item,
rejection: QueuePushRejection::BudgetExhausted(rejection),
};
}
let replaced = {
let mut state = self
.inner
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state.closed {
return QueuePushOutcome::Rejected {
item,
rejection: QueuePushRejection::Closed,
};
}
let replaced = state.items.len();
discarded.extend(state.items.drain(..).map(BudgetedItem::into_item));
replaced
};
self.inner.budget.record_coalesced(replaced);
let permit = match admission.try_acquire(retained_bytes, class) {
Ok(permit) => permit,
Err(retry_error) => {
return QueuePushOutcome::Rejected {
item,
rejection: QueuePushRejection::BudgetExhausted(retry_error),
};
}
};
self.enqueue(item, permit)
.unwrap_or(QueuePushOutcome::Coalesced { replaced })
}
FullPolicy::CloseSlowConsumer => {
let dropped = {
let mut state = self
.inner
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.closed = true;
let dropped = state.items.len();
discarded.extend(state.items.drain(..).map(BudgetedItem::into_item));
dropped
};
self.inner.budget.record_dropped(dropped);
self.inner.budget.record_slow_consumer_closed();
self.inner.notify.notify_waiters();
QueuePushOutcome::Rejected {
item,
rejection: QueuePushRejection::SlowConsumerClosed,
}
}
}
}
fn item_can_fit(&self, retained_bytes: usize, class: BudgetClass) -> bool {
let limit = self.inner.budget.limit();
let (count_capacity, byte_capacity) = match class {
BudgetClass::Data => (
limit.capacity.count.saturating_sub(limit.control_reserve.count),
limit.capacity.bytes.saturating_sub(limit.control_reserve.bytes),
),
BudgetClass::Control => (limit.capacity.count, limit.capacity.bytes),
};
count_capacity > 0 && retained_bytes <= byte_capacity
}
fn apply_age_policy(&self, discarded: &mut Vec<T>) -> usize {
let Some(max_age) = self.inner.budget.limit().max_age else {
return 0;
};
let policy = self.inner.budget.limit().full_policy;
if !matches!(policy, FullPolicy::DropStale | FullPolicy::CloseSlowConsumer) {
return 0;
}
let now = self.inner.budget.monotonic_now();
let mut state = self
.inner
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if policy == FullPolicy::CloseSlowConsumer
&& state
.items
.front()
.is_some_and(|entry| now.saturating_sub(entry.enqueued_at) >= max_age)
{
state.closed = true;
let dropped = state.items.len();
discarded.extend(state.items.drain(..).map(BudgetedItem::into_item));
drop(state);
self.inner.budget.record_dropped(dropped);
self.inner.budget.record_slow_consumer_closed();
self.inner.notify.notify_waiters();
return dropped;
}
let mut dropped = 0;
while state
.items
.front()
.is_some_and(|entry| now.saturating_sub(entry.enqueued_at) >= max_age)
{
if let Some(item) = state.items.pop_front() {
discarded.push(item.into_item());
}
dropped += 1;
}
drop(state);
self.inner.budget.record_dropped(dropped);
dropped
}
}
struct QueueInner<T> {
budget: ResourceBudget,
state: Mutex<QueueState<T>>,
coalesce_push: Mutex<()>,
notify: Notify,
metrics: QueueMetrics,
}
struct QueueState<T> {
items: VecDeque<BudgetedItem<T>>,
closed: bool,
}
#[derive(Default)]
struct QueueMetrics {
waiters: AtomicUsize,
wait_count: AtomicU64,
deadline_exceeded_count: AtomicU64,
}
impl QueueMetrics {
fn begin_wait(&self) -> WaiterGuard<'_> {
self.wait_count.fetch_add(1, Ordering::Relaxed);
self.waiters.fetch_add(1, Ordering::AcqRel);
WaiterGuard { metrics: self }
}
fn record_deadline_exceeded(&self) {
self.deadline_exceeded_count.fetch_add(1, Ordering::Relaxed);
}
}
struct WaiterGuard<'a> {
metrics: &'a QueueMetrics,
}
impl Drop for WaiterGuard<'_> {
fn drop(&mut self) {
let previous = self.metrics.waiters.fetch_sub(1, Ordering::AcqRel);
debug_assert!(previous > 0);
}
}