use std::collections::HashSet;
use crate::{
Readiness,
protocol::mux::{
StreamId,
parser::{self, H2Error},
serializer,
},
};
pub(super) const MIN_PENDING_RST_STREAMS: usize = 4000;
pub(super) fn pending_rst_bound(max_concurrent_streams: u32) -> usize {
MIN_PENDING_RST_STREAMS.max((max_concurrent_streams as usize).saturating_mul(4))
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum EnqueueRstOutcome {
Queued,
Deduped,
Dropped,
}
pub(super) struct H2ControlTx {
pending_rst_streams: Vec<(StreamId, H2Error)>,
total_rst_streams_queued: usize,
max_pending: usize,
overflowed: bool,
}
impl H2ControlTx {
pub(super) fn new(max_concurrent_streams: u32) -> Self {
Self::with_cap(pending_rst_bound(max_concurrent_streams))
}
pub(super) fn with_cap(max_pending: usize) -> Self {
Self {
pending_rst_streams: Vec::new(),
total_rst_streams_queued: 0,
max_pending,
overflowed: false,
}
}
pub(super) fn enqueue_rst(
&mut self,
rst_sent: Option<&mut HashSet<StreamId>>,
readiness: &mut Readiness,
wire_stream_id: StreamId,
error: H2Error,
) -> EnqueueRstOutcome {
let pending_before = self.pending_rst_streams.len();
let total_before = self.total_rst_streams_queued;
if pending_before >= self.max_pending {
self.overflowed = true;
debug_assert_eq!(
self.pending_rst_streams.len(),
pending_before,
"an at-capacity refusal must not enqueue"
);
self.debug_assert_invariants();
return EnqueueRstOutcome::Dropped;
}
if let Some(rst_sent) = rst_sent
&& !rst_sent.insert(wire_stream_id)
{
debug_assert!(
rst_sent.contains(&wire_stream_id),
"dedupe path requires the id to already be present in rst_sent"
);
debug_assert_eq!(
self.pending_rst_streams.len(),
pending_before,
"dedupe path must not enqueue a new pending RST"
);
debug_assert_eq!(
self.total_rst_streams_queued, total_before,
"dedupe path must not bump the queued-RST lifetime counter"
);
self.debug_assert_invariants();
return EnqueueRstOutcome::Deduped;
}
self.pending_rst_streams.push((wire_stream_id, error));
self.total_rst_streams_queued += 1;
readiness.arm_writable();
debug_assert_eq!(
self.pending_rst_streams.len(),
pending_before + 1,
"a freshly-queued RST must push exactly one pending entry"
);
debug_assert_eq!(
self.total_rst_streams_queued,
total_before + 1,
"a freshly-queued RST must bump the queued-RST lifetime counter by one"
);
debug_assert_eq!(
self.pending_rst_streams.last().map(|(id, _)| *id),
Some(wire_stream_id),
"the just-pushed entry must be the requested wire stream id"
);
self.debug_assert_invariants();
EnqueueRstOutcome::Queued
}
pub(super) fn drain_rst_streams_into(&mut self, buf: &mut [u8]) -> (usize, usize) {
let pending_before = self.pending_rst_streams.len();
let mut offset = 0;
let mut written_count = 0;
for &(stream_id, ref error) in &self.pending_rst_streams {
let frame_size = parser::FRAME_HEADER_SIZE + parser::RST_STREAM_PAYLOAD_SIZE as usize;
if offset + frame_size > buf.len() {
break;
}
match serializer::gen_rst_stream(&mut buf[offset..], stream_id, error.to_owned()) {
Ok((_, _)) => {
offset += frame_size;
written_count += 1;
}
Err(_) => break,
}
}
self.pending_rst_streams.drain(..written_count);
debug_assert_eq!(
self.pending_rst_streams.len(),
pending_before - written_count,
"the drain must remove exactly the frames it serialized"
);
debug_assert!(
offset <= buf.len(),
"the drain must never report more bytes than the buffer holds"
);
debug_assert_eq!(
offset == 0,
written_count == 0,
"bytes written and frames written must be zero together"
);
self.debug_assert_invariants();
(offset, written_count)
}
pub(super) fn has_pending(&self) -> bool {
!self.pending_rst_streams.is_empty()
}
pub(super) fn pending_len(&self) -> usize {
self.pending_rst_streams.len()
}
#[cfg(test)]
pub(super) fn pending(&self) -> &[(StreamId, H2Error)] {
&self.pending_rst_streams
}
pub(super) fn lifetime_queued(&self) -> usize {
self.total_rst_streams_queued
}
pub(super) fn max_pending(&self) -> usize {
self.max_pending
}
pub(super) fn overflowed(&self) -> bool {
self.overflowed
}
pub(super) fn clear_pending(&mut self) {
let total_before = self.total_rst_streams_queued;
self.pending_rst_streams.clear();
debug_assert_eq!(
self.total_rst_streams_queued, total_before,
"clearing the queue must not rewind the lifetime counter"
);
self.debug_assert_invariants();
}
#[cfg(debug_assertions)]
fn check_invariants(&self) {
debug_assert!(
self.total_rst_streams_queued >= self.pending_rst_streams.len(),
"queued-RST lifetime counter ({}) must be >= currently-pending queue ({})",
self.total_rst_streams_queued,
self.pending_rst_streams.len()
);
debug_assert!(
self.pending_rst_streams.len() <= self.max_pending + 1,
"pending RST queue must stay within its hard cap (escalates at the cap)"
);
debug_assert!(
!self.overflowed || self.total_rst_streams_queued >= self.max_pending,
"an overflow requires the queue to have reached its bound"
);
}
fn debug_assert_invariants(&self) {
#[cfg(debug_assertions)]
self.check_invariants();
}
}
#[cfg(test)]
mod tests {
use quickcheck::{Arbitrary, Gen, quickcheck};
use super::*;
fn readiness() -> Readiness {
Readiness::new()
}
const RST_FRAME_SIZE: usize =
parser::FRAME_HEADER_SIZE + parser::RST_STREAM_PAYLOAD_SIZE as usize;
#[test]
fn test_enqueue_rst_into_populates_queue_and_dedupe() {
let mut tx = H2ControlTx::new(100);
let mut sent: HashSet<StreamId> = HashSet::new();
let mut readiness = Readiness::new();
let first = tx.enqueue_rst(Some(&mut sent), &mut readiness, 5, H2Error::ProtocolError);
assert_eq!(
first,
EnqueueRstOutcome::Queued,
"first call must report a fresh queue"
);
let second = tx.enqueue_rst(Some(&mut sent), &mut readiness, 5, H2Error::InternalError);
assert_eq!(
second,
EnqueueRstOutcome::Deduped,
"second call for same stream must report Deduped"
);
assert_eq!(
tx.pending().len(),
1,
"dedupe must collapse to a single entry"
);
assert_eq!(
tx.pending()[0],
(5, H2Error::ProtocolError),
"the first error wins — second push is ignored"
);
assert_eq!(
tx.lifetime_queued(),
1,
"queued-cap counter must bump exactly once"
);
assert!(sent.contains(&5), "rst_sent must record the id");
}
#[test]
fn test_enqueue_rst_into_bumps_total_for_distinct_ids() {
let mut tx = H2ControlTx::new(100);
let mut sent: HashSet<StreamId> = HashSet::new();
let mut readiness = Readiness::new();
for sid in [1u32, 3, 5, 7] {
tx.enqueue_rst(Some(&mut sent), &mut readiness, sid, H2Error::ProtocolError);
}
assert_eq!(tx.pending().len(), 4);
assert_eq!(tx.lifetime_queued(), 4);
assert_eq!(sent.len(), 4);
}
#[test]
fn test_enqueue_rst_into_arms_writable_in_invariant_15_form() {
let mut tx = H2ControlTx::new(100);
let mut sent: HashSet<StreamId> = HashSet::new();
let mut readiness = Readiness::new();
assert!(!readiness.interest.is_writable());
assert!(!readiness.event.is_writable());
tx.enqueue_rst(
Some(&mut sent),
&mut readiness,
9,
H2Error::FlowControlError,
);
assert!(
readiness.interest.is_writable(),
"arm_writable must raise the interest bit"
);
assert!(
readiness.event.is_writable(),
"arm_writable must raise the event bit (edge-triggered epoll)"
);
}
#[test]
fn test_enqueue_rst_into_dedupe_does_not_rearm_writable() {
let mut tx = H2ControlTx::new(100);
let mut sent: HashSet<StreamId> = HashSet::new();
sent.insert(11);
let mut readiness = Readiness::new();
tx.enqueue_rst(Some(&mut sent), &mut readiness, 11, H2Error::ProtocolError);
assert!(
tx.pending().is_empty(),
"already-sent ids must not queue a second frame"
);
assert_eq!(tx.lifetime_queued(), 0);
assert!(!readiness.interest.is_writable());
assert!(!readiness.event.is_writable());
}
#[test]
fn test_enqueue_rst_into_refuses_at_capacity_without_side_effects() {
const MAX: usize = 4;
let mut tx = H2ControlTx::with_cap(MAX);
let mut sent: HashSet<StreamId> = HashSet::new();
let mut readiness = Readiness::new();
for sid in [1u32, 3, 5, 7] {
assert_eq!(
tx.enqueue_rst(Some(&mut sent), &mut readiness, sid, H2Error::Cancel),
EnqueueRstOutcome::Queued,
"every insert below the cap must be queued"
);
}
assert_eq!(
tx.pending().len(),
MAX,
"the queue must fill to exactly the cap"
);
let mut at_capacity = Readiness::new();
assert_eq!(
tx.enqueue_rst(Some(&mut sent), &mut at_capacity, 9, H2Error::Cancel),
EnqueueRstOutcome::Dropped,
"an insert at the cap must be refused"
);
assert_eq!(
tx.pending().len(),
MAX,
"a refused insert must leave the queue at the cap"
);
assert_eq!(
tx.lifetime_queued(),
MAX,
"a refused insert must not bump the queued-RST lifetime counter"
);
assert!(
!sent.contains(&9),
"an at-capacity refusal must not record the id as reset"
);
assert!(
!at_capacity.interest.is_writable() && !at_capacity.event.is_writable(),
"a refused insert must not arm WRITABLE for a frame it did not queue"
);
}
#[derive(Clone, Debug)]
enum RstStep {
Enqueue(u8),
Reap(u8),
Drain(u8),
}
impl Arbitrary for RstStep {
fn arbitrary(g: &mut Gen) -> Self {
match u8::arbitrary(g) % 3 {
0 => RstStep::Enqueue(u8::arbitrary(g)),
1 => RstStep::Reap(u8::arbitrary(g)),
_ => RstStep::Drain(u8::arbitrary(g)),
}
}
}
#[test]
fn prop_pending_rst_queue_stays_within_its_bound() {
fn prop(steps: Vec<RstStep>) -> bool {
const MAX: usize = 16;
let mut tx = H2ControlTx::with_cap(MAX);
let mut sent: HashSet<StreamId> = HashSet::new();
let mut readiness = Readiness::new();
let mut next_reaped: StreamId = 1001;
for step in steps {
match step {
RstStep::Enqueue(id) => {
tx.enqueue_rst(
Some(&mut sent),
&mut readiness,
2 * (id as StreamId % 64) + 1,
H2Error::ProtocolError,
);
}
RstStep::Reap(n) => {
for _ in 0..=(n % 96) {
tx.enqueue_rst(
Some(&mut sent),
&mut readiness,
next_reaped,
H2Error::Cancel,
);
next_reaped += 2;
}
}
RstStep::Drain(n) => {
let mut buf = vec![0u8; RST_FRAME_SIZE * (n as usize % 8)];
tx.drain_rst_streams_into(&mut buf);
}
}
if tx.pending().len() > MAX {
return false;
}
if tx.lifetime_queued() < tx.pending().len() {
return false;
}
if !tx.pending().iter().all(|(id, _)| sent.contains(id)) {
return false;
}
let unique: HashSet<StreamId> = tx.pending().iter().map(|(id, _)| *id).collect();
if unique.len() != tx.pending().len() {
return false;
}
}
true
}
quickcheck(prop as fn(Vec<RstStep>) -> bool);
}
#[test]
fn the_drain_serializes_every_queued_frame_when_the_buffer_is_large_enough() {
let mut tx = H2ControlTx::new(100);
let mut rst_sent = HashSet::new();
let mut readiness = readiness();
for id in [1, 3, 5] {
tx.enqueue_rst(Some(&mut rst_sent), &mut readiness, id, H2Error::Cancel);
}
let mut buf = vec![0u8; RST_FRAME_SIZE * 8];
let (bytes, frames) = tx.drain_rst_streams_into(&mut buf);
assert_eq!(frames, 3);
assert_eq!(bytes, RST_FRAME_SIZE * 3);
assert!(!tx.has_pending(), "a complete drain empties the queue");
assert_eq!(
tx.lifetime_queued(),
3,
"draining must not rewind the lifetime counter"
);
}
#[test]
fn a_short_buffer_writes_whole_frames_only_and_leaves_the_rest_queued() {
let mut tx = H2ControlTx::new(100);
let mut rst_sent = HashSet::new();
let mut readiness = readiness();
for id in [1, 3, 5, 7] {
tx.enqueue_rst(Some(&mut rst_sent), &mut readiness, id, H2Error::Cancel);
}
let mut buf = vec![0u8; RST_FRAME_SIZE * 2 + 1];
let (bytes, frames) = tx.drain_rst_streams_into(&mut buf);
assert_eq!(frames, 2, "a frame is written whole or not at all");
assert_eq!(
bytes,
RST_FRAME_SIZE * 2,
"the reported byte count must not include a partial frame"
);
assert_eq!(tx.pending().len(), 2, "the unwritten frames stay queued");
}
#[test]
fn a_buffer_too_small_for_one_frame_writes_nothing_and_drops_nothing() {
let mut tx = H2ControlTx::new(100);
let mut rst_sent = HashSet::new();
let mut readiness = readiness();
tx.enqueue_rst(Some(&mut rst_sent), &mut readiness, 1, H2Error::Cancel);
let mut buf = vec![0u8; RST_FRAME_SIZE - 1];
let (bytes, frames) = tx.drain_rst_streams_into(&mut buf);
assert_eq!((bytes, frames), (0, 0));
assert_eq!(tx.pending().len(), 1, "nothing fit, so nothing was dropped");
}
#[test]
fn an_exactly_sized_buffer_writes_the_frame() {
let mut tx = H2ControlTx::new(100);
let mut rst_sent = HashSet::new();
let mut readiness = readiness();
tx.enqueue_rst(Some(&mut rst_sent), &mut readiness, 1, H2Error::Cancel);
let mut buf = vec![0u8; RST_FRAME_SIZE];
let (bytes, frames) = tx.drain_rst_streams_into(&mut buf);
assert_eq!((bytes, frames), (RST_FRAME_SIZE, 1));
assert!(!tx.has_pending());
}
#[test]
fn draining_an_empty_queue_is_a_no_op() {
let mut tx = H2ControlTx::new(100);
let mut buf = vec![0u8; RST_FRAME_SIZE * 4];
assert_eq!(tx.drain_rst_streams_into(&mut buf), (0, 0));
}
#[test]
fn draining_keeps_any_number_of_resets_within_the_bound() {
let mut tx = H2ControlTx::new(100);
let bound = tx.max_pending();
let mut rst_sent = HashSet::new();
let mut readiness = readiness();
let mut buf = vec![0u8; RST_FRAME_SIZE * bound];
let mut next: StreamId = 1;
for _ in 0..5 {
for _ in 0..bound {
assert_eq!(
tx.enqueue_rst(Some(&mut rst_sent), &mut readiness, next, H2Error::Cancel),
EnqueueRstOutcome::Queued
);
next += 2;
}
let (_, frames) = tx.drain_rst_streams_into(&mut buf);
assert_eq!(frames, bound);
assert!(
!tx.overflowed(),
"a queue drained before it overflows must never report an overflow"
);
}
assert_eq!(tx.lifetime_queued(), 5 * bound);
}
#[test]
fn overflow_is_set_by_a_refused_insert_and_survives_the_drain() {
const MAX: usize = 4;
let mut tx = H2ControlTx::with_cap(MAX);
let mut sent: HashSet<StreamId> = HashSet::new();
let mut readiness = readiness();
for sid in [1u32, 3, 5, 7] {
tx.enqueue_rst(Some(&mut sent), &mut readiness, sid, H2Error::Cancel);
}
assert!(
!tx.overflowed(),
"a queue exactly at its bound has not overflowed"
);
assert_eq!(
tx.enqueue_rst(Some(&mut sent), &mut readiness, 9, H2Error::Cancel),
EnqueueRstOutcome::Dropped
);
assert!(tx.overflowed(), "the refused insert records the overflow");
let mut buf = vec![0u8; RST_FRAME_SIZE * MAX];
tx.drain_rst_streams_into(&mut buf);
assert!(!tx.has_pending());
assert!(
tx.overflowed(),
"the overflow survives the drain: the dropped RST never reached the wire"
);
}
#[test]
fn the_bound_follows_max_concurrent_streams() {
assert_eq!(pending_rst_bound(1), MIN_PENDING_RST_STREAMS);
assert_eq!(pending_rst_bound(100), MIN_PENDING_RST_STREAMS);
assert_eq!(pending_rst_bound(1000), MIN_PENDING_RST_STREAMS);
assert_eq!(pending_rst_bound(10_000), 40_000);
assert_eq!(H2ControlTx::new(10_000).max_pending(), 40_000);
for mcs in [1u32, 100, 250, 251, 1000, 10_000] {
assert!(pending_rst_bound(mcs) >= mcs as usize);
}
}
#[test]
fn clearing_the_queue_does_not_rewind_the_lifetime_counter() {
let mut tx = H2ControlTx::new(100);
let mut rst_sent = HashSet::new();
let mut readiness = readiness();
for id in [1, 3, 5] {
tx.enqueue_rst(Some(&mut rst_sent), &mut readiness, id, H2Error::Cancel);
}
tx.clear_pending();
assert!(!tx.has_pending());
assert_eq!(
tx.lifetime_queued(),
3,
"the lifetime count survives a queue clear"
);
}
}