#![allow(clippy::items_after_statements)]
use std::net::SocketAddr;
use std::time::{Duration, Instant};
use super::stream_id::Dir;
use super::testfix::{Solo, Wire, drain, max_data_frame, parse_frames, ramp, t0, v4, write_all};
use super::timers::TimerKind;
use crate::constants::{AMPLIFICATION_FACTOR, DEAD_TIMEOUT};
fn c_addr() -> SocketAddr {
v4(9, 41_000)
}
fn room(s: &Solo) -> Option<u64> {
s.conn
.amplification_budget()
.map(|(sent, recv)| (AMPLIFICATION_FACTOR * recv).saturating_sub(sent))
}
const GAPS: u64 = 48;
const BULK: usize = 9_000;
fn roamed_with_a_starved_budget(start: Instant) -> (Solo, Option<Instant>) {
let mut s = Solo::installed_at(start);
let mut credit = 1_000_000u64;
for _ in 0..GAPS {
let _burned = s.peer.seal(&[]);
credit += 100_000;
let _ = s.deliver(start, &max_data_frame(credit));
}
let r = s.conn.open(Dir::Uni).expect("§9.1: our own uni space");
let blocked = write_all(&mut s.conn, start, r, &ramp(0, BULK));
assert_eq!(blocked, 0, "fixture: {BULK} B fits the initial credit");
let _ = drain(&mut s.conn);
for _ in 0..64 {
if let Some(at) = s.conn.timer(TimerKind::AckDelay) {
s.conn.handle_timeout(at);
let _ = drain(&mut s.conn);
}
let headroom = s.conn.congestion_window() - s.conn.bytes_in_flight();
if headroom < 34 {
break;
}
let want = headroom.min(1_000);
let payload = want - 30 - 1 - if want - 32 < 64 { 1 } else { 2 };
let before = s.conn.bytes_in_flight();
s.conn
.send_datagram(start, &ramp(0, payload as usize))
.expect("§11: a sub-maximum datagram is accepted");
let _ = drain(&mut s.conn);
if s.conn.bytes_in_flight() == before {
break;
}
}
assert_eq!(
s.conn.congestion_window() - s.conn.bytes_in_flight(),
0,
"fixture: §14.5's gate must be shut, or the post-roam packet leaves \
and arms the death clock — see this function's header"
);
let d = s.deliver_from(start, c_addr(), &max_data_frame(credit + 1));
(s, d.deadline)
}
struct Run {
at: Instant,
sent: usize,
died: bool,
premise: Option<(bool, bool, Option<u64>)>,
ack: Option<(usize, usize)>,
}
fn run_driver(s: &mut Solo, start: Instant, mut next: Option<Instant>, horizon: Instant) -> Run {
const STALL_CAP: u32 = 4;
let mut at = start;
let mut sent = 0usize;
let mut premise = None;
let mut ack = None;
let mut stalled = 0u32;
for step in 0..4096 {
let (Some(when), true) = (next, at <= horizon) else {
return Run {
at,
sent,
died: false,
premise,
ack,
};
};
if when <= at {
stalled += 1;
assert!(
stalled <= STALL_CAP,
"spin: {stalled} consecutive turns without advancing, at step {step}"
);
} else {
stalled = 0;
}
let when = when.max(at);
if when > horizon {
return Run {
at,
sent,
died: false,
premise,
ack,
};
}
at = when;
s.conn.handle_timeout(at);
let d = drain(&mut s.conn);
sent += d.transmits().len();
if step == 0 {
ack = match d.transmits().as_slice() {
[t] => match parse_frames(&s.peer.open_dgram(&t.data)).as_slice() {
[Wire::Ack { ranges, .. }] => Some((t.data.len(), ranges.len())),
_ => None,
},
_ => None,
};
premise = s
.conn
.liveness()
.map(|l| (l.owes_passive_keepalive(), l.is_armed(), room(s)));
}
if d.closed().is_some() {
return Run {
at,
sent,
died: true,
premise,
ack,
};
}
next = d.deadline;
}
panic!("the driver never reached a fixed point");
}
#[test]
fn a_starved_passive_keepalive_must_not_park_the_connection_forever() {
let start = t0();
let (mut s, deadline) = roamed_with_a_starved_budget(start);
let horizon = start + DEAD_TIMEOUT + Duration::from_secs(5);
let run = run_driver(&mut s, start, deadline, horizon);
let (debt, armed, room_left) = run
.premise
.expect("fixture: the session is installed at the first driver step");
assert!(
debt,
"premise: §7.5's passive keepalive must be owed in the parked state"
);
assert!(
!armed,
"premise: §7.4's death clock must be disarmed — that is the whole \
subject. `armed` is cleared by the same receive that set the debt."
);
assert!(
room_left.is_some_and(|r| r < 30),
"premise: §7.3's budget must refuse the 30-byte keepalive, or \
`keepalive_can_leave()` is true and nothing is suppressed; \
room was {room_left:?}"
);
let (bytes, ranges) = run
.ack
.expect("the one post-roam packet must be a lone ACK (§12.4's standalone)");
assert!(
ranges >= 20,
"§12.2: the ACK must be filling the room with range pairs, not \
costing ~35 bytes; got {ranges} pairs in {bytes} B"
);
assert_eq!(
bytes, 105,
"§7.3 + ruling 203: a pure ACK is sized to the room — the roam \
funded 3 × 35 = 105 datagram bytes and the ACK took all of them"
);
assert!(
run.died || run.sent > 1,
"§7.4/§7.5: a connection owing a keepalive it cannot send must still \
be reaped at DEAD_TIMEOUT (or recover and send). It did neither: \
parked at {:?} after start with {} packet(s) sent since the roam, \
and the shell is asleep on Timeout(None) for ever.",
run.at - start,
run.sent
);
if run.died {
assert_eq!(
run.at - start,
DEAD_TIMEOUT,
"§7.4: the deadline is `last_authenticated_recv + DEAD_TIMEOUT`"
);
}
}
#[test]
fn the_backstop_does_not_reap_a_connection_whose_peer_comes_back() {
let start = t0();
let (mut s, deadline) = roamed_with_a_starved_budget(start);
let run = run_driver(&mut s, start, deadline, start + Duration::from_secs(1));
assert!(
!run.died,
"fixture: the hold is reached long before DEAD_TIMEOUT"
);
let back = start + Duration::from_secs(10);
let d = s.deliver_from(back, c_addr(), &max_data_frame(9_999_999));
assert!(
d.closed().is_none(),
"§7.2: an authenticated, window-fresh receive must not kill the \
connection it just refreshed"
);
assert!(
room(&s).is_some_and(|r| r >= 30),
"§7.3: the receive re-funds the budget, so the keepalive can leave"
);
assert!(
s.conn.timer(TimerKind::Keepalive).is_some(),
"§7.5: with the hold lifted the passive keepalive is announced again"
);
let run = run_driver(
&mut s,
back,
d.deadline,
back + DEAD_TIMEOUT + Duration::from_secs(5),
);
assert!(
run.sent >= 1,
"§7.5: the dance resumes — a keepalive must leave once the budget \
admits it"
);
assert_eq!(
run.died.then(|| run.at - start),
Some(Duration::from_secs(10) + DEAD_TIMEOUT),
"§7.4: the clock is anchored to the **latest** authenticated receive, \
so the session outlives the original anchor by exactly the 10 s the \
peer's return bought it"
);
}