use crate::context::Receiver;
use std::collections::VecDeque;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum Priority {
Low = 0,
#[default]
Normal = 1,
High = 2,
Critical = 3,
}
#[derive(Debug, Clone)]
pub struct PriorityQueueConfig {
pub max_per_priority: Option<usize>,
pub prevent_starvation: bool,
pub starvation_threshold: usize,
}
impl Default for PriorityQueueConfig {
fn default() -> Self {
Self {
max_per_priority: Some(1000),
prevent_starvation: true,
starvation_threshold: 100,
}
}
}
pub struct PriorityReceiver<T> {
sources: Vec<PrioritySource<T>>,
config: PriorityQueueConfig,
high_priority_count: usize,
}
struct PrioritySource<T> {
receiver: Receiver<T>,
priority: Priority,
buffer: VecDeque<T>,
}
impl<T> PriorityReceiver<T>
where
T: Send + 'static,
{
pub fn new() -> Self {
Self::with_config(PriorityQueueConfig::default())
}
pub fn with_config(config: PriorityQueueConfig) -> Self {
Self {
sources: Vec::new(),
config,
high_priority_count: 0,
}
}
pub fn add_source(&mut self, receiver: Receiver<T>, priority: Priority) {
self.sources.push(PrioritySource {
receiver,
priority,
buffer: VecDeque::new(),
});
}
pub async fn recv(&mut self) -> Option<(T, Priority)> {
if self.sources.is_empty() {
return None;
}
loop {
self.try_fill_buffers();
if self.config.prevent_starvation && self.high_priority_count >= self.config.starvation_threshold {
if let Some((msg, prio)) = self.take_lower_priority_message() {
self.high_priority_count = 0;
return Some((msg, prio));
}
}
if let Some((msg, prio)) = self.take_highest_priority_message() {
if prio >= Priority::High {
self.high_priority_count += 1;
}
return Some((msg, prio));
}
if !self.wait_for_message().await {
return None;
}
}
}
fn try_fill_buffers(&mut self) {
for source in &mut self.sources {
let max_size = self.config.max_per_priority.unwrap_or(usize::MAX);
while source.buffer.len() < max_size {
match source.receiver.try_recv() {
Ok(msg) => source.buffer.push_back(msg),
Err(_) => break, }
}
}
}
async fn wait_for_message(&mut self) -> bool {
if self.sources.is_empty() {
return false;
}
loop {
for source in &mut self.sources {
if let Some(msg) = source.receiver.recv().await {
source.buffer.push_back(msg);
return true;
}
}
self.sources.retain(|s| !s.buffer.is_empty() || !s.receiver.is_empty());
if self.sources.is_empty() {
return false;
}
}
}
fn take_highest_priority_message(&mut self) -> Option<(T, Priority)> {
let mut best: Option<(usize, Priority)> = None;
for (idx, source) in self.sources.iter().enumerate() {
if source.buffer.is_empty() {
continue;
}
match best {
None => best = Some((idx, source.priority)),
Some((_, best_priority)) if source.priority > best_priority => {
best = Some((idx, source.priority));
}
_ => {}
}
}
best.and_then(|(idx, _)| {
self.sources[idx]
.buffer
.pop_front()
.map(|msg| (msg, self.sources[idx].priority))
})
}
fn take_lower_priority_message(&mut self) -> Option<(T, Priority)> {
let mut best: Option<(usize, Priority)> = None;
for (idx, source) in self.sources.iter().enumerate() {
if source.buffer.is_empty() {
continue;
}
match best {
None => best = Some((idx, source.priority)),
Some((_, best_priority)) if source.priority < best_priority => {
best = Some((idx, source.priority));
}
_ => {}
}
}
best.and_then(|(idx, _)| {
self.sources[idx]
.buffer
.pop_front()
.map(|msg| (msg, self.sources[idx].priority))
})
}
pub fn stats(&self) -> PriorityQueueStats {
let mut counts_by_priority = [0; 4];
let mut total_buffered = 0;
for source in &self.sources {
let idx = source.priority as usize;
counts_by_priority[idx] += source.buffer.len();
total_buffered += source.buffer.len();
}
PriorityQueueStats {
total_buffered,
critical_buffered: counts_by_priority[Priority::Critical as usize],
high_buffered: counts_by_priority[Priority::High as usize],
normal_buffered: counts_by_priority[Priority::Normal as usize],
low_buffered: counts_by_priority[Priority::Low as usize],
source_count: self.sources.len(),
}
}
}
impl<T> Default for PriorityReceiver<T>
where
T: Send + 'static,
{
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_source(priority: Priority, messages: Vec<u32>) -> PrioritySource<u32> {
let (_tx, rx) = tokio::sync::mpsc::unbounded_channel();
PrioritySource {
receiver: Receiver::from(rx),
priority,
buffer: messages.into_iter().collect(),
}
}
#[test]
fn take_highest_priority_message_selects_all_low_messages() {
let mut prio_rx: PriorityReceiver<u32> = PriorityReceiver::new();
prio_rx.sources.push(make_source(Priority::Low, vec![1, 2, 3]));
let (msg, priority) = prio_rx
.take_highest_priority_message()
.expect("a Low-priority message should still be returned when it's the only one buffered");
assert_eq!(msg, 1);
assert_eq!(priority, Priority::Low);
assert_eq!(prio_rx.take_highest_priority_message(), Some((2, Priority::Low)));
assert_eq!(prio_rx.take_highest_priority_message(), Some((3, Priority::Low)));
assert_eq!(prio_rx.take_highest_priority_message(), None);
}
#[test]
fn take_lower_priority_message_selects_all_critical_messages() {
let mut prio_rx: PriorityReceiver<u32> = PriorityReceiver::new();
prio_rx.sources.push(make_source(Priority::Critical, vec![10, 20]));
let (msg, priority) = prio_rx
.take_lower_priority_message()
.expect("a Critical-priority message should still be returned when it's the only one buffered");
assert_eq!(msg, 10);
assert_eq!(priority, Priority::Critical);
assert_eq!(prio_rx.take_lower_priority_message(), Some((20, Priority::Critical)));
assert_eq!(prio_rx.take_lower_priority_message(), None);
}
#[tokio::test]
async fn recv_delivers_messages_from_a_low_priority_only_source() {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<u32>();
let mut prio_rx: PriorityReceiver<u32> = PriorityReceiver::new();
prio_rx.add_source(Receiver::from(rx), Priority::Low);
tx.send(42).unwrap();
drop(tx);
let result = tokio::time::timeout(std::time::Duration::from_secs(5), prio_rx.recv())
.await
.expect("recv() should not hang starving a Low-only source");
assert_eq!(result, Some((42, Priority::Low)));
}
#[test]
fn take_highest_priority_message_prefers_higher_priority() {
let mut prio_rx: PriorityReceiver<u32> = PriorityReceiver::new();
prio_rx.sources.push(make_source(Priority::Low, vec![1]));
prio_rx.sources.push(make_source(Priority::Critical, vec![2]));
assert_eq!(prio_rx.take_highest_priority_message(), Some((2, Priority::Critical)));
}
}
#[derive(Debug, Clone)]
pub struct PriorityQueueStats {
pub total_buffered: usize,
pub critical_buffered: usize,
pub high_buffered: usize,
pub normal_buffered: usize,
pub low_buffered: usize,
pub source_count: usize,
}