use std::collections::BTreeMap;
use super::serializer;
pub(super) struct H2FlowControl {
window: i32,
received_bytes_since_update: u32,
pending_window_updates: BTreeMap<u32, u32>,
}
pub(super) enum QueueWindowUpdateOutcome {
Coalesced { old: u32, new: u32 },
Inserted { increment: u32 },
Dropped,
Noop,
}
pub(super) enum ApplyWindowUpdateOutcome {
Applied {
new_window: i32,
should_arm_writable: bool,
},
Overflow,
}
impl H2FlowControl {
pub(super) fn new(initial_window: i32) -> Self {
let flow_control = H2FlowControl {
window: initial_window,
received_bytes_since_update: 0,
pending_window_updates: BTreeMap::new(),
};
flow_control.debug_assert_invariants();
flow_control
}
pub(super) fn window(&self) -> i32 {
self.window
}
pub(super) fn consume_send_window(&mut self, consumed: i32) {
debug_assert!(
consumed >= 0,
"send-window consumption must not be negative"
);
let before = self.window;
self.window = self.window.saturating_sub(consumed);
debug_assert!(
self.window <= before,
"consuming send credit must never grow the window"
);
self.debug_assert_invariants();
}
pub(super) fn refund_send_window(&mut self, refunded: i32) -> ApplyWindowUpdateOutcome {
debug_assert!(refunded > 0, "only a positive refund is ever applied");
let before = self.window;
let Some(window) = self.window.checked_add(refunded) else {
debug_assert_eq!(self.window, before, "an overflowing refund changes nothing");
return ApplyWindowUpdateOutcome::Overflow;
};
self.window = window;
debug_assert_eq!(
i64::from(self.window),
i64::from(before) + i64::from(refunded),
"a refund grows the send window by exactly what it gives back"
);
debug_assert!(
self.window > before,
"a positive refund must strictly grow the send window"
);
self.debug_assert_invariants();
ApplyWindowUpdateOutcome::Applied {
new_window: window,
should_arm_writable: before <= 0 && window > 0,
}
}
pub(super) fn apply_window_update(&mut self, increment: i32) -> ApplyWindowUpdateOutcome {
debug_assert!(
increment > 0,
"WINDOW_UPDATE increment must be strictly positive at this point"
);
let window_before = self.window;
match self.window.checked_add(increment) {
Some(new_window) => {
let should_arm_writable = self.window <= 0 && new_window > 0;
self.window = new_window;
debug_assert_eq!(
self.window,
window_before + increment,
"connection window must increase by exactly the increment"
);
debug_assert!(
self.window > window_before,
"a positive WINDOW_UPDATE must strictly grow the connection window"
);
self.debug_assert_invariants();
ApplyWindowUpdateOutcome::Applied {
new_window,
should_arm_writable,
}
}
None => {
debug_assert!(
i64::from(window_before) + i64::from(increment) > i64::from(i32::MAX),
"checked_add rejected a WINDOW_UPDATE that would not have overflowed i32"
);
ApplyWindowUpdateOutcome::Overflow
}
}
}
pub(super) fn account_received_bytes(
&mut self,
wire_payload_len: u32,
threshold: u32,
) -> Option<u32> {
self.received_bytes_since_update = self
.received_bytes_since_update
.saturating_add(wire_payload_len);
let increment = if self.received_bytes_since_update >= threshold {
let increment = self.received_bytes_since_update;
self.received_bytes_since_update = 0;
Some(increment)
} else {
None
};
self.debug_assert_invariants();
increment
}
pub(super) fn pending_window_updates_len(&self) -> usize {
self.pending_window_updates.len()
}
pub(super) fn pending_window_updates_is_empty(&self) -> bool {
self.pending_window_updates.is_empty()
}
pub(super) fn clear_pending_window_updates(&mut self) {
self.pending_window_updates.clear();
self.debug_assert_invariants();
}
pub(super) fn queue_window_update(
&mut self,
stream_id: u32,
increment: u32,
max_pending: usize,
) -> QueueWindowUpdateOutcome {
if increment == 0 {
return QueueWindowUpdateOutcome::Noop;
}
let max_increment = i32::MAX as u32;
let len_before = self.pending_window_updates.len();
let outcome = if let Some(existing) = self.pending_window_updates.get_mut(&stream_id) {
let old = *existing;
*existing = existing.saturating_add(increment).min(max_increment);
debug_assert!(
*existing >= old,
"coalesced WINDOW_UPDATE increment must be monotonic non-decreasing"
);
debug_assert!(
*existing <= max_increment,
"coalesced WINDOW_UPDATE increment must stay within i32::MAX"
);
QueueWindowUpdateOutcome::Coalesced {
old,
new: *existing,
}
} else if self.pending_window_updates.len() < max_pending {
let capped = increment.min(max_increment);
self.pending_window_updates.insert(stream_id, capped);
QueueWindowUpdateOutcome::Inserted { increment: capped }
} else {
QueueWindowUpdateOutcome::Dropped
};
debug_assert!(
self.pending_window_updates.len() >= len_before,
"queue_window_update must never shrink the pending map"
);
self.debug_assert_invariants();
outcome
}
pub(super) fn drain_window_updates_into(&mut self, buf: &mut [u8]) -> (usize, usize) {
let len_before = self.pending_window_updates.len();
let mut offset = 0usize;
let mut frames_written = 0usize;
let mut drained_ids: Vec<u32> = Vec::new();
for (&stream_id, &increment) in self.pending_window_updates.iter() {
if increment == 0 {
debug_assert!(
false,
"pending_window_updates must never hold a zero-increment entry"
);
drained_ids.push(stream_id);
continue;
}
match serializer::gen_window_update(&mut buf[offset..], stream_id, increment) {
Ok((_, size)) => {
offset += size;
frames_written += 1;
drained_ids.push(stream_id);
}
Err(_) => {
break;
}
}
}
for id in &drained_ids {
self.pending_window_updates.remove(id);
}
debug_assert!(
self.pending_window_updates.len() <= len_before,
"drain must only shrink the pending map"
);
debug_assert_eq!(
drained_ids.len(),
frames_written,
"every drained id must have produced a written frame (zero entries are unreachable)"
);
self.debug_assert_invariants();
(offset, frames_written)
}
#[cfg(debug_assertions)]
fn check_invariants(&self) {
debug_assert!(
self.pending_window_updates
.values()
.all(|&increment| increment != 0),
"pending_window_updates must never hold a zero-increment entry"
);
}
#[inline]
fn debug_assert_invariants(&self) {
#[cfg(debug_assertions)]
self.check_invariants();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initial_state() {
let fc = H2FlowControl::new(65_535);
assert_eq!(fc.window(), 65_535);
assert_eq!(fc.pending_window_updates_len(), 0);
assert!(fc.pending_window_updates_is_empty());
}
#[test]
fn refund_gives_back_consumed_credit_and_reports_a_reopened_window() {
let mut fc = H2FlowControl::new(65_535);
fc.consume_send_window(65_535);
assert_eq!(fc.window(), 0);
assert!(
matches!(
fc.refund_send_window(16_384),
ApplyWindowUpdateOutcome::Applied {
new_window: 16_384,
should_arm_writable: true
}
),
"a closed window reopens"
);
assert!(
matches!(
fc.refund_send_window(49_151),
ApplyWindowUpdateOutcome::Applied {
new_window: 65_535,
should_arm_writable: false
}
),
"an already open window does not reopen"
);
assert_eq!(fc.window(), 65_535, "back to the credit before the prepare");
}
#[test]
fn a_refund_past_the_window_ceiling_is_an_overflow_and_changes_nothing() {
let mut fc = H2FlowControl::new(65_535);
fc.consume_send_window(65_535);
assert!(matches!(
fc.apply_window_update(i32::MAX),
ApplyWindowUpdateOutcome::Applied { .. }
));
assert!(matches!(
fc.refund_send_window(65_535),
ApplyWindowUpdateOutcome::Overflow
));
assert_eq!(
fc.window(),
i32::MAX,
"the window is left as the peer set it"
);
}
#[test]
fn window_update_coalescing() {
let mut fc = H2FlowControl::new(65_535);
match fc.queue_window_update(1, 1000, 10) {
QueueWindowUpdateOutcome::Inserted { increment } => assert_eq!(increment, 1000),
_ => panic!("expected Inserted"),
}
match fc.queue_window_update(1, 500, 10) {
QueueWindowUpdateOutcome::Coalesced { old, new } => {
assert_eq!(old, 1000);
assert_eq!(new, 1500);
}
_ => panic!("expected Coalesced"),
}
fc.queue_window_update(3, 2000, 10);
assert_eq!(fc.pending_window_updates_len(), 2);
}
#[test]
fn window_update_saturation() {
let mut fc = H2FlowControl::new(65_535);
let max_increment = i32::MAX as u32;
fc.queue_window_update(1, max_increment - 100, 10);
match fc.queue_window_update(1, 200, 10) {
QueueWindowUpdateOutcome::Coalesced { new, .. } => assert_eq!(new, max_increment),
_ => panic!("expected Coalesced"),
}
}
#[test]
fn zero_increment_is_a_noop_and_never_queued() {
let mut fc = H2FlowControl::new(65_535);
match fc.queue_window_update(1, 0, 10) {
QueueWindowUpdateOutcome::Noop => {}
_ => panic!("expected Noop"),
}
assert!(fc.pending_window_updates_is_empty());
}
#[test]
fn queue_window_update_drops_beyond_cap() {
let mut fc = H2FlowControl::new(65_535);
for i in 0..4u32 {
fc.queue_window_update(i, 1000, 4);
}
assert_eq!(fc.pending_window_updates_len(), 4);
match fc.queue_window_update(4, 1000, 4) {
QueueWindowUpdateOutcome::Dropped => {}
_ => panic!("expected Dropped"),
}
assert_eq!(fc.pending_window_updates_len(), 4);
}
#[test]
fn connection_window_can_go_negative_via_consume() {
let mut fc = H2FlowControl::new(100);
fc.consume_send_window(200);
assert_eq!(fc.window(), -100);
}
#[test]
fn consume_send_window_saturates_at_i32_min_without_wrapping() {
let mut fc = H2FlowControl::new(i32::MIN + 10);
fc.consume_send_window(i32::MAX);
assert_eq!(fc.window(), i32::MIN);
}
#[test]
fn apply_window_update_grows_window_and_signals_arm_on_unstall() {
let mut fc = H2FlowControl::new(0);
match fc.apply_window_update(50) {
ApplyWindowUpdateOutcome::Applied {
new_window,
should_arm_writable,
} => {
assert_eq!(new_window, 50);
assert!(should_arm_writable, "0 -> positive must arm writable");
}
ApplyWindowUpdateOutcome::Overflow => panic!("unexpected overflow"),
}
}
#[test]
fn apply_window_update_overflow_is_reported() {
let mut fc = H2FlowControl::new(i32::MAX - 1);
match fc.apply_window_update(10) {
ApplyWindowUpdateOutcome::Overflow => {}
ApplyWindowUpdateOutcome::Applied { .. } => panic!("expected Overflow"),
}
}
#[test]
fn account_received_bytes_accumulates_then_resets_at_threshold() {
let mut fc = H2FlowControl::new(65_535);
assert_eq!(fc.account_received_bytes(100, 250), None);
assert_eq!(fc.account_received_bytes(100, 250), None);
assert_eq!(fc.account_received_bytes(100, 250), Some(300));
assert_eq!(fc.account_received_bytes(1, 250), None);
}
#[test]
fn drain_emits_ascending_stream_id_order_deterministically() {
let mut fc = H2FlowControl::new(65_535);
let insertion_order: [u32; 21] = [
17, 3, 900, 1, 0, 501, 2, 19, 4, 777, 13, 6, 8, 333, 21, 5, 9, 111, 7, 45, 23,
];
for &stream_id in &insertion_order {
fc.queue_window_update(stream_id, 1000, insertion_order.len());
}
assert_eq!(fc.pending_window_updates_len(), insertion_order.len());
let mut buf = [0u8; 21 * 13];
let (bytes_written, frames_written) = fc.drain_window_updates_into(&mut buf);
assert_eq!(frames_written, insertion_order.len());
assert!(fc.pending_window_updates_is_empty());
let mut observed_order = Vec::new();
let mut offset = 0;
while offset < bytes_written {
let stream_id = u32::from_be_bytes([
buf[offset + 5],
buf[offset + 6],
buf[offset + 7],
buf[offset + 8],
]) & 0x7FFF_FFFF;
observed_order.push(stream_id);
offset += 13;
}
let mut expected_order = insertion_order.to_vec();
expected_order.sort_unstable();
assert_eq!(
observed_order, expected_order,
"WINDOW_UPDATE drain order must be the deterministic ascending stream_id order"
);
assert_eq!(observed_order[0], 0);
}
}