Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions quinn-proto/src/config/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub struct TransportConfig {
pub(crate) mtu_discovery_config: Option<MtuDiscoveryConfig>,
pub(crate) pad_to_mtu: bool,
pub(crate) ack_frequency_config: Option<AckFrequencyConfig>,
pub(crate) bundled_ack_interval: Duration,
pub(crate) max_outgoing_bytes_per_second: Option<u64>,

pub(crate) persistent_congestion_threshold: u32,
Expand Down Expand Up @@ -385,6 +386,7 @@ impl Default for TransportConfig {
mtu_discovery_config: Some(MtuDiscoveryConfig::default()),
pad_to_mtu: false,
ack_frequency_config: None,
bundled_ack_interval: Duration::from_millis(100),
max_outgoing_bytes_per_second: None,

persistent_congestion_threshold: 3,
Expand Down Expand Up @@ -423,6 +425,7 @@ impl fmt::Debug for TransportConfig {
mtu_discovery_config,
pad_to_mtu,
ack_frequency_config,
bundled_ack_interval,
max_outgoing_bytes_per_second,
persistent_congestion_threshold,
keep_alive_interval,
Expand Down Expand Up @@ -453,6 +456,7 @@ impl fmt::Debug for TransportConfig {
.field("mtu_discovery_config", mtu_discovery_config)
.field("pad_to_mtu", pad_to_mtu)
.field("ack_frequency_config", ack_frequency_config)
.field("bundled_ack_interval", bundled_ack_interval)
.field(
"max_outgoing_bytes_per_second",
max_outgoing_bytes_per_second,
Expand Down
39 changes: 35 additions & 4 deletions quinn-proto/src/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ pub struct Connection {
permit_idle_reset: bool,
/// Negotiated idle timeout
idle_timeout: Option<Duration>,
/// The time we send next bundled ACK
next_bundled_ack_time: Option<Instant>,
timers: TimerTable,
/// Number of packets received which could not be authenticated
authentication_failures: u64,
Expand Down Expand Up @@ -335,6 +337,7 @@ impl Connection {
ack_frequency: AckFrequencyState::new(get_max_ack_delay(
&TransportParameters::default(),
)),
next_bundled_ack_time: None,

pto_count: 0,

Expand Down Expand Up @@ -789,13 +792,14 @@ impl Connection {
// especially important with ack delay, since the peer might not
// have gotten any other ACK for the data earlier on.
if !self.spaces[space_id].pending_acks.ranges().is_empty() {
Self::populate_acks(
Self::try_populate_acks(
now,
self.receiving_ecn,
&mut SentFrames::default(),
&mut self.spaces[space_id],
buf,
&mut self.stats,
usize::MAX,
);
}

Expand Down Expand Up @@ -900,6 +904,7 @@ impl Connection {
if sent.largest_acked.is_some() {
self.spaces[space_id].pending_acks.acks_sent();
self.timers.stop(Timer::MaxAckDelay);
self.next_bundled_ack_time = Some(now + self.config.bundled_ack_interval);
}

// Keep information about the packet around until it gets finalized
Expand Down Expand Up @@ -3062,6 +3067,7 @@ impl Connection {
{
self.timers
.set(Timer::MaxAckDelay, now + self.ack_frequency.max_ack_delay);
self.next_bundled_ack_time = Some(now);
}

// Issue stream ID credit due to ACKs of outgoing finish/resets and incoming finish/resets
Expand Down Expand Up @@ -3225,13 +3231,14 @@ impl Connection {

// ACK
if space.pending_acks.can_send() {
Self::populate_acks(
Self::try_populate_acks(
now,
self.receiving_ecn,
&mut sent,
space,
buf,
&mut self.stats,
max_size,
);
}

Expand Down Expand Up @@ -3440,20 +3447,38 @@ impl Connection {
self.stats.frame_tx.stream += sent.stream_frames.len() as u64;
}

// Bundle ACK for data frames when there is room for them
if (sent_datagrams || !sent.stream_frames.is_empty())
&& sent.largest_acked.is_none()
&& self.next_bundled_ack_time.is_some_and(|time| time <= now)
&& space.pending_acks.can_send_with_other_frames()
{
Self::try_populate_acks(
now,
self.receiving_ecn,
&mut sent,
space,
buf,
&mut self.stats,
max_size,
);
}

sent
}

/// Write pending ACKs into a buffer
///
/// This method assumes ACKs are pending, and should only be called if
/// `!PendingAcks::ranges().is_empty()` returns `true`.
fn populate_acks(
fn try_populate_acks(
now: Instant,
receiving_ecn: bool,
sent: &mut SentFrames,
space: &mut PacketSpace,
buf: &mut Vec<u8>,
stats: &mut ConnectionStats,
max_size: usize,
) {
debug_assert!(!space.pending_acks.ranges().is_empty());

Expand All @@ -3464,7 +3489,6 @@ impl Connection {
} else {
None
};
sent.largest_acked = space.pending_acks.ranges().max();

let delay_micros = space.pending_acks.ack_delay(now).as_micros() as u64;

Expand All @@ -3478,7 +3502,14 @@ impl Connection {
delay_micros
);

let original_len = buf.len();
frame::Ack::encode(delay as _, space.pending_acks.ranges(), ecn, buf);
if buf.len() > max_size {
// The ACK frame is too large. Remove it.
buf.truncate(original_len);
return;
}
sent.largest_acked = space.pending_acks.ranges().max();
stats.frame_tx.acks += 1;
}

Expand Down
7 changes: 6 additions & 1 deletion quinn-proto/src/connection/spaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -662,11 +662,16 @@ impl PendingAcks {
.map(|earliest_unacked| earliest_unacked + max_ack_delay)
}

/// Whether any ACK frames can be sent
/// Whether any ACK frames can be sent in ack initiated packet
pub(super) fn can_send(&self) -> bool {
self.immediate_ack_required && !self.ranges.is_empty()
}

/// Whether any ACK frames can be sent withing data initiated packet
pub(super) fn can_send_with_other_frames(&self) -> bool {
!self.ranges.is_empty()
}

/// Returns the delay since the packet with the largest packet number was received
pub(super) fn ack_delay(&self, now: Instant) -> Duration {
self.largest_packet
Expand Down
28 changes: 28 additions & 0 deletions quinn-proto/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3490,6 +3490,34 @@ fn voluntary_ack_with_large_datagrams() {
);
}

#[test]
fn ack_bundled_with_datagrams() {
let _guard = subscribe();
let mut pair = Pair::default();
let (client_ch, server_ch) = pair.connect();

// Send packet from client and then send from server. the packet from server should include ACKs
pair.client_datagrams(client_ch)
.send(vec![0; 1].into(), false)
.unwrap();
pair.drive_client();
pair.drive_server();
pair.server_datagrams(server_ch)
.send(vec![0; 1].into(), false)
.unwrap();
pair.drive_server();

let server_tx_after_datagrams = pair.server_conn_mut(client_ch).stats().udp_tx.bytes;

pair.drive();

// No more packets should be sent from server since ACK to the first packet has been sent
assert_eq!(
server_tx_after_datagrams,
pair.server_conn_mut(client_ch).stats().udp_tx.bytes
);
}

/// Verify that dropping oversized datagrams will trigger a DatagramsUnblocked event.
#[test]
fn oversized_datagrams_trigger_unblock() {
Expand Down