use std::collections::VecDeque;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use tokio::sync::Notify;
use tracing::Instrument;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverflowPolicy {
DropOldest,
Reject,
Block,
}
#[derive(Debug)]
pub enum BackpressureError<T> {
QueueFull(T),
}
impl<T> std::fmt::Display for BackpressureError<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "backpressure queue is full")
}
}
impl<T: std::fmt::Debug> std::error::Error for BackpressureError<T> {}
struct Inner<T> {
queue: Mutex<VecDeque<T>>,
not_full: Notify,
not_empty: Notify,
capacity: usize,
policy: OverflowPolicy,
accepted: AtomicU64,
dropped: AtomicU64,
rejected: AtomicU64,
closed: AtomicU64,
}
const CLOSED: u64 = 1;
const OPEN: u64 = 0;
#[derive(Clone)]
pub struct BackpressureQueue<T> {
inner: Arc<Inner<T>>,
}
impl<T> BackpressureQueue<T> {
pub fn new(capacity: usize, policy: OverflowPolicy) -> Self {
assert!(capacity > 0, "backpressure queue capacity must be > 0");
Self {
inner: Arc::new(Inner {
queue: Mutex::new(VecDeque::with_capacity(capacity)),
not_full: Notify::new(),
not_empty: Notify::new(),
capacity,
policy,
accepted: AtomicU64::new(0),
dropped: AtomicU64::new(0),
rejected: AtomicU64::new(0),
closed: AtomicU64::new(OPEN),
}),
}
}
pub async fn push(&self, item: T) -> Result<(), BackpressureError<T>> {
if self.inner.closed.load(Ordering::Acquire) == CLOSED {
return Err(BackpressureError::QueueFull(item));
}
match self.inner.policy {
OverflowPolicy::Block => {
let span = tracing::info_span!("mytheclipse_backpressure_task");
loop {
{
let mut queue = self.inner.queue.lock().unwrap();
if queue.len() < self.inner.capacity {
queue.push_back(item);
self.inner.accepted.fetch_add(1, Ordering::AcqRel);
drop(queue);
self.inner.not_empty.notify_one();
return Ok(());
}
}
let waiter = self.inner.not_full.notified();
tokio::pin!(waiter);
waiter.as_mut().instrument(span.clone()).await;
}
}
OverflowPolicy::Reject => self.try_push(item),
OverflowPolicy::DropOldest => {
self.try_push_dropping(item);
Ok(())
}
}
}
pub fn try_push(&self, item: T) -> Result<(), BackpressureError<T>> {
let mut queue = self.inner.queue.lock().unwrap();
if self.inner.closed.load(Ordering::Acquire) == CLOSED {
return Err(BackpressureError::QueueFull(item));
}
if queue.len() >= self.inner.capacity {
match self.inner.policy {
OverflowPolicy::DropOldest => {
let _dropped = queue.pop_front();
self.inner.dropped.fetch_add(1, Ordering::AcqRel);
}
_ => {
self.inner.rejected.fetch_add(1, Ordering::AcqRel);
return Err(BackpressureError::QueueFull(item));
}
}
}
queue.push_back(item);
self.inner.accepted.fetch_add(1, Ordering::AcqRel);
drop(queue);
self.inner.not_empty.notify_one();
Ok(())
}
fn try_push_dropping(&self, item: T) {
let mut queue = self.inner.queue.lock().unwrap();
if queue.len() >= self.inner.capacity {
let _dropped = queue.pop_front();
self.inner.dropped.fetch_add(1, Ordering::AcqRel);
}
queue.push_back(item);
self.inner.accepted.fetch_add(1, Ordering::AcqRel);
drop(queue);
self.inner.not_empty.notify_one();
}
pub async fn pop(&self) -> Option<T> {
loop {
{
let mut queue = self.inner.queue.lock().unwrap();
if let Some(item) = queue.pop_front() {
self.inner.not_full.notify_one();
return Some(item);
}
if self.inner.closed.load(Ordering::Acquire) == CLOSED && queue.is_empty() {
return None;
}
}
self.inner.not_empty.notified().await;
}
}
pub fn try_pop(&self) -> Option<T> {
let mut queue = self.inner.queue.lock().unwrap();
let item = queue.pop_front();
if item.is_some() {
self.inner.not_full.notify_one();
}
item
}
pub fn len(&self) -> usize {
self.inner.queue.lock().unwrap().len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn capacity(&self) -> usize {
self.inner.capacity
}
pub fn remaining(&self) -> usize {
self.inner.capacity - self.len()
}
pub fn accepted_count(&self) -> u64 {
self.inner.accepted.load(Ordering::Acquire)
}
pub fn dropped_count(&self) -> u64 {
self.inner.dropped.load(Ordering::Acquire)
}
pub fn rejected_count(&self) -> u64 {
self.inner.rejected.load(Ordering::Acquire)
}
pub fn close(&self) {
self.inner.closed.store(CLOSED, Ordering::Release);
self.inner.not_empty.notify_waiters();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn push_pop_roundtrip() {
let q: BackpressureQueue<u32> = BackpressureQueue::new(4, OverflowPolicy::Reject);
q.push(1).await.unwrap();
q.push(2).await.unwrap();
assert_eq!(q.pop().await, Some(1));
assert_eq!(q.pop().await, Some(2));
assert!(q.is_empty());
}
#[tokio::test]
async fn reject_returns_item_when_full() {
let q: BackpressureQueue<u32> = BackpressureQueue::new(2, OverflowPolicy::Reject);
q.push(1).await.unwrap();
q.push(2).await.unwrap();
let err = q.push(3).await.unwrap_err();
match err {
BackpressureError::QueueFull(v) => assert_eq!(v, 3),
}
assert_eq!(q.len(), 2);
assert_eq!(q.rejected_count(), 1);
}
#[tokio::test]
async fn drop_oldest_evicts_front() {
let q: BackpressureQueue<u32> = BackpressureQueue::new(2, OverflowPolicy::DropOldest);
q.push(1).await.unwrap();
q.push(2).await.unwrap();
q.push(3).await.unwrap();
assert_eq!(q.len(), 2);
assert_eq!(q.pop().await, Some(2)); assert_eq!(q.dropped_count(), 1);
}
#[tokio::test]
async fn block_waits_for_a_slot() {
let q: BackpressureQueue<u32> = BackpressureQueue::new(1, OverflowPolicy::Block);
q.push(1).await.unwrap();
let q2 = q.clone();
let pusher = tokio::spawn(async move {
q2.push(2).await.unwrap();
});
tokio::time::sleep(Duration::from_millis(20)).await;
assert_eq!(q.pop().await, Some(1));
pusher.await.unwrap();
assert_eq!(q.pop().await, Some(2));
assert_eq!(q.accepted_count(), 2);
}
#[tokio::test]
async fn close_drains_then_returns_none() {
let q: BackpressureQueue<u32> = BackpressureQueue::new(2, OverflowPolicy::Reject);
q.push(1).await.unwrap();
q.push(2).await.unwrap();
let q2 = q.clone();
let drainer = tokio::spawn(async move {
let mut seen = Vec::new();
while let Some(v) = q2.pop().await {
seen.push(v);
}
seen
});
q.close();
assert_eq!(drainer.await.unwrap(), vec![1, 2]);
assert!(q.push(3).await.is_err());
}
}