use std::{
collections::{BTreeMap, HashMap, VecDeque},
sync::{Arc, Mutex},
};
use tokio::sync::Notify;
use crate::{Error, Frame, StreamId};
struct StreamSlot {
priority: u8,
frames: VecDeque<Frame>,
}
struct Inner {
bands: BTreeMap<u8, VecDeque<StreamId>>,
streams: HashMap<StreamId, StreamSlot>,
len: usize,
closed: bool,
}
impl Inner {
fn arm(&mut self, id: StreamId, band: u8) {
self.bands.entry(band).or_default().push_back(id);
}
}
#[derive(Clone)]
pub struct PriorityQueue {
inner: Arc<Mutex<Inner>>,
non_empty: Arc<Notify>,
has_space: Arc<Notify>,
capacity: usize,
}
impl PriorityQueue {
pub fn new(capacity: usize) -> Self {
Self {
inner: Arc::new(Mutex::new(Inner {
bands: BTreeMap::new(),
streams: HashMap::new(),
len: 0,
closed: false,
})),
non_empty: Arc::new(Notify::new()),
has_space: Arc::new(Notify::new()),
capacity,
}
}
pub async fn push(&self, priority: u8, id: StreamId, frame: Frame) -> Result<(), Error> {
loop {
let notified = self.has_space.notified();
{
let mut inner = self.inner.lock().unwrap();
if inner.closed {
return Err(Error::Closed);
}
if inner.len < self.capacity {
self.push_locked(&mut inner, priority, id, frame);
return Ok(());
}
}
notified.await;
}
}
pub fn push_now(&self, priority: u8, id: StreamId, frame: Frame) -> Result<(), Error> {
let mut inner = self.inner.lock().unwrap();
if inner.closed {
return Err(Error::Closed);
}
self.push_locked(&mut inner, priority, id, frame);
Ok(())
}
fn push_locked(&self, inner: &mut Inner, priority: u8, id: StreamId, frame: Frame) {
match inner.streams.get_mut(&id) {
Some(slot) => {
slot.frames.push_back(frame);
}
None => {
let mut frames = VecDeque::new();
frames.push_back(frame);
inner.streams.insert(id, StreamSlot { priority, frames });
inner.arm(id, priority);
}
}
inner.len += 1;
self.non_empty.notify_one();
}
pub async fn pop(&self) -> Option<Frame> {
loop {
let notified = self.non_empty.notified();
{
let mut inner = self.inner.lock().unwrap();
if let Some(frame) = self.pop_locked(&mut inner) {
return Some(frame);
}
if inner.closed {
return None;
}
}
notified.await;
}
}
fn pop_locked(&self, inner: &mut Inner) -> Option<Frame> {
let (&band, queue) = inner.bands.iter_mut().next_back()?;
let id = queue.pop_front().expect("scheduled band must be non-empty");
if queue.is_empty() {
inner.bands.remove(&band);
}
let slot = inner
.streams
.get_mut(&id)
.expect("scheduled stream must have a slot");
let frame = slot
.frames
.pop_front()
.expect("scheduled slot must be non-empty");
if slot.frames.is_empty() {
inner.streams.remove(&id);
} else {
let priority = slot.priority;
inner.arm(id, priority);
}
inner.len -= 1;
self.has_space.notify_one();
Some(frame)
}
pub fn set_priority(&self, id: StreamId, new: u8) {
let mut inner = self.inner.lock().unwrap();
let old = match inner.streams.get(&id) {
Some(slot) => slot.priority,
None => return,
};
if old == new {
return;
}
if let Some(queue) = inner.bands.get_mut(&old) {
if let Some(pos) = queue.iter().position(|&s| s == id) {
queue.remove(pos);
if queue.is_empty() {
inner.bands.remove(&old);
}
inner.bands.entry(new).or_default().push_back(id);
}
}
if let Some(slot) = inner.streams.get_mut(&id) {
slot.priority = new;
}
}
pub fn close(&self) {
{
let mut inner = self.inner.lock().unwrap();
inner.closed = true;
}
self.non_empty.notify_waiters();
self.has_space.notify_waiters();
}
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use crate::proto::Stream;
use crate::{StreamDir, StreamId};
fn sid(index: u64) -> StreamId {
StreamId::new(index, StreamDir::Uni, false)
}
fn frame(id: StreamId, tag: u8) -> Frame {
Frame::Stream(Stream {
id,
data: Bytes::copy_from_slice(&[tag]),
fin: false,
})
}
fn tag_of(frame: &Frame) -> u8 {
match frame {
Frame::Stream(s) => s.data[0],
_ => panic!("expected stream frame"),
}
}
fn id_of(frame: &Frame) -> StreamId {
match frame {
Frame::Stream(s) => s.id,
_ => panic!("expected stream frame"),
}
}
#[tokio::test]
async fn higher_priority_first() {
let q = PriorityQueue::new(8);
let lo = sid(0);
let hi = sid(1);
q.push(10, lo, frame(lo, b'l')).await.unwrap();
q.push(200, hi, frame(hi, b'h')).await.unwrap();
assert_eq!(tag_of(&q.pop().await.unwrap()), b'h');
assert_eq!(tag_of(&q.pop().await.unwrap()), b'l');
}
#[tokio::test]
async fn equal_priority_round_robin() {
let q = PriorityQueue::new(8);
let a = sid(0);
let b = sid(1);
q.push(5, a, frame(a, 1)).await.unwrap();
q.push(5, a, frame(a, 2)).await.unwrap();
q.push(5, b, frame(b, 1)).await.unwrap();
q.push(5, b, frame(b, 2)).await.unwrap();
assert_eq!(id_of(&q.pop().await.unwrap()), a);
assert_eq!(id_of(&q.pop().await.unwrap()), b);
assert_eq!(id_of(&q.pop().await.unwrap()), a);
assert_eq!(id_of(&q.pop().await.unwrap()), b);
}
#[tokio::test]
async fn per_stream_fifo_preserved() {
let q = PriorityQueue::new(8);
let a = sid(0);
for i in 0..4u8 {
q.push(5, a, frame(a, i)).await.unwrap();
}
for i in 0..4u8 {
assert_eq!(tag_of(&q.pop().await.unwrap()), i);
}
}
#[tokio::test]
async fn set_priority_moves_pointer_not_frames() {
let q = PriorityQueue::new(8);
let lo = sid(0);
let hi = sid(1);
q.push(10, lo, frame(lo, 1)).await.unwrap();
q.push(10, lo, frame(lo, 2)).await.unwrap();
q.push(20, hi, frame(hi, 1)).await.unwrap();
q.set_priority(lo, 100);
assert_eq!(id_of(&q.pop().await.unwrap()), lo);
assert_eq!(tag_of(&q.pop().await.unwrap()), 2); assert_eq!(id_of(&q.pop().await.unwrap()), hi);
}
#[tokio::test]
async fn set_priority_unknown_stream_is_noop() {
let q = PriorityQueue::new(8);
q.set_priority(sid(99), 50); }
#[tokio::test]
async fn close_unblocks_pop() {
let q = PriorityQueue::new(8);
let q2 = q.clone();
let handle = tokio::spawn(async move { q2.pop().await });
tokio::task::yield_now().await;
q.close();
assert!(handle.await.unwrap().is_none());
}
#[tokio::test]
async fn close_unblocks_push() {
let q = PriorityQueue::new(1);
let a = sid(0);
q.push(5, a, frame(a, 1)).await.unwrap();
let q2 = q.clone();
let handle = tokio::spawn(async move { q2.push(5, sid(1), frame(sid(1), 2)).await });
tokio::task::yield_now().await;
q.close();
assert!(matches!(handle.await.unwrap(), Err(Error::Closed)));
}
#[tokio::test]
async fn backpressure_blocks_at_capacity() {
let q = PriorityQueue::new(2);
let a = sid(0);
q.push(5, a, frame(a, 1)).await.unwrap();
q.push(5, a, frame(a, 2)).await.unwrap();
let q2 = q.clone();
let pushing = tokio::spawn(async move { q2.push(5, a, frame(a, 3)).await });
tokio::task::yield_now().await;
assert!(!pushing.is_finished(), "push should block while full");
q.pop().await.unwrap();
pushing.await.unwrap().unwrap();
}
}