use core::sync::atomic::{AtomicUsize, Ordering};
use super::StaticRing;
#[cfg(feature = "alloc")]
use super::{Drainer, Ring};
pub trait RingStorage {
type Item;
fn push(&self, item: Self::Item) -> Result<(), Self::Item>;
fn dequeue(&self) -> Option<Self::Item>;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn capacity(&self) -> usize;
fn drain_into(&self, out: &mut [Self::Item]) -> usize {
let mut count = 0;
while count < out.len() {
match self.dequeue() {
Some(item) => {
out[count] = item;
count += 1;
}
None => break,
}
}
count
}
}
impl<T, const N: usize> RingStorage for StaticRing<T, N> {
type Item = T;
fn push(&self, item: T) -> Result<(), T> {
StaticRing::push(self, item)
}
fn dequeue(&self) -> Option<T> {
StaticRing::dequeue(self)
}
fn len(&self) -> usize {
StaticRing::len(self)
}
fn capacity(&self) -> usize {
StaticRing::capacity(self)
}
}
#[cfg(feature = "alloc")]
impl<T> RingStorage for Ring<T> {
type Item = T;
#[inline]
fn push(&self, item: T) -> Result<(), T> {
Ring::push(self, item)
}
#[inline]
fn dequeue(&self) -> Option<T> {
Ring::dequeue(self)
}
fn len(&self) -> usize {
Ring::len(self)
}
fn capacity(&self) -> usize {
Ring::cap(self)
}
fn drain_into(&self, out: &mut [T]) -> usize {
Drainer::new(self).drain_into(out)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FailMode {
DropOldest,
DropNewest,
FailClosed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnqueueOutcome {
Enqueued,
DroppedOldest,
DroppedNewest,
Refused,
}
impl EnqueueOutcome {
#[must_use]
pub fn is_drop(self) -> bool {
!matches!(self, Self::Enqueued)
}
}
pub struct BoundedQueue<R: RingStorage> {
queue: R,
fail_mode: FailMode,
drops: AtomicUsize,
}
impl<R: RingStorage> BoundedQueue<R> {
pub const fn from_storage(queue: R, fail_mode: FailMode) -> Self {
Self {
queue,
fail_mode,
drops: AtomicUsize::new(0),
}
}
#[must_use]
pub fn capacity(&self) -> usize {
self.queue.capacity()
}
#[must_use]
pub fn len(&self) -> usize {
self.queue.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.queue.is_empty()
}
#[must_use]
pub fn dropped(&self) -> u64 {
self.drops.load(Ordering::Relaxed) as u64
}
pub fn note_drop(&self) {
self.drops.fetch_add(1, Ordering::Relaxed);
}
pub fn enqueue(&self, item: R::Item) -> EnqueueOutcome {
let outcome = match self.queue.push(item) {
Ok(()) => EnqueueOutcome::Enqueued,
Err(rejected) => match self.fail_mode {
FailMode::DropOldest => {
let _oldest = self.queue.dequeue();
match self.queue.push(rejected) {
Ok(()) => EnqueueOutcome::DroppedOldest,
Err(_lost) => EnqueueOutcome::DroppedNewest,
}
}
FailMode::DropNewest => EnqueueOutcome::DroppedNewest,
FailMode::FailClosed => EnqueueOutcome::Refused,
},
};
if outcome.is_drop() {
self.drops.fetch_add(1, Ordering::Relaxed);
}
outcome
}
#[inline]
pub fn try_enqueue(&self, item: R::Item) -> Result<(), R::Item> {
self.queue.push(item)
}
pub fn enqueue_assisting<F>(&self, mut item: R::Item, mut on_full: F) -> Result<(), R::Item>
where
F: FnMut() -> bool,
{
loop {
match self.queue.push(item) {
Ok(()) => return Ok(()),
Err(returned) => {
item = returned;
if !on_full() {
return Err(item);
}
}
}
}
}
#[must_use]
pub fn dequeue(&self) -> Option<R::Item> {
self.queue.dequeue()
}
pub fn drain_into(&self, out: &mut [R::Item]) -> usize {
self.queue.drain_into(out)
}
}
pub type StaticBoundedQueue<T, const N: usize> = BoundedQueue<StaticRing<T, N>>;
impl<T, const N: usize> BoundedQueue<StaticRing<T, N>> {
#[must_use]
#[cfg(not(feature = "loom"))]
pub const fn new(fail_mode: FailMode) -> Self {
Self::from_storage(StaticRing::new(), fail_mode)
}
#[must_use]
#[cfg(feature = "loom")]
pub fn new(fail_mode: FailMode) -> Self {
Self::from_storage(StaticRing::new(), fail_mode)
}
}
#[cfg(feature = "alloc")]
pub type HeapBoundedQueue<T> = BoundedQueue<Ring<T>>;
#[cfg(feature = "alloc")]
impl<T> BoundedQueue<Ring<T>> {
#[must_use]
pub fn new(capacity: usize, fail_mode: FailMode) -> Self {
Self::from_storage(Ring::with_capacity(capacity), fail_mode)
}
}
#[cfg(all(test, not(feature = "loom")))]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn drop_oldest_keeps_newest<R: RingStorage<Item = u32>>(queue: BoundedQueue<R>) {
assert_eq!(queue.enqueue(1), EnqueueOutcome::Enqueued);
assert_eq!(queue.enqueue(2), EnqueueOutcome::Enqueued);
assert_eq!(
queue.enqueue(3),
EnqueueOutcome::DroppedOldest,
"evicts the oldest"
);
assert_eq!(queue.dropped(), 1);
assert_eq!(
queue.dequeue(),
Some(2),
"oldest (1) evicted; 2 and 3 remain"
);
assert_eq!(queue.dequeue(), Some(3));
assert_eq!(queue.dequeue(), None);
}
fn fail_closed_refuses<R: RingStorage<Item = u32>>(queue: BoundedQueue<R>) {
assert_eq!(queue.enqueue(1), EnqueueOutcome::Enqueued);
assert_eq!(queue.enqueue(2), EnqueueOutcome::Enqueued);
assert_eq!(queue.enqueue(3), EnqueueOutcome::Refused);
assert_eq!(queue.dropped(), 1);
}
fn try_enqueue_hands_back_without_dropping<R: RingStorage<Item = u32>>(queue: BoundedQueue<R>) {
assert!(queue.try_enqueue(1).is_ok());
assert!(queue.try_enqueue(2).is_ok());
assert_eq!(queue.try_enqueue(3), Err(3), "handed back, not dropped");
assert_eq!(queue.dropped(), 0, "try_enqueue never counts a drop");
}
#[test]
fn static_tier_matches() {
drop_oldest_keeps_newest(StaticBoundedQueue::<u32, 2>::new(FailMode::DropOldest));
fail_closed_refuses(StaticBoundedQueue::<u32, 2>::new(FailMode::FailClosed));
try_enqueue_hands_back_without_dropping(StaticBoundedQueue::<u32, 2>::new(
FailMode::FailClosed,
));
}
#[cfg(feature = "alloc")]
#[test]
fn heap_tier_matches() {
drop_oldest_keeps_newest(HeapBoundedQueue::<u32>::new(2, FailMode::DropOldest));
fail_closed_refuses(HeapBoundedQueue::<u32>::new(2, FailMode::FailClosed));
try_enqueue_hands_back_without_dropping(HeapBoundedQueue::<u32>::new(
2,
FailMode::FailClosed,
));
}
#[cfg(feature = "alloc")]
#[test]
fn heap_capacity_rounds_up_to_power_of_two() {
assert_eq!(
HeapBoundedQueue::<u32>::new(3, FailMode::DropNewest).capacity(),
4
);
assert_eq!(
HeapBoundedQueue::<u32>::new(1, FailMode::DropNewest).capacity(),
2
);
}
#[test]
fn enqueue_assisting_makes_room_and_lands() {
let queue = StaticBoundedQueue::<u32, 2>::new(FailMode::FailClosed);
queue.enqueue(1);
queue.enqueue(2);
let landed = queue.enqueue_assisting(3, || queue.dequeue().is_some());
assert!(landed.is_ok());
assert_eq!(queue.dequeue(), Some(2), "1 was freed to make room");
assert_eq!(queue.dequeue(), Some(3));
}
#[test]
fn enqueue_assisting_hands_back_on_give_up() {
let queue = StaticBoundedQueue::<u32, 2>::new(FailMode::FailClosed);
queue.enqueue(1);
queue.enqueue(2);
assert_eq!(
queue.enqueue_assisting(3, || false),
Err(3),
"gave up, item handed back"
);
}
#[test]
fn drain_into_batch_pops_fifo_both_tiers() {
let queue = StaticBoundedQueue::<u32, 4>::new(FailMode::DropNewest);
for value in 1..=3 {
queue.enqueue(value);
}
let mut out = [0u32; 8];
assert_eq!(
queue.drain_into(&mut out),
3,
"drains exactly what is queued"
);
assert_eq!(&out[..3], &[1, 2, 3]);
assert!(queue.dequeue().is_none());
}
#[test]
fn drop_newest_leaves_queue_untouched() {
let queue = StaticBoundedQueue::<u32, 2>::new(FailMode::DropNewest);
queue.enqueue(1);
queue.enqueue(2);
assert_eq!(queue.enqueue(3), EnqueueOutcome::DroppedNewest);
assert_eq!(queue.dropped(), 1);
assert_eq!(queue.dequeue(), Some(1), "queue untouched");
assert_eq!(queue.dequeue(), Some(2));
}
}