use core::sync::atomic::{AtomicBool, Ordering};
use portable_atomic::AtomicU64;
#[cfg(feature = "alloc")]
use proxima_core::ring::Ring;
use proxima_core::ring::{BoundedQueue, EnqueueOutcome, FailMode, RingStorage, StaticRing};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[must_use]
pub enum Admission {
Accepted,
Dormant,
Dropped(DropReason),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DropReason {
OldestEvicted,
NewestDiscarded,
Refused,
}
impl Admission {
#[must_use]
pub fn is_accepted(self) -> bool {
matches!(self, Self::Accepted)
}
#[must_use]
pub fn leaves_item_queued(self) -> bool {
matches!(
self,
Self::Accepted | Self::Dropped(DropReason::OldestEvicted)
)
}
}
fn admission_of(outcome: EnqueueOutcome) -> Admission {
match outcome {
EnqueueOutcome::Enqueued => Admission::Accepted,
EnqueueOutcome::DroppedOldest => Admission::Dropped(DropReason::OldestEvicted),
EnqueueOutcome::DroppedNewest => Admission::Dropped(DropReason::NewestDiscarded),
EnqueueOutcome::Refused => Admission::Dropped(DropReason::Refused),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SinkLifecycle {
Dormant,
Accepting,
Backpressured,
}
pub struct SinkCounters {
appended: AtomicU64,
drained: AtomicU64,
}
impl Default for SinkCounters {
fn default() -> Self {
Self::new()
}
}
impl SinkCounters {
#[must_use]
pub const fn new() -> Self {
Self {
appended: AtomicU64::new(0),
drained: AtomicU64::new(0),
}
}
pub fn record_append(&self) {
self.appended.fetch_add(1, Ordering::SeqCst);
}
pub fn record_drain(&self) {
self.drained.fetch_add(1, Ordering::SeqCst);
}
#[must_use]
pub fn appended(&self) -> u64 {
self.appended.load(Ordering::Relaxed)
}
#[must_use]
pub fn drained(&self) -> u64 {
self.drained.load(Ordering::Relaxed)
}
#[must_use]
pub fn is_quiescent(&self, queue_len: usize, total_drops: u64) -> bool {
let appended = self.appended.load(Ordering::SeqCst);
let drained = self.drained.load(Ordering::SeqCst);
queue_len == 0 && drained + total_drops >= appended
}
}
pub struct SinkFront<R: RingStorage> {
queue: BoundedQueue<R>,
armed: AtomicBool,
counters: SinkCounters,
}
impl<R: RingStorage> SinkFront<R> {
pub fn from_storage(storage: R, fail_mode: FailMode) -> Self {
Self {
queue: BoundedQueue::from_storage(storage, fail_mode),
armed: AtomicBool::new(false),
counters: SinkCounters::new(),
}
}
pub fn arm(&self) {
self.armed.store(true, Ordering::Release);
}
pub fn disarm(&self) {
self.armed.store(false, Ordering::Release);
}
#[must_use]
pub fn is_armed(&self) -> bool {
self.armed.load(Ordering::Acquire)
}
pub fn emit(&self, item: R::Item) -> Admission {
if !self.is_armed() {
return Admission::Dormant;
}
self.counters.record_append();
admission_of(self.queue.enqueue(item))
}
pub fn emit_lossless(&self, item: R::Item, mut assist: impl FnMut(&Self) -> bool) -> Admission {
if !self.is_armed() {
return Admission::Dormant;
}
self.counters.record_append();
match self.queue.enqueue_assisting(item, || assist(self)) {
Ok(()) => Admission::Accepted,
Err(item) => admission_of(self.queue.enqueue(item)),
}
}
#[must_use]
pub fn drain_one(&self) -> Option<R::Item> {
let item = self.queue.dequeue()?;
self.counters.record_drain();
Some(item)
}
#[must_use]
pub fn lifecycle(&self) -> SinkLifecycle {
if !self.is_armed() {
SinkLifecycle::Dormant
} else if self.queue.len() >= self.queue.capacity() {
SinkLifecycle::Backpressured
} else {
SinkLifecycle::Accepting
}
}
#[must_use]
pub fn is_quiescent(&self) -> bool {
self.counters
.is_quiescent(self.queue.len(), self.queue.dropped())
}
#[must_use]
pub fn dropped(&self) -> u64 {
self.queue.dropped()
}
#[must_use]
pub fn drained(&self) -> u64 {
self.counters.drained()
}
}
pub type StaticSinkFront<T, const N: usize> = SinkFront<StaticRing<T, N>>;
impl<T, const N: usize> SinkFront<StaticRing<T, N>> {
#[must_use]
pub fn new(fail_mode: FailMode) -> Self {
Self::from_storage(StaticRing::new(), fail_mode)
}
}
#[cfg(feature = "alloc")]
pub type HeapSinkFront<T> = SinkFront<Ring<T>>;
#[cfg(feature = "alloc")]
impl<T> SinkFront<Ring<T>> {
#[must_use]
pub fn new(capacity: usize, fail_mode: FailMode) -> Self {
Self::from_storage(Ring::with_capacity(capacity), fail_mode)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn dormant_emit_never_queues() {
let sink = StaticSinkFront::<u32, 8>::new(FailMode::DropOldest);
assert_eq!(sink.lifecycle(), SinkLifecycle::Dormant);
let outcome = sink.emit(42);
assert_eq!(outcome, Admission::Dormant);
assert!(!outcome.leaves_item_queued());
assert!(sink.drain_one().is_none());
}
#[test]
fn armed_emit_accepts_and_drains_to_quiescence() {
let sink = StaticSinkFront::<u32, 8>::new(FailMode::DropOldest);
sink.arm();
assert_eq!(sink.lifecycle(), SinkLifecycle::Accepting);
assert_eq!(sink.emit(7), Admission::Accepted);
assert_eq!(sink.drain_one(), Some(7));
assert!(sink.is_quiescent());
}
#[test]
fn full_static_sink_is_backpressured_and_drop_oldest_keeps_item() {
let sink = StaticSinkFront::<u32, 2>::new(FailMode::DropOldest);
sink.arm();
assert_eq!(sink.emit(1), Admission::Accepted);
assert_eq!(sink.emit(2), Admission::Accepted);
assert_eq!(sink.lifecycle(), SinkLifecycle::Backpressured);
let evicted = sink.emit(3);
assert_eq!(evicted, Admission::Dropped(DropReason::OldestEvicted));
assert!(evicted.leaves_item_queued());
assert_eq!(sink.drain_one(), Some(2));
assert_eq!(sink.drain_one(), Some(3));
}
#[test]
fn emit_lossless_drains_to_make_room_no_drop() {
let sink = StaticSinkFront::<u32, 2>::new(FailMode::FailClosed);
sink.arm();
assert_eq!(sink.emit(1), Admission::Accepted);
assert_eq!(sink.emit(2), Admission::Accepted);
let admission = sink.emit_lossless(3, |front| front.drain_one().is_some());
assert_eq!(admission, Admission::Accepted);
assert_eq!(sink.dropped(), 0, "producer-assist dropped nothing");
}
#[test]
fn emit_lossless_falls_back_to_policy_when_assist_gives_up() {
let sink = StaticSinkFront::<u32, 2>::new(FailMode::FailClosed);
sink.arm();
assert_eq!(sink.emit(1), Admission::Accepted);
assert_eq!(sink.emit(2), Admission::Accepted);
let admission = sink.emit_lossless(3, |_front| false);
assert_eq!(admission, Admission::Dropped(DropReason::Refused));
assert_eq!(sink.dropped(), 1);
}
#[cfg(feature = "alloc")]
#[test]
fn heap_tier_matches_static_behavior() {
let sink = HeapSinkFront::<u32>::new(2, FailMode::DropNewest);
sink.arm();
assert_eq!(sink.emit(1), Admission::Accepted);
assert_eq!(sink.emit(2), Admission::Accepted);
assert_eq!(
sink.emit(3),
Admission::Dropped(DropReason::NewestDiscarded)
);
assert_eq!(sink.drain_one(), Some(1));
assert_eq!(sink.drain_one(), Some(2));
}
}