use std::collections::{BTreeMap, HashMap};
use sozu_command::logging::ansi_palette;
use super::{GlobalStreamId, StreamId, parser};
#[derive(Default)]
pub struct Prioriser {
priorities: HashMap<StreamId, (u8, bool)>,
incremental_cursor: [StreamId; URGENCY_LEVELS],
}
const DEFAULT_URGENCY: u8 = 3;
const MAX_PRIORITIES: usize = 4096;
const PRIORITY_IDLE_LOOKAHEAD: u32 = 64;
impl Prioriser {
pub fn push_priority(&mut self, stream_id: StreamId, priority: parser::PriorityPart) -> bool {
trace!(
"{} PRIORITY REQUEST FOR {}: {:?}",
log_module_context!(),
stream_id,
priority
);
debug_assert!(
self.priorities.len() <= MAX_PRIORITIES,
"priority map must never exceed MAX_PRIORITIES entries"
);
if !self.priorities.contains_key(&stream_id) && self.priorities.len() >= MAX_PRIORITIES {
return false;
}
match priority {
parser::PriorityPart::Rfc7540 {
stream_dependency,
weight: _,
} => {
stream_dependency.stream_id == stream_id
}
parser::PriorityPart::Rfc9218 {
urgency,
incremental,
} => {
self.priorities
.insert(stream_id, (urgency.min(7), incremental));
debug_assert!(
self.priorities
.get(&stream_id)
.is_some_and(|(u, _)| *u <= 7),
"stored RFC 9218 urgency must be clamped to [0, 7]"
);
debug_assert!(
self.priorities.len() <= MAX_PRIORITIES,
"priority map must stay within MAX_PRIORITIES after insert"
);
false
}
}
}
pub fn push_priority_guarded(
&mut self,
stream_id: StreamId,
priority: parser::PriorityPart,
last_stream_id: StreamId,
open_streams: &BTreeMap<StreamId, GlobalStreamId>,
) -> bool {
if !self.is_acceptable(stream_id, last_stream_id, open_streams) {
trace!(
"{} PRIORITY dropped for unknown/far stream {} (last_stream_id={})",
log_module_context!(),
stream_id,
last_stream_id
);
return false;
}
self.push_priority(stream_id, priority)
}
fn is_acceptable(
&self,
stream_id: StreamId,
last_stream_id: StreamId,
open_streams: &BTreeMap<StreamId, GlobalStreamId>,
) -> bool {
if open_streams.contains_key(&stream_id) {
return true;
}
let upper = last_stream_id.saturating_add(PRIORITY_IDLE_LOOKAHEAD);
stream_id > last_stream_id && stream_id <= upper
}
pub fn remove(&mut self, stream_id: &StreamId) {
let had = self.priorities.contains_key(stream_id);
let before = self.priorities.len();
self.priorities.remove(stream_id);
debug_assert!(
!self.priorities.contains_key(stream_id),
"remove must evict the priority entry"
);
debug_assert_eq!(
self.priorities.len(),
before - had as usize,
"priority map length drops by exactly one iff the id was present"
);
}
#[inline]
pub fn get(&self, stream_id: &StreamId) -> (u8, bool) {
self.priorities
.get(stream_id)
.copied()
.unwrap_or((DEFAULT_URGENCY, false))
}
pub fn apply_incremental_rotation(&self, buf: &mut [StreamId]) -> usize {
#[cfg(debug_assertions)]
debug_assert!(
buf.windows(2)
.all(|w| self.get(&w[0]).0 <= self.get(&w[1]).0),
"apply_incremental_rotation requires input pre-sorted by urgency"
);
let len_before = buf.len();
#[cfg(debug_assertions)]
let expected_incremental = buf.iter().filter(|id| self.get(id).1).count();
let mut total_incremental = 0usize;
let mut i = 0;
while i < buf.len() {
let (urgency_i, _) = self.get(&buf[i]);
let mut j = i + 1;
while j < buf.len() {
let (urgency_j, _) = self.get(&buf[j]);
if urgency_j != urgency_i {
break;
}
j += 1;
}
let cursor = self.incremental_cursor[bucket(urgency_i)];
let bucket = &mut buf[i..j];
if bucket.len() > 1 {
bucket.sort_by_key(|id| self.get(id).1);
let split = bucket.partition_point(|id| !self.get(id).1);
let incremental_tail = &mut bucket[split..];
if incremental_tail.len() > 1 {
let start = incremental_tail.partition_point(|id| *id <= cursor);
incremental_tail.rotate_left(start);
}
total_incremental += incremental_tail.len();
} else if bucket.len() == 1 && self.get(&bucket[0]).1 {
total_incremental += 1;
}
i = j;
}
debug_assert_eq!(
buf.len(),
len_before,
"rotation must preserve the slice (no streams dropped or added)"
);
#[cfg(debug_assertions)]
debug_assert_eq!(
total_incremental, expected_incremental,
"reported incremental count must equal the incremental streams in buf"
);
total_incremental
}
pub fn advance_incremental_cursor(
&mut self,
first_incremental_fired: &[Option<StreamId>; URGENCY_LEVELS],
) {
for (cursor, leader) in self
.incremental_cursor
.iter_mut()
.zip(first_incremental_fired.iter())
{
if let Some(id) = leader {
*cursor = *id;
}
}
}
}
const MAX_URGENCY: u8 = 7;
const URGENCY_LEVELS: usize = MAX_URGENCY as usize + 1;
#[inline]
fn bucket(urgency: u8) -> usize {
debug_assert!(
urgency <= MAX_URGENCY,
"urgency must be clamped to [0, MAX_URGENCY] before it reaches the census"
);
usize::from(urgency.min(MAX_URGENCY))
}
pub(super) struct ReadyIncrementalCensus {
ready: [usize; URGENCY_LEVELS],
incremental_count: usize,
first_incremental_fired: [Option<StreamId>; URGENCY_LEVELS],
}
impl ReadyIncrementalCensus {
pub(super) fn incremental_peer_count(&self, urgency: u8) -> usize {
self.ready[bucket(urgency)]
}
pub(super) fn note_ineligible(&mut self, urgency: u8, is_incremental: bool) {
if !is_incremental {
return;
}
let slot = &mut self.ready[bucket(urgency)];
*slot = slot.saturating_sub(1);
}
pub(super) fn note_fired(
&mut self,
urgency: u8,
stream_id: StreamId,
is_incremental: bool,
consumed: i32,
) {
if !is_incremental || consumed <= 0 {
return;
}
let slot = &mut self.first_incremental_fired[bucket(urgency)];
if slot.is_none() {
*slot = Some(stream_id);
}
}
pub(super) fn ready_total(&self) -> usize {
self.ready.iter().sum()
}
pub(super) fn incremental_count(&self) -> usize {
self.incremental_count
}
pub(super) fn ready_buckets(&self) -> &[usize; URGENCY_LEVELS] {
&self.ready
}
}
#[derive(Default)]
pub(super) struct H2Scheduler {
prioriser: Prioriser,
order: Vec<StreamId>,
}
impl H2Scheduler {
#[inline]
pub(super) fn priority(&self, stream_id: &StreamId) -> (u8, bool) {
self.prioriser.get(stream_id)
}
pub(super) fn push_priority(
&mut self,
stream_id: StreamId,
priority: parser::PriorityPart,
) -> bool {
self.prioriser.push_priority(stream_id, priority)
}
pub(super) fn push_priority_guarded(
&mut self,
stream_id: StreamId,
priority: parser::PriorityPart,
last_stream_id: StreamId,
open_streams: &BTreeMap<StreamId, GlobalStreamId>,
) -> bool {
self.prioriser
.push_priority_guarded(stream_id, priority, last_stream_id, open_streams)
}
pub(super) fn remove_stream(&mut self, stream_id: &StreamId) {
self.prioriser.remove(stream_id);
}
pub(super) fn prioriser_mut(&mut self) -> &mut Prioriser {
&mut self.prioriser
}
pub(super) fn begin_pass<I, F>(
&mut self,
stream_ids: I,
mut ready: F,
) -> (Vec<StreamId>, ReadyIncrementalCensus)
where
I: IntoIterator<Item = StreamId>,
F: FnMut(StreamId) -> bool,
{
let mut order = std::mem::take(&mut self.order);
order.clear();
order.extend(stream_ids);
order.sort_by_cached_key(|id| {
let (urgency, _) = self.prioriser.get(id);
(urgency, *id)
});
let incremental_count = self.prioriser.apply_incremental_rotation(&mut order);
let mut census = ReadyIncrementalCensus {
ready: [0; URGENCY_LEVELS],
incremental_count,
first_incremental_fired: [None; URGENCY_LEVELS],
};
for &stream_id in order.iter() {
let (urgency, is_incremental) = self.prioriser.get(&stream_id);
if !is_incremental {
continue;
}
if ready(stream_id) {
census.ready[bucket(urgency)] += 1;
}
}
(order, census)
}
pub(super) fn end_pass(&mut self, order: Vec<StreamId>, census: ReadyIncrementalCensus) {
self.order = order;
self.prioriser
.advance_incremental_cursor(&census.first_incremental_fired);
}
pub(super) fn reclaim_idle_buffer(&mut self, retain_size: usize) {
if self.order.capacity() > retain_size * 4 {
self.order.shrink_to(retain_size);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prioriser_defaults_for_unknown_stream() {
let p = Prioriser::default();
assert_eq!(p.get(&1), (3, false));
assert_eq!(p.get(&999), (3, false));
}
#[test]
fn test_prioriser_push_rfc9218_and_get() {
let mut p = Prioriser::default();
let invalid = p.push_priority(
1,
parser::PriorityPart::Rfc9218 {
urgency: 0,
incremental: true,
},
);
assert!(!invalid);
assert_eq!(p.get(&1), (0, true));
let invalid = p.push_priority(
3,
parser::PriorityPart::Rfc9218 {
urgency: 7,
incremental: false,
},
);
assert!(!invalid);
assert_eq!(p.get(&3), (7, false));
}
#[test]
fn test_prioriser_urgency_clamped_to_7() {
let mut p = Prioriser::default();
p.push_priority(
1,
parser::PriorityPart::Rfc9218 {
urgency: 255,
incremental: false,
},
);
assert_eq!(p.get(&1), (7, false));
}
#[test]
fn test_prioriser_update_priority() {
let mut p = Prioriser::default();
p.push_priority(
1,
parser::PriorityPart::Rfc9218 {
urgency: 3,
incremental: false,
},
);
assert_eq!(p.get(&1), (3, false));
p.push_priority(
1,
parser::PriorityPart::Rfc9218 {
urgency: 1,
incremental: true,
},
);
assert_eq!(p.get(&1), (1, true));
}
#[test]
fn test_prioriser_remove() {
let mut p = Prioriser::default();
p.push_priority(
1,
parser::PriorityPart::Rfc9218 {
urgency: 0,
incremental: true,
},
);
assert_eq!(p.get(&1), (0, true));
p.remove(&1);
assert_eq!(p.get(&1), (3, false));
}
#[test]
fn test_prioriser_rfc7540_self_dependency() {
let mut p = Prioriser::default();
let invalid = p.push_priority(
5,
parser::PriorityPart::Rfc7540 {
stream_dependency: parser::StreamDependency {
exclusive: false,
stream_id: 5, },
weight: 16,
},
);
assert!(invalid);
}
#[test]
fn test_prioriser_rfc7540_valid_dependency() {
let mut p = Prioriser::default();
let invalid = p.push_priority(
5,
parser::PriorityPart::Rfc7540 {
stream_dependency: parser::StreamDependency {
exclusive: false,
stream_id: 3, },
weight: 16,
},
);
assert!(!invalid);
assert_eq!(p.get(&5), (3, false));
}
#[test]
fn test_prioriser_max_entries_cap() {
let mut p = Prioriser::default();
for i in 0..MAX_PRIORITIES as u32 {
let stream_id = i * 2 + 1; p.push_priority(
stream_id,
parser::PriorityPart::Rfc9218 {
urgency: (i % 8) as u8,
incremental: false,
},
);
}
let next_id = (MAX_PRIORITIES as u32) * 2 + 1;
let invalid = p.push_priority(
next_id,
parser::PriorityPart::Rfc9218 {
urgency: 0,
incremental: true,
},
);
assert!(!invalid); assert_eq!(p.get(&next_id), (3, false)); }
#[test]
fn test_prioriser_update_existing_at_cap() {
let mut p = Prioriser::default();
for i in 0..MAX_PRIORITIES as u32 {
p.push_priority(
i * 2 + 1,
parser::PriorityPart::Rfc9218 {
urgency: 3,
incremental: false,
},
);
}
p.push_priority(
1,
parser::PriorityPart::Rfc9218 {
urgency: 0,
incremental: true,
},
);
assert_eq!(p.get(&1), (0, true));
}
#[test]
fn test_prioriser_guarded_accepts_open_stream() {
let mut p = Prioriser::default();
let mut open: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
open.insert(3, 0);
let invalid = p.push_priority_guarded(
3,
parser::PriorityPart::Rfc9218 {
urgency: 1,
incremental: false,
},
7,
&open,
);
assert!(!invalid);
assert_eq!(p.get(&3), (1, false));
}
#[test]
fn test_prioriser_guarded_accepts_idle_lookahead() {
let mut p = Prioriser::default();
let open: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
let invalid = p.push_priority_guarded(
105,
parser::PriorityPart::Rfc9218 {
urgency: 2,
incremental: true,
},
99,
&open,
);
assert!(!invalid);
assert_eq!(p.get(&105), (2, true));
}
#[test]
fn test_prioriser_guarded_drops_far_future_stream() {
let mut p = Prioriser::default();
let open: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
let invalid = p.push_priority_guarded(
1_000_001,
parser::PriorityPart::Rfc9218 {
urgency: 0,
incremental: false,
},
3,
&open,
);
assert!(!invalid); assert_eq!(p.get(&1_000_001), (DEFAULT_URGENCY, false));
}
#[test]
fn test_prioriser_guarded_drops_closed_past_stream() {
let mut p = Prioriser::default();
let open: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
let invalid = p.push_priority_guarded(
3,
parser::PriorityPart::Rfc9218 {
urgency: 5,
incremental: false,
},
99,
&open,
);
assert!(!invalid);
assert_eq!(p.get(&3), (DEFAULT_URGENCY, false));
}
#[test]
fn test_prioriser_guarded_cannot_flood_with_far_ids() {
let mut p = Prioriser::default();
let open: BTreeMap<StreamId, GlobalStreamId> = BTreeMap::new();
for delta in 10_000..(10_000 + MAX_PRIORITIES as u32) {
p.push_priority_guarded(
delta,
parser::PriorityPart::Rfc9218 {
urgency: 0,
incremental: false,
},
0,
&open,
);
}
assert_eq!(p.priorities.len(), 0);
}
fn commit_bucket_leader(p: &mut Prioriser, urgency: u8, stream_id: StreamId) {
let mut leaders = [None; URGENCY_LEVELS];
leaders[bucket(urgency)] = Some(stream_id);
p.advance_incremental_cursor(&leaders);
}
fn set_prio(p: &mut Prioriser, stream_id: StreamId, urgency: u8, incremental: bool) {
p.push_priority(
stream_id,
parser::PriorityPart::Rfc9218 {
urgency,
incremental,
},
);
}
#[test]
fn test_apply_incremental_rotation_all_non_incremental_is_noop() {
let mut p = Prioriser::default();
set_prio(&mut p, 1, 3, false);
set_prio(&mut p, 3, 3, false);
set_prio(&mut p, 5, 3, false);
let mut buf = vec![1u32, 3, 5];
let count = p.apply_incremental_rotation(&mut buf);
assert_eq!(count, 0);
assert_eq!(buf, vec![1, 3, 5]);
}
#[test]
fn test_apply_incremental_rotation_moves_incremental_to_tail() {
let mut p = Prioriser::default();
set_prio(&mut p, 1, 3, true);
set_prio(&mut p, 3, 3, false);
set_prio(&mut p, 5, 3, true);
set_prio(&mut p, 7, 3, false);
let mut buf = vec![1u32, 3, 5, 7];
let count = p.apply_incremental_rotation(&mut buf);
assert_eq!(count, 2);
assert_eq!(buf, vec![3, 7, 1, 5]);
}
#[test]
fn test_apply_incremental_rotation_respects_urgency_buckets() {
let mut p = Prioriser::default();
set_prio(&mut p, 1, 0, true); set_prio(&mut p, 3, 3, false); set_prio(&mut p, 5, 3, true); set_prio(&mut p, 7, 5, false);
let mut buf = vec![1u32, 3, 5, 7];
let count = p.apply_incremental_rotation(&mut buf);
assert_eq!(count, 2);
assert_eq!(buf, vec![1, 3, 5, 7]);
}
#[test]
fn test_apply_incremental_rotation_rotates_by_cursor() {
let mut p = Prioriser::default();
set_prio(&mut p, 1, 3, true);
set_prio(&mut p, 3, 3, true);
set_prio(&mut p, 5, 3, true);
let base = vec![1u32, 3, 5];
let mut buf = base.clone();
assert_eq!(p.apply_incremental_rotation(&mut buf), 3);
assert_eq!(buf, vec![1, 3, 5]);
commit_bucket_leader(&mut p, 3, 1);
let mut buf = base.clone();
assert_eq!(p.apply_incremental_rotation(&mut buf), 3);
assert_eq!(buf, vec![3, 5, 1]);
commit_bucket_leader(&mut p, 3, 3);
let mut buf = base.clone();
assert_eq!(p.apply_incremental_rotation(&mut buf), 3);
assert_eq!(buf, vec![5, 1, 3]);
commit_bucket_leader(&mut p, 3, 5);
let mut buf = base;
assert_eq!(p.apply_incremental_rotation(&mut buf), 3);
assert_eq!(buf, vec![1, 3, 5]);
}
#[test]
fn test_apply_incremental_rotation_cursor_unknown_id() {
let mut p = Prioriser::default();
set_prio(&mut p, 3, 3, true);
set_prio(&mut p, 5, 3, true);
set_prio(&mut p, 7, 3, true);
commit_bucket_leader(&mut p, 3, 4);
let mut buf = vec![3u32, 5, 7];
assert_eq!(p.apply_incremental_rotation(&mut buf), 3);
assert_eq!(buf, vec![5, 7, 3]);
}
#[test]
fn test_apply_incremental_rotation_single_stream_buckets() {
let mut p = Prioriser::default();
set_prio(&mut p, 1, 1, true);
set_prio(&mut p, 3, 2, false);
set_prio(&mut p, 5, 3, true);
let mut buf = vec![1u32, 3, 5];
let count = p.apply_incremental_rotation(&mut buf);
assert_eq!(count, 2);
assert_eq!(buf, vec![1, 3, 5]);
}
#[test]
fn test_advance_incremental_cursor_none_is_noop() {
let mut p = Prioriser::default();
commit_bucket_leader(&mut p, 3, 5);
p.advance_incremental_cursor(&[None; URGENCY_LEVELS]);
assert_eq!(p.incremental_cursor[bucket(3)], 5);
}
#[test]
fn test_advance_incremental_cursor_is_per_bucket() {
let mut p = Prioriser::default();
let mut leaders = [None; URGENCY_LEVELS];
leaders[bucket(0)] = Some(1);
leaders[bucket(3)] = Some(7);
p.advance_incremental_cursor(&leaders);
assert_eq!(
p.incremental_cursor[bucket(0)],
1,
"urgency 0 keeps its own leader"
);
assert_eq!(
p.incremental_cursor[bucket(3)],
7,
"urgency 3 keeps its own leader"
);
assert_eq!(
p.incremental_cursor[bucket(5)],
0,
"a bucket nobody led keeps its sentinel"
);
}
#[test]
fn test_apply_incremental_rotation_mixed_bucket_with_cursor() {
let mut p = Prioriser::default();
set_prio(&mut p, 1, 3, true);
set_prio(&mut p, 3, 3, false);
set_prio(&mut p, 5, 3, true);
set_prio(&mut p, 7, 3, false);
set_prio(&mut p, 9, 3, true);
commit_bucket_leader(&mut p, 3, 5);
let mut buf = vec![1u32, 3, 5, 7, 9];
let count = p.apply_incremental_rotation(&mut buf);
assert_eq!(count, 3);
assert_eq!(buf, vec![3, 7, 9, 1, 5]);
}
fn scheduler_with(entries: &[(StreamId, u8, bool)]) -> H2Scheduler {
let mut scheduler = H2Scheduler::default();
for &(stream_id, urgency, incremental) in entries {
scheduler.push_priority(
stream_id,
parser::PriorityPart::Rfc9218 {
urgency,
incremental,
},
);
}
scheduler
}
fn drive_leaders(
scheduler: &mut H2Scheduler,
ids: &[StreamId],
passes: usize,
urgency: u8,
) -> Vec<Option<StreamId>> {
let mut leaders = Vec::with_capacity(passes);
for _ in 0..passes {
let (order, mut census) = scheduler.begin_pass(ids.iter().copied(), |_| true);
for &stream_id in &order {
let (stream_urgency, is_incremental) = scheduler.priority(&stream_id);
census.note_fired(stream_urgency, stream_id, is_incremental, 1);
}
leaders.push(census.first_incremental_fired[bucket(urgency)]);
scheduler.end_pass(order, census);
}
leaders
}
#[test]
fn pass_order_is_ascending_urgency_then_id_with_incrementals_at_the_bucket_tail() {
let mut scheduler = scheduler_with(&[
(1, 3, true),
(3, 0, false),
(5, 3, false),
(7, 0, true),
(9, 3, false),
]);
let (order, _) = scheduler.begin_pass([9u32, 1, 7, 5, 3], |_| true);
assert_eq!(
order,
vec![3, 7, 5, 9, 1],
"urgency 0 before urgency 3; inside each bucket non-incremental \
first in ascending id order, then the incremental tail"
);
}
#[test]
fn incremental_leadership_rotates_one_position_per_pass() {
let mut scheduler =
scheduler_with(&[(1, 3, true), (3, 3, true), (5, 3, true), (7, 3, true)]);
let leaders = drive_leaders(&mut scheduler, &[1, 3, 5, 7], 8, 3);
assert_eq!(
leaders,
vec![
Some(1),
Some(3),
Some(5),
Some(7),
Some(1),
Some(3),
Some(5),
Some(7)
],
"leadership must advance exactly one position per pass and wrap"
);
}
#[test]
fn no_incremental_peer_is_starved_over_a_full_cycle() {
let ids = [1u32, 3, 5, 7, 9];
let mut scheduler = scheduler_with(&ids.map(|id| (id, 3, true)));
let leaders = drive_leaders(&mut scheduler, &ids, 10, 3);
for id in ids {
let led = leaders.iter().filter(|l| **l == Some(id)).count();
assert_eq!(
led, 2,
"stream {id} must lead 2 of 10 passes, led {led}: {leaders:?}"
);
}
}
#[test]
fn a_window_blocked_stream_does_not_take_the_lead() {
let mut scheduler = scheduler_with(&[(1, 3, true), (3, 3, true), (5, 3, true)]);
let (order, mut census) = scheduler.begin_pass([1u32, 3, 5], |_| true);
assert_eq!(order, vec![1, 3, 5]);
census.note_fired(3, 1, true, 0);
census.note_fired(3, 3, true, 1);
census.note_fired(3, 5, true, 1);
assert_eq!(
census.first_incremental_fired[bucket(3)],
Some(3),
"the lead belongs to the first incremental stream that moved bytes"
);
}
#[test]
fn a_pass_with_no_incremental_progress_leaves_the_cursor_alone() {
let mut scheduler = scheduler_with(&[(1, 3, true), (3, 3, true)]);
let leaders = drive_leaders(&mut scheduler, &[1, 3], 1, 3);
assert_eq!(leaders, vec![Some(1)]);
let (order, census) = scheduler.begin_pass([1u32, 3], |_| true);
assert_eq!(order, vec![3, 1], "pass two starts after the pass-one lead");
assert_eq!(census.first_incremental_fired[bucket(3)], None);
scheduler.end_pass(order, census);
assert_eq!(
scheduler.prioriser.incremental_cursor[bucket(3)],
1,
"a pass with no incremental progress must not move the cursor"
);
}
#[test]
fn every_urgency_bucket_rotates_its_own_incremental_tail() {
let mut scheduler =
scheduler_with(&[(1, 0, true), (3, 0, true), (5, 3, true), (7, 3, true)]);
let mut orders = Vec::new();
for _ in 0..6 {
let (order, mut census) = scheduler.begin_pass([1u32, 3, 5, 7], |_| true);
for &stream_id in &order {
let (urgency, is_incremental) = scheduler.priority(&stream_id);
census.note_fired(urgency, stream_id, is_incremental, 1);
}
orders.push(order.clone());
scheduler.end_pass(order, census);
}
assert_eq!(
orders,
vec![
vec![1, 3, 5, 7],
vec![3, 1, 7, 5],
vec![1, 3, 5, 7],
vec![3, 1, 7, 5],
vec![1, 3, 5, 7],
vec![3, 1, 7, 5],
],
"every urgency bucket rotates its own incremental tail: u=0 \
alternates `1, 3` / `3, 1` and u=3 alternates `5, 7` / `7, 5`"
);
}
#[test]
fn the_ready_census_is_scoped_to_one_urgency_bucket() {
let mut scheduler =
scheduler_with(&[(1, 0, true), (3, 7, true), (5, 7, true), (7, 3, false)]);
let (_, census) = scheduler.begin_pass([1u32, 3, 5, 7], |_| true);
assert_eq!(
census.incremental_peer_count(0),
1,
"the solo u=0 incremental stream has no peer to interleave with"
);
assert_eq!(census.incremental_peer_count(7), 2);
assert_eq!(
census.incremental_peer_count(3),
0,
"a non-incremental stream is never a peer"
);
assert_eq!(census.ready_total(), 3);
assert_eq!(
census.incremental_count(),
3,
"the traced incremental count covers every bucket"
);
}
#[test]
fn an_unready_incremental_stream_is_not_counted_as_a_peer() {
let mut scheduler = scheduler_with(&[(1, 3, true), (3, 3, true), (5, 3, true)]);
let (_, census) = scheduler.begin_pass([1u32, 3, 5], |id| id != 3);
assert_eq!(census.incremental_peer_count(3), 2);
assert_eq!(
census.incremental_count(),
3,
"the rotation still placed all three; only the READY census drops one"
);
}
#[test]
fn the_readiness_projection_runs_for_incremental_streams_only() {
let mut scheduler = scheduler_with(&[(1, 3, true), (3, 3, false), (5, 3, false)]);
let mut consulted = Vec::new();
let (_, _census) = scheduler.begin_pass([1u32, 3, 5], |id| {
consulted.push(id);
true
});
assert_eq!(
consulted,
vec![1],
"only the incremental stream's readiness may be projected"
);
}
#[test]
fn note_ineligible_reduces_only_its_own_bucket() {
let mut census = ReadyIncrementalCensus {
ready: [0, 3, 0, 2, 0, 0, 0, 0],
incremental_count: 5,
first_incremental_fired: [None; URGENCY_LEVELS],
};
census.note_ineligible(1, true);
assert_eq!(census.incremental_peer_count(1), 2, "urgency-1 drops to 2");
assert_eq!(census.incremental_peer_count(3), 2, "urgency-3 untouched");
assert_eq!(census.ready_total(), 4);
}
#[test]
fn note_ineligible_saturates_at_zero() {
let mut census = ReadyIncrementalCensus {
ready: [0; URGENCY_LEVELS],
incremental_count: 0,
first_incremental_fired: [None; URGENCY_LEVELS],
};
census.note_ineligible(0, true);
assert_eq!(
census.incremental_peer_count(0),
0,
"an empty bucket must saturate rather than wrap"
);
}
#[test]
fn note_ineligible_is_a_no_op_for_a_non_incremental_stream() {
let mut census = ReadyIncrementalCensus {
ready: [0, 3, 0, 0, 0, 0, 0, 0],
incremental_count: 3,
first_incremental_fired: [None; URGENCY_LEVELS],
};
census.note_ineligible(1, false);
assert_eq!(
census.incremental_peer_count(1),
3,
"a non-incremental stream was never counted, so it cannot leave"
);
}
#[test]
fn end_pass_returns_the_order_buffer_for_reuse() {
let mut scheduler = scheduler_with(&[(1, 3, false), (3, 3, false)]);
let (order, census) = scheduler.begin_pass([1u32, 3], |_| true);
assert!(
scheduler.order.is_empty(),
"the scheduler holds an empty buffer while a pass is live"
);
scheduler.end_pass(order, census);
assert_eq!(scheduler.order, vec![1, 3]);
assert!(scheduler.order.capacity() >= 2);
}
#[test]
fn reclaim_idle_buffer_shrinks_an_oversized_order_buffer() {
let mut scheduler = H2Scheduler {
order: Vec::with_capacity(4096),
..H2Scheduler::default()
};
scheduler.reclaim_idle_buffer(16);
assert!(scheduler.order.capacity() <= 4096, "capacity must not grow");
assert!(
scheduler.order.capacity() < 4096,
"a buffer past 4x the retain size must shrink"
);
let before = scheduler.order.capacity();
scheduler.reclaim_idle_buffer(16);
assert_eq!(
scheduler.order.capacity(),
before,
"reclaim is idempotent once the buffer is within budget"
);
}
mod fairness_property {
use quickcheck::{Arbitrary, Gen, TestResult, quickcheck};
use super::*;
#[derive(Debug, Clone)]
struct RotationPlan {
incremental: Vec<StreamId>,
urgency: u8,
trailing: Option<(u8, Vec<StreamId>)>,
distractors: Vec<(StreamId, u8, bool)>,
cycles: u8,
}
impl Arbitrary for RotationPlan {
fn arbitrary(g: &mut Gen) -> Self {
let peers = *g.choose(&[2u8, 3, 4, 5, 6, 7, 8]).expect("non-empty slice");
let urgency = *g
.choose(&[0u8, 1, 2, 3, 4, 5, 6, 7])
.expect("non-empty slice");
let mut next: StreamId = 1;
let mut incremental = Vec::with_capacity(usize::from(peers));
for _ in 0..peers {
incremental.push(next);
next += 2 * (1 + u32::from(u8::arbitrary(g) % 4));
}
let trailing = if urgency < MAX_URGENCY {
let spread = MAX_URGENCY - urgency;
let trailing_urgency = urgency + 1 + (u8::arbitrary(g) % spread);
let peers = *g.choose(&[2u8, 3, 4]).expect("non-empty slice");
let mut ids = Vec::with_capacity(usize::from(peers));
for _ in 0..peers {
ids.push(next);
next += 2 * (1 + u32::from(u8::arbitrary(g) % 4));
}
Some((trailing_urgency, ids))
} else {
None
};
let mut distractors = Vec::new();
for _ in 0..(u8::arbitrary(g) % 4) {
let id = next;
next += 2;
if urgency < MAX_URGENCY && bool::arbitrary(g) {
let spread = MAX_URGENCY - urgency;
let lower_priority = urgency + 1 + (u8::arbitrary(g) % spread);
distractors.push((id, lower_priority, true));
} else {
distractors.push((id, urgency, false));
}
}
RotationPlan {
incremental,
urgency,
trailing,
distractors,
cycles: 1 + u8::arbitrary(g) % 4,
}
}
}
fn drive(plan: RotationPlan) -> TestResult {
let peers = plan.incremental.len();
let mut entries: Vec<(StreamId, u8, bool)> = plan
.incremental
.iter()
.map(|&id| (id, plan.urgency, true))
.collect();
if let Some((trailing_urgency, ids)) = &plan.trailing {
entries.extend(ids.iter().map(|&id| (id, *trailing_urgency, true)));
}
entries.extend(plan.distractors.iter().copied());
let all_ids: Vec<StreamId> = entries.iter().map(|&(id, _, _)| id).collect();
let mut buckets: [Vec<StreamId>; URGENCY_LEVELS] = Default::default();
for &(id, urgency, incremental) in &entries {
if incremental {
buckets[bucket(urgency)].push(id);
}
}
for ids in buckets.iter_mut() {
ids.sort_unstable();
}
let mut scheduler = scheduler_with(&entries);
let passes = peers * usize::from(plan.cycles);
let mut leaders = Vec::with_capacity(passes);
let mut trailing_leaders: Vec<Option<StreamId>> = Vec::with_capacity(passes);
for pass in 0..passes {
let (order, mut census) = scheduler.begin_pass(all_ids.iter().copied(), |_| true);
if order.len() != all_ids.len() {
return TestResult::error(format!(
"pass {pass}: the rotation is a permutation, expected \
{} ids, got {order:?}",
all_ids.len()
));
}
if census.incremental_peer_count(plan.urgency) != peers {
return TestResult::error(format!(
"pass {pass}: urgency {} must hold exactly {peers} ready \
incremental peers, holds {}",
plan.urgency,
census.incremental_peer_count(plan.urgency)
));
}
for &stream_id in &order {
let (urgency, is_incremental) = scheduler.priority(&stream_id);
census.note_fired(urgency, stream_id, is_incremental, 1);
}
let pass_leaders = census.first_incremental_fired;
scheduler.end_pass(order, census);
for (index, ids) in buckets.iter().enumerate() {
if ids.is_empty() {
continue;
}
let expected = Some(ids[pass % ids.len()]);
if pass_leaders[index] != expected {
return TestResult::error(format!(
"pass {pass}: urgency {index} expected leader \
{expected:?}, got {:?} (bucket {ids:?}, main \
bucket u={} {:?})",
pass_leaders[index], plan.urgency, plan.incremental
));
}
}
leaders.push(pass_leaders[bucket(plan.urgency)]);
if let Some((trailing_urgency, _)) = &plan.trailing {
trailing_leaders.push(pass_leaders[bucket(*trailing_urgency)]);
}
}
for (pass, leader) in leaders.iter().enumerate() {
let expected = Some(plan.incremental[pass % peers]);
if *leader != expected {
return TestResult::error(format!(
"pass {pass}: expected leader {expected:?}, got {leader:?} \
(peers {:?}, leaders {leaders:?})",
plan.incremental
));
}
}
for &id in &plan.incremental {
let led = leaders.iter().filter(|l| **l == Some(id)).count();
if led != usize::from(plan.cycles) {
return TestResult::error(format!(
"stream {id} led {led} of {passes} passes, expected {} \
— a starved peer",
plan.cycles
));
}
}
if let Some((trailing_urgency, ids)) = &plan.trailing {
let counts: Vec<usize> = ids
.iter()
.map(|&id| trailing_leaders.iter().filter(|l| **l == Some(id)).count())
.collect();
let spread = counts.iter().max().copied().unwrap_or(0)
- counts.iter().min().copied().unwrap_or(0);
if spread > 1 {
return TestResult::error(format!(
"trailing bucket u={trailing_urgency} {ids:?} led \
{counts:?} over {passes} passes — spread {spread}, \
so a peer waited while another led repeatedly \
(leaders {trailing_leaders:?})"
));
}
}
TestResult::passed()
}
quickcheck! {
fn qc_incremental_leadership_visits_every_peer_once_per_cycle(plan: RotationPlan) -> TestResult {
drive(plan)
}
}
}
}