A server accepting a fresh Connect can send before its Accept is acknowledged, so no ack of the new generation prunes the acknowledged sizes; only `end_session` clearing them keeps the ended generation's bytes from counting against the new window. Rust, TypeScript, Python and C# each pin it: 4 MiB acknowledged above a lost packet, a fresh Connect, and the server's first 1 MiB send of the new generation goes out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
310 lines
12 KiB
Rust
310 lines
12 KiB
Rust
//! The sender's flow window: a reliable packet goes on the wire only while its
|
|
//! sequence is at most 1,023 above the lowest unacked one and the payload above
|
|
//! that sequence stays within 4 MiB. Everything else waits in order in the queue.
|
|
|
|
use anyhow::Result;
|
|
use cultnet_rs::*;
|
|
|
|
const CONNECTION_ID: u32 = 0x464c_4f57;
|
|
const MIB: usize = 1024 * 1024;
|
|
|
|
fn session(initial_sequence: u32) -> CultNetRudpSession {
|
|
CultNetRudpSession::new(CultNetRudpSessionOptions {
|
|
connection_id: CONNECTION_ID,
|
|
initial_sequence: Some(initial_sequence),
|
|
resend_delay_ms: 25,
|
|
max_pending_reliable_packets: None,
|
|
})
|
|
}
|
|
|
|
/// A sender and a receiver that have shaken hands: the receiver's watermark is
|
|
/// seeded by the sender's Connect, and the sender's Connect is acknowledged, so
|
|
/// nothing is pending on the sender.
|
|
fn connected_pair(
|
|
sender_initial_sequence: u32,
|
|
receiver_initial_sequence: u32,
|
|
) -> Result<(CultNetRudpSession, CultNetRudpSession)> {
|
|
let mut sender = session(sender_initial_sequence);
|
|
let mut receiver = session(receiver_initial_sequence);
|
|
let connect = sender.create_connect(0, Vec::new())?;
|
|
let accept = receiver.accept_connect(&connect, 0, Vec::new())?;
|
|
sender.receive(&accept, 0)?;
|
|
Ok((sender, receiver))
|
|
}
|
|
|
|
fn connected(initial_sequence: u32) -> Result<CultNetRudpSession> {
|
|
Ok(connected_pair(initial_sequence, 900)?.0)
|
|
}
|
|
|
|
fn reliable() -> CultNetRudpSendOptions {
|
|
CultNetRudpSendOptions {
|
|
reliable: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// What `send_many` puts on the wire now; the rest of the packet waits queued.
|
|
fn send(session: &mut CultNetRudpSession, payload_bytes: usize) -> Result<Vec<CultNetRudpPacket>> {
|
|
session.send_many("state", vec![7; payload_bytes], reliable(), None)
|
|
}
|
|
|
|
fn ack_for(sequence: u32) -> CultNetRudpPacket {
|
|
CultNetRudpPacket {
|
|
packet_type: CultNetRudpPacketType::Ack,
|
|
connection_id: CONNECTION_ID,
|
|
sequence: 0,
|
|
ack: sequence,
|
|
ack_mask: 0,
|
|
channel_id: "control".into(),
|
|
reliable: false,
|
|
ordered: false,
|
|
sequenced: false,
|
|
fragment_id: 0,
|
|
fragment_index: 0,
|
|
fragment_count: 0,
|
|
payload: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn sequences(packets: &[CultNetRudpPacket]) -> Vec<u32> {
|
|
packets.iter().map(|packet| packet.sequence).collect()
|
|
}
|
|
|
|
/// Sends `g`, then acks every packet up to and including the one 1,023 above
|
|
/// it, leaving `g` the only unacked sequence. Returns `g`.
|
|
fn lose_g_and_fill_the_span(session: &mut CultNetRudpSession) -> Result<CultNetRudpPacket> {
|
|
let g = send(session, 1)?.remove(0);
|
|
for offset in 1..=1_023u32 {
|
|
let sent = send(session, 1)?;
|
|
assert_eq!(sequences(&sent), vec![g.sequence + offset]);
|
|
session.receive(&ack_for(g.sequence + offset), 1)?;
|
|
}
|
|
Ok(g)
|
|
}
|
|
|
|
#[test]
|
|
fn a_lost_packet_holds_the_sender_1023_sequences_ahead_and_is_still_delivered() -> Result<()> {
|
|
let (mut sender, mut receiver) = connected_pair(1, 100)?;
|
|
let g = send(&mut sender, 1)?.remove(0);
|
|
// g is lost. Everything after it is delivered and acked, until the sender stops.
|
|
let mut admitted_after_g = 0u32;
|
|
for _ in 0..4_200 {
|
|
for packet in send(&mut sender, 1)? {
|
|
admitted_after_g += 1;
|
|
receiver.receive(&packet, 1)?;
|
|
sender.receive(&receiver.create_ack(), 1)?;
|
|
}
|
|
}
|
|
assert_eq!(admitted_after_g, 1_023);
|
|
assert_eq!(sender.queued_reliable_packet_count(), 4_200 - 1_023);
|
|
|
|
let retransmit = sender
|
|
.due_resends(1_000)
|
|
.into_iter()
|
|
.find(|packet| packet.sequence == g.sequence)
|
|
.expect("g is still pending and due");
|
|
let delivered = receiver.receive(&retransmit, 1_000)?.delivered;
|
|
assert_eq!(delivered.len(), 1);
|
|
assert_eq!(delivered[0].sequence, g.sequence);
|
|
|
|
let promoted = sender
|
|
.receive(&receiver.create_ack_for_received(g.sequence), 1_001)?
|
|
.ready_to_send;
|
|
assert_eq!(promoted.first().map(|packet| packet.sequence), Some(g.sequence + 1_024));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn a_direct_send_past_the_span_is_refused_without_consuming_a_sequence() -> Result<()> {
|
|
let mut sender = connected(1)?;
|
|
let g = lose_g_and_fill_the_span(&mut sender)?;
|
|
assert!(sender.send("state", vec![7], reliable()).is_err());
|
|
assert_eq!(sender.queued_reliable_packet_count(), 0);
|
|
sender.receive(&ack_for(g.sequence), 2)?;
|
|
let next = sender.send("state", vec![7], reliable())?;
|
|
assert_eq!(next.sequence, g.sequence + 1_024);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn payload_above_the_lowest_unacked_sequence_is_bounded_by_4_mib() -> Result<()> {
|
|
let mut sender = connected(1)?;
|
|
let g = send(&mut sender, MIB)?.remove(0);
|
|
// The lowest unacked packet's own bytes do not count: four more MiB fit above it.
|
|
for _ in 0..4 {
|
|
assert_eq!(send(&mut sender, MIB)?.len(), 1);
|
|
}
|
|
assert!(send(&mut sender, MIB)?.is_empty());
|
|
assert!(send(&mut sender, MIB)?.is_empty());
|
|
assert_eq!(sender.queued_reliable_packet_count(), 2);
|
|
|
|
// Acking g moves the floor up one packet: exactly one MiB more fits.
|
|
let promoted = sender.receive(&ack_for(g.sequence), 1)?.ready_to_send;
|
|
assert_eq!(sequences(&promoted), vec![g.sequence + 5]);
|
|
assert_eq!(sender.queued_reliable_packet_count(), 1);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn a_packet_larger_than_the_window_goes_out_alone_and_waits_behind_anything_unacked() -> Result<()> {
|
|
let mut sender = connected(1)?;
|
|
assert_eq!(send(&mut sender, 5 * MIB)?.len(), 1);
|
|
let mut sender = connected(1)?;
|
|
let g = send(&mut sender, 1)?.remove(0);
|
|
assert!(send(&mut sender, 5 * MIB)?.is_empty());
|
|
let promoted = sender.receive(&ack_for(g.sequence), 1)?.ready_to_send;
|
|
assert_eq!(sequences(&promoted), vec![g.sequence + 1]);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn a_small_packet_does_not_overtake_a_queued_large_one() -> Result<()> {
|
|
let mut sender = connected(1)?;
|
|
let g = send(&mut sender, 1)?.remove(0);
|
|
assert_eq!(send(&mut sender, 3 * MIB)?.len(), 1);
|
|
assert!(send(&mut sender, 2 * MIB)?.is_empty());
|
|
// A direct send refuses instead of queueing behind it, and takes no sequence.
|
|
assert!(sender.send("state", vec![7], reliable()).is_err());
|
|
assert_eq!(sender.queued_reliable_packet_count(), 1);
|
|
// One byte would fit above g, but the 2 MiB packet is ahead of it.
|
|
assert!(send(&mut sender, 1)?.is_empty());
|
|
let promoted = sender.receive(&ack_for(g.sequence), 1)?.ready_to_send;
|
|
assert_eq!(sequences(&promoted), vec![g.sequence + 2, g.sequence + 3]);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn acknowledged_bytes_above_a_lost_packet_still_count_against_4_mib() -> Result<()> {
|
|
let mut sender = connected(1)?;
|
|
let g = send(&mut sender, 1)?.remove(0);
|
|
// g is lost. Each 1 MiB frame above it is delivered and acknowledged, and
|
|
// the receiver still holds it behind the gap, so it keeps counting.
|
|
for offset in 1..=4u32 {
|
|
let sent = send(&mut sender, MIB)?;
|
|
assert_eq!(sequences(&sent), vec![g.sequence + offset]);
|
|
sender.receive(&ack_for(g.sequence + offset), 1)?;
|
|
}
|
|
assert!(send(&mut sender, MIB)?.is_empty());
|
|
assert_eq!(sender.queued_reliable_packet_count(), 1);
|
|
assert!(sender.send("state", vec![7; MIB], reliable()).is_err());
|
|
|
|
// g arrives: nothing is held behind a gap any more, and the queue drains.
|
|
let promoted = sender.receive(&ack_for(g.sequence), 2)?.ready_to_send;
|
|
assert_eq!(sequences(&promoted), vec![g.sequence + 5]);
|
|
// The bytes above the old gap no longer count: another MiB fits.
|
|
assert_eq!(sequences(&send(&mut sender, MIB)?), vec![g.sequence + 6]);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn acknowledged_bytes_count_against_the_lowest_unacked_sequence_as_it_advances() -> Result<()> {
|
|
let mut sender = connected(1)?;
|
|
let g = send(&mut sender, 1)?.remove(0);
|
|
let h = send(&mut sender, 1)?.remove(0);
|
|
for _ in 0..3 {
|
|
send(&mut sender, MIB)?;
|
|
}
|
|
// g and the 3 MiB above h are acknowledged; h is lost.
|
|
for offset in [2, 3, 4, 0] {
|
|
sender.receive(&ack_for(g.sequence + offset), 1)?;
|
|
}
|
|
assert_eq!(sequences(&send(&mut sender, MIB)?), vec![g.sequence + 5]);
|
|
// 4 MiB sit above h, acknowledged or not: one more byte does not fit.
|
|
assert!(send(&mut sender, 1)?.is_empty());
|
|
// h arrives, the floor moves to g + 5, and the byte fits.
|
|
let promoted = sender.receive(&ack_for(h.sequence), 2)?.ready_to_send;
|
|
assert_eq!(sequences(&promoted), vec![g.sequence + 6]);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn acknowledged_bytes_stop_counting_when_the_lowest_unacked_packet_expires() -> Result<()> {
|
|
let mut sender = connected(1)?;
|
|
let expiring = CultNetRudpSendOptions {
|
|
reliable: true,
|
|
now_ms: 0,
|
|
reliable_expire_after_ms: Some(100),
|
|
..Default::default()
|
|
};
|
|
let g = sender
|
|
.send_many("state", vec![7], expiring, None)?
|
|
.remove(0);
|
|
for offset in 1..=4u32 {
|
|
assert_eq!(send(&mut sender, MIB)?.len(), 1);
|
|
sender.receive(&ack_for(g.sequence + offset), 1)?;
|
|
}
|
|
assert!(sender.send("state", vec![7; MIB], reliable()).is_err());
|
|
|
|
// g's deadline passes and it leaves the window; nothing is unacked, so the
|
|
// acknowledged bytes no longer sit above any gap.
|
|
let at = |now_ms| CultNetRudpSendOptions {
|
|
reliable: true,
|
|
now_ms,
|
|
..Default::default()
|
|
};
|
|
let first = sender.send_many("state", vec![7], at(200), None)?;
|
|
assert_eq!(first.len(), 1);
|
|
for _ in 0..4 {
|
|
assert_eq!(sender.send_many("state", vec![7; MIB], at(200), None)?.len(), 1);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn a_direct_send_is_not_refused_for_bytes_whose_lowest_packet_has_expired() -> Result<()> {
|
|
let mut sender = connected(1)?;
|
|
let expiring = CultNetRudpSendOptions {
|
|
reliable: true,
|
|
now_ms: 0,
|
|
reliable_expire_after_ms: Some(100),
|
|
..Default::default()
|
|
};
|
|
let g = sender
|
|
.send_many("state", vec![7], expiring, None)?
|
|
.remove(0);
|
|
for offset in 1..=4u32 {
|
|
assert_eq!(send(&mut sender, MIB)?.len(), 1);
|
|
sender.receive(&ack_for(g.sequence + offset), 1)?;
|
|
}
|
|
assert!(sender.send("state", vec![7; MIB], reliable()).is_err());
|
|
|
|
// The first call after g's deadline is a direct send: it measures the
|
|
// window without g, as `send_many` would, and goes on the wire.
|
|
let at_200 = CultNetRudpSendOptions {
|
|
reliable: true,
|
|
now_ms: 200,
|
|
..Default::default()
|
|
};
|
|
let sent = sender.send("state", vec![7; MIB], at_200)?;
|
|
assert_eq!(sent.sequence, g.sequence + 5);
|
|
assert_eq!(sender.outstanding_reliable_packet_count(), 1);
|
|
Ok(())
|
|
}
|
|
|
|
/// A server that accepts a fresh Connect may send before its Accept is
|
|
/// acknowledged, so no acknowledgement of the new generation has moved the
|
|
/// floor yet. Bytes the ended generation had acknowledged above its own lost
|
|
/// packet belong to no receiver's hold any more and must not count.
|
|
#[test]
|
|
fn a_new_generation_on_the_server_does_not_inherit_acknowledged_bytes() -> Result<()> {
|
|
let mut server = session(500);
|
|
let mut first_client = session(1);
|
|
let connect = first_client.create_connect(0, Vec::new())?;
|
|
let accept = server.accept_connect(&connect, 0, Vec::new())?;
|
|
server.receive(&ack_for(accept.sequence), 0)?;
|
|
let g = send(&mut server, 1)?.remove(0);
|
|
for offset in 1..=4u32 {
|
|
assert_eq!(sequences(&send(&mut server, MIB)?), vec![g.sequence + offset]);
|
|
server.receive(&ack_for(g.sequence + offset), 1)?;
|
|
}
|
|
assert!(send(&mut server, MIB)?.is_empty());
|
|
|
|
let mut second_client = session(200_000);
|
|
let connect = second_client.create_connect(2, Vec::new())?;
|
|
let accept = server.accept_connect(&connect, 2, Vec::new())?;
|
|
assert_eq!(accept.packet_type, CultNetRudpPacketType::Accept);
|
|
// The Accept is unacknowledged and the only thing pending: 1 MiB fits above it.
|
|
let sent = server.send("state", vec![7; MIB], reliable())?;
|
|
assert_eq!(sent.sequence, accept.sequence + 1);
|
|
Ok(())
|
|
}
|