use crate::constants;
use super::stream_id::{Dir, MAX_STREAMS_CEILING};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct FlowWindows {
pub(crate) stream: u64,
pub(crate) connection: u64,
}
impl Default for FlowWindows {
fn default() -> Self {
Self {
stream: constants::INITIAL_MAX_STREAM_DATA,
connection: constants::INITIAL_MAX_DATA,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub(crate) enum Violation {
#[error("flow control: the peer exceeded advertised credit")]
FlowControl,
#[error("stream limit: the peer opened beyond the cumulative limit")]
StreamLimit,
#[error("stream state: the peer named a stream it could not send on")]
StreamState,
#[error("final size: the frame contradicts a pinned final size")]
FinalSize,
#[error(
"protocol violation: reassembly ranges exceed the credit-derived ceiling (floor REASSEMBLY_CHUNKS_MAX)"
)]
Reassembly,
}
impl Violation {
pub(crate) fn code(self) -> u64 {
match self {
Violation::FlowControl => constants::FLOW_CONTROL_ERROR,
Violation::StreamLimit => constants::STREAM_LIMIT_ERROR,
Violation::StreamState => constants::STREAM_STATE_ERROR,
Violation::FinalSize => constants::FINAL_SIZE_ERROR,
Violation::Reassembly => constants::PROTOCOL_VIOLATION,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct CreditWindow {
window: u64,
consumed: u64,
last_advertised: u64,
pending_announce: bool,
}
impl CreditWindow {
pub(crate) fn new(window: u64) -> Self {
Self {
window,
consumed: 0,
last_advertised: window,
pending_announce: false,
}
}
pub(crate) fn configured(window: u64, ratified: u64) -> Self {
Self {
pending_announce: window > ratified,
..Self::new(window)
}
}
pub(crate) fn take_announcement(&mut self) -> bool {
std::mem::take(&mut self.pending_announce)
}
pub(crate) fn advertised(&self) -> u64 {
self.last_advertised
}
pub(crate) fn consumed(&self) -> u64 {
self.consumed
}
pub(crate) fn consume(&mut self, n: u64) {
self.consumed = self.consumed.saturating_add(n);
}
pub(crate) fn consume_to(&mut self, value: u64) -> u64 {
let delta = value.saturating_sub(self.consumed);
self.consumed = self.consumed.max(value);
delta
}
pub(crate) fn take_grant(&mut self) -> Option<u64> {
let prospective = self.consumed.saturating_add(self.window);
let threshold = self.window / constants::CREDIT_REGRANT_DIVISOR;
if prospective.saturating_sub(self.last_advertised) >= threshold {
self.last_advertised = prospective;
Some(prospective)
} else {
None
}
}
}
pub(crate) struct Flow {
recv_charged: u64,
recv: CreditWindow,
send_charged: u64,
send_max_data: u64,
local_max_streams: [u64; 2],
ungranted: [u64; 2],
remote_max_streams: [u64; 2],
}
impl Flow {
pub(crate) fn new() -> Self {
Self::with_window(constants::INITIAL_MAX_DATA)
}
pub(crate) fn with_window(window: u64) -> Self {
let initial = [
constants::INITIAL_MAX_STREAMS_BIDI,
constants::INITIAL_MAX_STREAMS_UNI,
];
Self {
recv_charged: 0,
recv: CreditWindow::configured(window, constants::INITIAL_MAX_DATA),
send_charged: 0,
send_max_data: constants::INITIAL_MAX_DATA,
local_max_streams: initial,
ungranted: [0, 0],
remote_max_streams: initial,
}
}
pub(crate) fn check_recv_charge(&self, delta: u64) -> Result<(), Violation> {
match self.recv_charged.checked_add(delta) {
Some(total) if total <= self.recv.advertised() => Ok(()),
_ => Err(Violation::FlowControl),
}
}
pub(crate) fn charge_recv(&mut self, delta: u64) {
self.recv_charged = self.recv_charged.saturating_add(delta);
}
pub(crate) fn recv_charged(&self) -> u64 {
self.recv_charged
}
pub(crate) fn recv_window(&mut self) -> &mut CreditWindow {
&mut self.recv
}
pub(crate) fn recv_advertised(&self) -> u64 {
self.recv.advertised()
}
pub(crate) fn send_room(&self) -> u64 {
self.send_max_data.saturating_sub(self.send_charged)
}
pub(crate) fn charge_send(&mut self, delta: u64) {
self.send_charged = self.send_charged.saturating_add(delta);
}
pub(crate) fn on_max_data(&mut self, max: u64) -> bool {
if max > self.send_max_data {
self.send_max_data = max;
true
} else {
false
}
}
pub(crate) fn send_max_data(&self) -> u64 {
self.send_max_data
}
pub(crate) fn remote_max_streams(&self, dir: Dir) -> u64 {
self.remote_max_streams[dir.slot()]
}
pub(crate) fn local_max_streams(&self, dir: Dir) -> u64 {
self.local_max_streams[dir.slot()]
}
pub(crate) fn on_max_streams(&mut self, dir: Dir, max: u64) -> bool {
let slot = dir.slot();
if max > self.remote_max_streams[slot] {
self.remote_max_streams[slot] = max;
true
} else {
false
}
}
pub(crate) fn max_streams_is_representable(max: u64) -> bool {
max <= MAX_STREAMS_CEILING
}
pub(crate) fn grant_stream_credit(&mut self, dir: Dir) {
let slot = dir.slot();
self.ungranted[slot] = self.ungranted[slot].saturating_add(1);
}
pub(crate) fn take_streams_grant(&mut self, dir: Dir, peer_opened: u64) -> Option<u64> {
let slot = dir.slot();
let ungranted = self.ungranted[slot];
if ungranted == 0 {
return None;
}
let remaining = self.local_max_streams[slot].saturating_sub(peer_opened);
let batch = constants::STREAMS_CREDIT_BATCH;
if ungranted >= batch || remaining <= batch {
self.ungranted[slot] = 0;
let limit = self.local_max_streams[slot]
.saturating_add(ungranted)
.min(MAX_STREAMS_CEILING);
self.local_max_streams[slot] = limit;
Some(limit)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_fresh_window_owes_no_grant() {
let mut w = CreditWindow::new(constants::INITIAL_MAX_STREAM_DATA);
assert_eq!(w.advertised(), constants::INITIAL_MAX_STREAM_DATA);
assert_eq!(w.take_grant(), None);
}
#[test]
fn the_regrant_threshold_is_two_sided_at_half_the_window() {
let window = constants::INITIAL_MAX_STREAM_DATA;
let half = window / constants::CREDIT_REGRANT_DIVISOR;
let mut below = CreditWindow::new(window);
below.consume(half - 1);
assert_eq!(below.take_grant(), None);
let mut at = CreditWindow::new(window);
at.consume(half);
assert_eq!(at.take_grant(), Some(half + window));
assert_eq!(at.take_grant(), None);
}
#[test]
fn consume_to_is_monotone_and_idempotent() {
let mut w = CreditWindow::new(constants::INITIAL_MAX_STREAM_DATA);
w.consume(100);
assert_eq!(w.consume_to(250), 150);
assert_eq!(w.consumed(), 250);
assert_eq!(w.consume_to(250), 0);
assert_eq!(w.consume_to(10), 0);
assert_eq!(w.consumed(), 250);
}
#[test]
fn the_max_streams_ceiling_boundary_is_two_sided() {
assert!(Flow::max_streams_is_representable(MAX_STREAMS_CEILING - 1));
assert!(Flow::max_streams_is_representable(MAX_STREAMS_CEILING));
assert!(!Flow::max_streams_is_representable(MAX_STREAMS_CEILING + 1));
}
#[test]
fn both_max_streams_triggers_use_the_batch_constant() {
let batch = constants::STREAMS_CREDIT_BATCH;
let mut flow = Flow::new();
for _ in 0..batch - 1 {
flow.grant_stream_credit(Dir::Uni);
assert_eq!(flow.take_streams_grant(Dir::Uni, 0), None);
}
flow.grant_stream_credit(Dir::Uni);
assert_eq!(
flow.take_streams_grant(Dir::Uni, 0),
Some(constants::INITIAL_MAX_STREAMS_UNI + batch)
);
let mut flow = Flow::new();
flow.grant_stream_credit(Dir::Uni);
let plenty = constants::INITIAL_MAX_STREAMS_UNI - batch - 1;
assert_eq!(flow.take_streams_grant(Dir::Uni, plenty), None);
let tight = constants::INITIAL_MAX_STREAMS_UNI - batch;
assert_eq!(
flow.take_streams_grant(Dir::Uni, tight),
Some(constants::INITIAL_MAX_STREAMS_UNI + 1)
);
}
#[test]
fn credit_frames_apply_as_monotone_max() {
let mut flow = Flow::new();
assert!(!flow.on_max_data(constants::INITIAL_MAX_DATA));
assert!(!flow.on_max_data(constants::INITIAL_MAX_DATA - 1));
assert!(flow.on_max_data(constants::INITIAL_MAX_DATA + 1));
assert_eq!(flow.send_max_data(), constants::INITIAL_MAX_DATA + 1);
assert!(!flow.on_max_streams(Dir::Bi, constants::INITIAL_MAX_STREAMS_BIDI));
assert!(flow.on_max_streams(Dir::Bi, constants::INITIAL_MAX_STREAMS_BIDI + 4));
assert_eq!(
flow.remote_max_streams(Dir::Bi),
constants::INITIAL_MAX_STREAMS_BIDI + 4
);
}
#[test]
fn the_connection_bound_uses_checked_arithmetic() {
let mut flow = Flow::new();
flow.charge_recv(1_000);
assert_eq!(
flow.check_recv_charge(u64::MAX),
Err(Violation::FlowControl)
);
assert_eq!(
flow.check_recv_charge(constants::INITIAL_MAX_DATA),
Err(Violation::FlowControl)
);
assert_eq!(
flow.check_recv_charge(constants::INITIAL_MAX_DATA - 1_000),
Ok(())
);
}
#[test]
fn every_violation_carries_its_registry_code() {
assert_eq!(Violation::FlowControl.code(), constants::FLOW_CONTROL_ERROR);
assert_eq!(Violation::StreamLimit.code(), constants::STREAM_LIMIT_ERROR);
assert_eq!(Violation::StreamState.code(), constants::STREAM_STATE_ERROR);
assert_eq!(Violation::FinalSize.code(), constants::FINAL_SIZE_ERROR);
assert_eq!(Violation::Reassembly.code(), constants::PROTOCOL_VIOLATION);
}
}