use std::{
collections::{BTreeMap, HashMap, HashSet, VecDeque},
time::{Duration, Instant},
};
use super::{GlobalStreamId, StreamId, h2::H2StreamId};
pub(super) const RECENTLY_RESET_CAPACITY: usize = 256;
const RESET_STREAM_HEADER_BLOCKS: u8 = 2;
const RESET_ALLOWANCES_CAPACITY: usize = 2 * RECENTLY_RESET_CAPACITY;
pub(super) struct H2StreamTable {
streams: BTreeMap<StreamId, GlobalStreamId>,
highest_peer_stream_id: StreamId,
expect_read: Option<(H2StreamId, usize)>,
expect_write: Option<H2StreamId>,
rst_sent: HashSet<StreamId>,
recently_reset: VecDeque<StreamId>,
recently_reset_ids: HashSet<StreamId>,
reset_data_allowance: HashMap<StreamId, u32>,
reset_header_blocks: HashMap<StreamId, u8>,
stream_last_activity_at: BTreeMap<StreamId, Instant>,
stream_fc_stalled_since: BTreeMap<StreamId, Instant>,
stream_fc_stalled_progress: BTreeMap<StreamId, usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum RemoveOutcome {
Removed,
NotPresent,
}
impl H2StreamTable {
pub(super) fn new(expect_read: Option<(H2StreamId, usize)>) -> Self {
let table = H2StreamTable {
streams: BTreeMap::new(),
highest_peer_stream_id: 0,
expect_read,
expect_write: None,
rst_sent: HashSet::new(),
recently_reset: VecDeque::new(),
recently_reset_ids: HashSet::new(),
reset_data_allowance: HashMap::new(),
reset_header_blocks: HashMap::new(),
stream_last_activity_at: BTreeMap::new(),
stream_fc_stalled_since: BTreeMap::new(),
stream_fc_stalled_progress: BTreeMap::new(),
};
table.debug_assert_invariants();
table
}
pub(super) fn streams(&self) -> &BTreeMap<StreamId, GlobalStreamId> {
&self.streams
}
pub(super) fn get(&self, stream_id: StreamId) -> Option<GlobalStreamId> {
self.streams.get(&stream_id).copied()
}
pub(super) fn len(&self) -> usize {
self.streams.len()
}
pub(super) fn is_empty(&self) -> bool {
self.streams.is_empty()
}
pub(super) fn register(&mut self, stream_id: StreamId, gid: GlobalStreamId, now: Instant) {
self.streams.insert(stream_id, gid);
self.stream_last_activity_at.insert(stream_id, now);
debug_assert_eq!(
self.streams.get(&stream_id).copied(),
Some(gid),
"register must map the wire id to the given slot"
);
debug_assert!(
self.stream_last_activity_at.contains_key(&stream_id),
"register must arm the per-stream idle timer"
);
self.debug_assert_invariants();
}
#[cfg(any(test, feature = "e2e-hooks"))]
pub(super) fn __test_insert_wire_mapping_only(
&mut self,
stream_id: StreamId,
gid: GlobalStreamId,
) {
self.streams.insert(stream_id, gid);
}
pub(super) fn remove(
&mut self,
stream_id: StreamId,
global_stream_id: GlobalStreamId,
) -> RemoveOutcome {
let outcome = if self.streams.remove(&stream_id).is_some() {
RemoveOutcome::Removed
} else {
RemoveOutcome::NotPresent
};
if self.rst_sent.remove(&stream_id) {
self.remember_reset(stream_id);
}
self.stream_last_activity_at.remove(&stream_id);
self.stream_fc_stalled_since.remove(&stream_id);
self.stream_fc_stalled_progress.remove(&stream_id);
if matches!(self.expect_write, Some(H2StreamId::Other { gid, .. }) if gid == global_stream_id)
{
self.expect_write = None;
}
if matches!(
self.expect_read,
Some((H2StreamId::Other { gid, .. }, _)) if gid == global_stream_id
) {
self.expect_read = None;
}
debug_assert!(
!self.rst_sent.contains(&stream_id),
"rst_sent still contains stream_id {stream_id} after eviction"
);
debug_assert!(
!self.stream_last_activity_at.contains_key(&stream_id),
"stream_last_activity_at still contains stream_id {stream_id} after eviction"
);
debug_assert!(
!self.stream_fc_stalled_since.contains_key(&stream_id),
"stream_fc_stalled_since still contains stream_id {stream_id} after eviction"
);
debug_assert!(
!self.stream_fc_stalled_progress.contains_key(&stream_id),
"stream_fc_stalled_progress still contains stream_id {stream_id} after eviction"
);
self.debug_assert_invariants();
outcome
}
pub(super) fn remember_refused(&mut self, stream_id: StreamId) {
debug_assert!(
!self.streams.contains_key(&stream_id),
"a refused stream is never registered"
);
self.remember_reset(stream_id);
self.debug_assert_invariants();
}
fn remember_reset(&mut self, stream_id: StreamId) {
if !self.recently_reset_ids.insert(stream_id) {
return;
}
if self.recently_reset.len() == RECENTLY_RESET_CAPACITY
&& let Some(oldest) = self.recently_reset.pop_front()
{
self.recently_reset_ids.remove(&oldest);
self.reset_data_allowance.remove(&oldest);
self.reset_header_blocks.remove(&oldest);
}
self.recently_reset.push_back(stream_id);
}
pub(super) fn highest_peer_stream_id(&self) -> StreamId {
self.highest_peer_stream_id
}
pub(super) fn observe_peer_stream_id(&mut self, stream_id: StreamId) {
let before = self.highest_peer_stream_id;
if stream_id > self.highest_peer_stream_id {
self.highest_peer_stream_id = stream_id;
}
debug_assert!(
self.highest_peer_stream_id >= before,
"highest_peer_stream_id must never regress"
);
}
pub(super) fn expect_read(&self) -> Option<(H2StreamId, usize)> {
self.expect_read
}
pub(super) fn set_expect_read(&mut self, value: Option<(H2StreamId, usize)>) {
self.expect_read = value;
}
pub(super) fn expect_write(&self) -> Option<H2StreamId> {
self.expect_write
}
pub(super) fn set_expect_write(&mut self, value: Option<H2StreamId>) {
self.expect_write = value;
}
pub(super) fn rst_sent_contains(&self, stream_id: StreamId) -> bool {
self.rst_sent.contains(&stream_id)
}
pub(super) fn was_reset_locally(&self, stream_id: StreamId) -> bool {
self.rst_sent.contains(&stream_id) || self.recently_reset_ids.contains(&stream_id)
}
pub(super) fn charge_reset_stream_header_block(&mut self, stream_id: StreamId) -> bool {
if !self.reset_header_blocks.contains_key(&stream_id)
&& self.reset_header_blocks.len() >= RESET_ALLOWANCES_CAPACITY
{
return false;
}
let blocks = self.reset_header_blocks.entry(stream_id).or_insert(0);
*blocks = blocks.saturating_add(1);
*blocks <= RESET_STREAM_HEADER_BLOCKS
}
pub(super) fn charge_reset_stream_data(
&mut self,
stream_id: StreamId,
len: u32,
granted: u32,
) -> bool {
if !self.reset_data_allowance.contains_key(&stream_id)
&& self.reset_data_allowance.len() >= RESET_ALLOWANCES_CAPACITY
{
return false;
}
let allowance = self
.reset_data_allowance
.entry(stream_id)
.or_insert(granted);
match allowance.checked_sub(len) {
Some(left) => {
*allowance = left;
true
}
None => {
*allowance = 0;
false
}
}
}
pub(super) fn rst_sent_mut(&mut self) -> &mut HashSet<StreamId> {
&mut self.rst_sent
}
#[cfg(any(debug_assertions, test))]
pub(super) fn stream_last_activity_at(&self) -> &BTreeMap<StreamId, Instant> {
&self.stream_last_activity_at
}
pub(super) fn activity_is_empty(&self) -> bool {
self.stream_last_activity_at.is_empty()
}
pub(super) fn touch_activity(&mut self, stream_id: StreamId, at: Instant) -> bool {
if let Some(t) = self.stream_last_activity_at.get_mut(&stream_id) {
*t = at;
true
} else {
false
}
}
pub(super) fn fc_stall_is_empty(&self) -> bool {
self.stream_fc_stalled_since.is_empty()
}
pub(super) fn fc_stall_progress(&self, stream_id: StreamId) -> Option<usize> {
self.stream_fc_stalled_progress.get(&stream_id).copied()
}
pub(super) fn clear_fc_stall(&mut self, stream_id: StreamId) {
self.stream_fc_stalled_since.remove(&stream_id);
self.stream_fc_stalled_progress.remove(&stream_id);
}
pub(super) fn arm_fc_stall(&mut self, stream_id: StreamId, now: Instant, progress: usize) {
self.stream_fc_stalled_since.entry(stream_id).or_insert(now);
self.stream_fc_stalled_progress.insert(stream_id, progress);
}
pub(super) fn collect_timed_out(
&self,
now: Instant,
deadline: Duration,
) -> Vec<(StreamId, &'static str)> {
let eligible =
|sid: StreamId| self.streams.contains_key(&sid) && !self.rst_sent.contains(&sid);
let expired = |t: Instant| now.saturating_duration_since(t) > deadline;
let mut seen: HashSet<StreamId> = HashSet::new();
let mut out: Vec<(StreamId, &'static str)> = Vec::new();
for (&sid, &t) in &self.stream_last_activity_at {
if eligible(sid) && expired(t) && seen.insert(sid) {
out.push((sid, "H2::IdleTimeout"));
}
}
for (&sid, &t) in &self.stream_fc_stalled_since {
if eligible(sid) && expired(t) && seen.insert(sid) {
out.push((sid, "H2::WindowStall"));
}
}
out
}
#[cfg(debug_assertions)]
fn check_invariants(&self) {
debug_assert!(
self.recently_reset.len() <= RECENTLY_RESET_CAPACITY
&& self.recently_reset_ids.len() == self.recently_reset.len(),
"recently_reset must stay bounded and indexed"
);
debug_assert!(
self.reset_data_allowance.len() <= RESET_ALLOWANCES_CAPACITY
&& self.reset_header_blocks.len() <= RESET_ALLOWANCES_CAPACITY,
"reset-stream allowances must stay bounded"
);
debug_assert!(
self.stream_fc_stalled_since
.keys()
.eq(self.stream_fc_stalled_progress.keys()),
"stream_fc_stalled_since and stream_fc_stalled_progress must track exactly the same stream ids"
);
}
#[inline]
fn debug_assert_invariants(&self) {
#[cfg(debug_assertions)]
self.check_invariants();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initial_state() {
let table = H2StreamTable::new(None);
assert!(table.is_empty());
assert_eq!(table.len(), 0);
assert_eq!(table.highest_peer_stream_id(), 0);
assert_eq!(table.expect_read(), None);
assert_eq!(table.expect_write(), None);
}
#[test]
fn register_maps_wire_id_and_arms_activity() {
let mut table = H2StreamTable::new(None);
let now = Instant::now();
table.register(1, 0, now);
assert_eq!(table.get(1), Some(0));
assert_eq!(table.len(), 1);
assert!(table.stream_last_activity_at().contains_key(&1));
}
#[test]
fn remove_evicts_every_cache_and_invalidates_matching_expect() {
let mut table = H2StreamTable::new(None);
let now = Instant::now();
table.register(1, 0, now);
table.rst_sent_mut().insert(1);
table.arm_fc_stall(1, now, 10);
table.set_expect_write(Some(H2StreamId::Other { id: 1, gid: 0 }));
table.set_expect_read(Some((H2StreamId::Other { id: 1, gid: 0 }, 5)));
assert_eq!(table.remove(1, 0), RemoveOutcome::Removed);
assert_eq!(table.get(1), None);
assert!(!table.rst_sent_contains(1));
assert_eq!(table.fc_stall_progress(1), None);
assert!(!table.stream_last_activity_at().contains_key(&1));
assert_eq!(table.expect_write(), None);
assert_eq!(table.expect_read(), None);
}
#[test]
fn remove_on_absent_stream_reports_not_present_but_still_evicts_caches() {
let mut table = H2StreamTable::new(None);
assert_eq!(table.remove(42, 0), RemoveOutcome::NotPresent);
}
#[test]
fn remove_only_invalidates_expect_referencing_the_removed_gid() {
let mut table = H2StreamTable::new(None);
let now = Instant::now();
table.register(1, 0, now);
table.register(3, 1, now);
table.set_expect_write(Some(H2StreamId::Other { id: 3, gid: 1 }));
table.remove(1, 0);
assert_eq!(
table.expect_write(),
Some(H2StreamId::Other { id: 3, gid: 1 })
);
}
#[test]
fn observe_peer_stream_id_is_monotonic() {
let mut table = H2StreamTable::new(None);
table.observe_peer_stream_id(5);
assert_eq!(table.highest_peer_stream_id(), 5);
table.observe_peer_stream_id(3);
assert_eq!(table.highest_peer_stream_id(), 5, "must not regress");
table.observe_peer_stream_id(9);
assert_eq!(table.highest_peer_stream_id(), 9);
}
#[test]
fn fc_stall_arm_does_not_refresh_an_already_armed_deadline() {
let mut table = H2StreamTable::new(None);
let t0 = Instant::now();
table.arm_fc_stall(1, t0, 100);
let t1 = t0 + std::time::Duration::from_secs(5);
table.arm_fc_stall(1, t1, 200);
assert_eq!(table.stream_last_activity_at().get(&1), None);
assert_eq!(table.fc_stall_progress(1), Some(200));
}
#[test]
fn touch_activity_does_not_insert_for_an_untracked_stream() {
let mut table = H2StreamTable::new(None);
assert!(!table.touch_activity(1, Instant::now()));
assert!(table.activity_is_empty());
}
#[test]
fn collect_timed_out_is_ascending_stream_id_order_deterministically() {
let mut table = H2StreamTable::new(None);
let old = Instant::now() - Duration::from_secs(60);
let deadline = Duration::from_secs(30);
let insertion_order: [StreamId; 9] = [17, 3, 901, 1, 501, 19, 777, 5, 45];
for &sid in &insertion_order {
table.register(sid, sid as GlobalStreamId, old);
}
let timed_out = table.collect_timed_out(Instant::now(), deadline);
let observed: Vec<StreamId> = timed_out.iter().map(|&(sid, _)| sid).collect();
let mut expected = insertion_order.to_vec();
expected.sort_unstable();
assert_eq!(
observed, expected,
"collect_timed_out must return the idle-timeout guard group in ascending stream_id order"
);
assert!(
timed_out
.iter()
.all(|&(_, reason)| reason == "H2::IdleTimeout")
);
}
#[test]
fn collect_timed_out_skips_untracked_and_already_rst_streams() {
let mut table = H2StreamTable::new(None);
let old = Instant::now() - Duration::from_secs(60);
let deadline = Duration::from_secs(30);
table.register(1, 0, old);
table.register(3, 1, old);
table.rst_sent_mut().insert(3);
let timed_out = table.collect_timed_out(Instant::now(), deadline);
assert_eq!(timed_out, vec![(1, "H2::IdleTimeout")]);
}
#[test]
fn collect_timed_out_idle_takes_precedence_on_a_tie() {
let mut table = H2StreamTable::new(None);
let old = Instant::now() - Duration::from_secs(60);
let deadline = Duration::from_secs(30);
table.register(1, 0, old);
table.arm_fc_stall(1, old, 0);
let timed_out = table.collect_timed_out(Instant::now(), deadline);
assert_eq!(timed_out, vec![(1, "H2::IdleTimeout")]);
}
#[test]
fn collect_timed_out_reaps_fc_stall_despite_fresh_liveness() {
let mut table = H2StreamTable::new(None);
let now = Instant::now();
let deadline = Duration::from_secs(2);
table.register(7, 0, now); table.arm_fc_stall(7, now - Duration::from_secs(5), 0);
let timed_out = table.collect_timed_out(now, deadline);
assert_eq!(timed_out, vec![(7, "H2::WindowStall")]);
}
#[test]
fn collect_timed_out_empty_when_all_fresh() {
let mut table = H2StreamTable::new(None);
let now = Instant::now();
let deadline = Duration::from_secs(2);
table.register(1, 0, now);
table.arm_fc_stall(1, now, 0);
assert!(table.collect_timed_out(now, deadline).is_empty());
}
#[test]
fn streams_iterates_in_ascending_stream_id_order_whatever_the_insertion_order() {
let mut table = H2StreamTable::new(None);
let now = Instant::now();
let insertion_order: [StreamId; 9] = [901, 5, 17, 45, 1, 777, 19, 3, 501];
for (slot, &stream_id) in insertion_order.iter().enumerate() {
table.register(stream_id, slot as GlobalStreamId, now);
}
let mut expected_pairs: Vec<(StreamId, GlobalStreamId)> = insertion_order
.iter()
.enumerate()
.map(|(slot, &stream_id)| (stream_id, slot as GlobalStreamId))
.collect();
expected_pairs.sort_unstable();
let expected_ids: Vec<StreamId> = expected_pairs.iter().map(|&(id, _)| id).collect();
let expected_slots: Vec<GlobalStreamId> =
expected_pairs.iter().map(|&(_, slot)| slot).collect();
let observed_ids: Vec<StreamId> = table.streams().keys().copied().collect();
assert_eq!(
observed_ids, expected_ids,
"streams() must walk ascending wire StreamId: begin_scheduler_pass \
seeds the whole write-pass order buffer from exactly these keys"
);
let observed_slots: Vec<GlobalStreamId> = table.streams().values().copied().collect();
assert_eq!(
observed_slots, expected_slots,
"streams().values() must follow that same ascending key order: it is \
the prefix update_initial_window_size has already mutated when a \
checked_add overflow aborts it, and the order close tears down in"
);
let observed_pairs: Vec<(StreamId, GlobalStreamId)> = table
.streams()
.iter()
.map(|(&stream_id, &slot)| (stream_id, slot))
.collect();
assert_eq!(
observed_pairs, expected_pairs,
"streams() pairs must agree with its own keys and values: \
handle_goaway_frame reads both halves of this iterator together"
);
}
#[test]
fn reset_stream_allowances_stay_bounded() {
let mut table = H2StreamTable::new(None);
let capacity = RESET_ALLOWANCES_CAPACITY as u32;
for index in 0..capacity {
let stream_id = 1 + 2 * index;
table.rst_sent_mut().insert(stream_id);
assert!(table.charge_reset_stream_data(stream_id, 1, 10));
assert!(table.charge_reset_stream_header_block(stream_id));
}
let next = 1 + 2 * capacity;
table.rst_sent_mut().insert(next);
assert!(
!table.charge_reset_stream_data(next, 1, 10),
"no DATA allowance past the bound"
);
assert!(
!table.charge_reset_stream_header_block(next),
"no header-block allowance past the bound"
);
assert!(
table.charge_reset_stream_data(1, 1, 10),
"an allowance already held is kept"
);
assert_eq!(table.reset_data_allowance.len(), RESET_ALLOWANCES_CAPACITY);
}
#[test]
fn was_reset_locally_survives_removal_within_the_bound() {
let mut table = H2StreamTable::new(None);
let now = Instant::now();
table.register(1, 0, now);
table.register(3, 1, now);
table.rst_sent_mut().insert(1);
table.remove(1, 0);
table.remove(3, 1);
assert!(!table.rst_sent_contains(1), "rst_sent is still evicted");
assert!(table.was_reset_locally(1), "a reset stream stays known");
assert!(!table.was_reset_locally(3), "a stream never reset is not");
let capacity = RECENTLY_RESET_CAPACITY as u32;
for id in 0..capacity {
let stream_id = 5 + 2 * id;
table.register(stream_id, 0, now);
table.rst_sent_mut().insert(stream_id);
table.remove(stream_id, 0);
}
assert!(!table.was_reset_locally(1), "the oldest id is forgotten");
assert!(table.was_reset_locally(5), "every newer id is kept");
assert!(table.was_reset_locally(5 + 2 * (capacity - 1)));
assert_eq!(table.recently_reset.len(), RECENTLY_RESET_CAPACITY);
}
#[test]
fn refused_streams_share_the_recently_reset_bound() {
let mut table = H2StreamTable::new(None);
table.remember_refused(1);
assert!(
table.was_reset_locally(1),
"a refused stream is known as reset"
);
assert!(
!table.rst_sent_contains(1),
"a refused id stays out of rst_sent"
);
assert!(table.charge_reset_stream_header_block(1));
assert!(table.charge_reset_stream_data(1, 10, 10));
table.remember_refused(1);
assert_eq!(table.recently_reset.len(), 1, "an id is remembered once");
let capacity = RECENTLY_RESET_CAPACITY as u32;
for id in 1..=capacity {
table.remember_refused(1 + 2 * id);
}
assert!(
!table.was_reset_locally(1),
"the oldest refused id is forgotten"
);
assert!(
!table.reset_header_blocks.contains_key(&1)
&& !table.reset_data_allowance.contains_key(&1),
"its allowances go with it"
);
assert!(table.was_reset_locally(3) && table.was_reset_locally(1 + 2 * capacity));
assert_eq!(table.recently_reset.len(), RECENTLY_RESET_CAPACITY);
}
}