Skip to content

Commit dc86400

Browse files
aochagaviadjc
authored andcommitted
Introduce max_outgoing_bytes_per_second option
1 parent d8db7a0 commit dc86400

3 files changed

Lines changed: 102 additions & 10 deletions

File tree

quinn-proto/src/config/transport.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub struct TransportConfig {
4141
pub(crate) mtu_discovery_config: Option<MtuDiscoveryConfig>,
4242
pub(crate) pad_to_mtu: bool,
4343
pub(crate) ack_frequency_config: Option<AckFrequencyConfig>,
44+
pub(crate) max_outgoing_bytes_per_second: Option<u64>,
4445

4546
pub(crate) persistent_congestion_threshold: u32,
4647
pub(crate) keep_alive_interval: Option<Duration>,
@@ -243,6 +244,14 @@ impl TransportConfig {
243244
self
244245
}
245246

247+
/// Configures an outbound rate limit (in bytes per second) for each connection.
248+
///
249+
/// Defaults to `None`, which disables rate limiting.
250+
pub fn max_outgoing_bytes_per_second(&mut self, value: Option<u64>) -> &mut Self {
251+
self.max_outgoing_bytes_per_second = value;
252+
self
253+
}
254+
246255
/// Number of consecutive PTOs after which network is considered to be experiencing persistent congestion.
247256
pub fn persistent_congestion_threshold(&mut self, value: u32) -> &mut Self {
248257
self.persistent_congestion_threshold = value;
@@ -376,6 +385,7 @@ impl Default for TransportConfig {
376385
mtu_discovery_config: Some(MtuDiscoveryConfig::default()),
377386
pad_to_mtu: false,
378387
ack_frequency_config: None,
388+
max_outgoing_bytes_per_second: None,
379389

380390
persistent_congestion_threshold: 3,
381391
keep_alive_interval: None,
@@ -413,6 +423,7 @@ impl fmt::Debug for TransportConfig {
413423
mtu_discovery_config,
414424
pad_to_mtu,
415425
ack_frequency_config,
426+
max_outgoing_bytes_per_second,
416427
persistent_congestion_threshold,
417428
keep_alive_interval,
418429
crypto_buffer_size,
@@ -442,6 +453,10 @@ impl fmt::Debug for TransportConfig {
442453
.field("mtu_discovery_config", mtu_discovery_config)
443454
.field("pad_to_mtu", pad_to_mtu)
444455
.field("ack_frequency_config", ack_frequency_config)
456+
.field(
457+
"max_outgoing_bytes_per_second",
458+
max_outgoing_bytes_per_second,
459+
)
445460
.field(
446461
"persistent_congestion_threshold",
447462
persistent_congestion_threshold,

quinn-proto/src/connection/pacing.rs

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,36 @@ pub(super) struct Pacer {
1717
last_window: u64,
1818
last_mtu: u16,
1919
tokens: u64,
20+
max_bytes_per_second: Option<u64>,
2021
prev: Instant,
2122
}
2223

2324
impl Pacer {
2425
/// Obtains a new [`Pacer`].
25-
pub(super) fn new(smoothed_rtt: Duration, window: u64, mtu: u16, now: Instant) -> Self {
26+
pub(super) fn new(
27+
smoothed_rtt: Duration,
28+
window: u64,
29+
mtu: u16,
30+
max_bytes_per_second: Option<u64>,
31+
now: Instant,
32+
) -> Self {
33+
let window = rate_limited_window(smoothed_rtt, window, max_bytes_per_second);
2634
let capacity = optimal_capacity(smoothed_rtt, window, mtu);
2735
Self {
2836
capacity,
2937
last_window: window,
3038
last_mtu: mtu,
3139
tokens: capacity,
40+
max_bytes_per_second,
3241
prev: now,
3342
}
3443
}
3544

45+
/// Obtains the `max_bytes_per_second` used when this [`Pacer`] was constructed.
46+
pub(crate) fn max_bytes_per_second(&self) -> Option<u64> {
47+
self.max_bytes_per_second
48+
}
49+
3650
/// Record that a packet has been transmitted.
3751
pub(super) fn on_transmit(&mut self, packet_length: u16) {
3852
self.tokens = self.tokens.saturating_sub(packet_length.into())
@@ -58,6 +72,7 @@ impl Pacer {
5872
"zero-sized congestion control window is nonsense"
5973
);
6074

75+
let window = rate_limited_window(smoothed_rtt, window, self.max_bytes_per_second);
6176
if window != self.last_window || mtu != self.last_mtu {
6277
self.capacity = optimal_capacity(smoothed_rtt, window, mtu);
6378

@@ -147,6 +162,27 @@ fn optimal_capacity(smoothed_rtt: Duration, window: u64, mtu: u16) -> u64 {
147162
)
148163
}
149164

165+
/// Clamps the window to limit the sending rate to `max_bytes_per_second`.
166+
///
167+
/// If `max_bytes_per_second` is `None`, the original window is returned.
168+
fn rate_limited_window(
169+
smoothed_rtt: Duration,
170+
window: u64,
171+
max_bytes_per_second: Option<u64>,
172+
) -> u64 {
173+
let Some(max_bytes_per_second) = max_bytes_per_second else {
174+
return window;
175+
};
176+
177+
let rate_window = max_bytes_per_second as f64 * smoothed_rtt.as_secs_f64();
178+
179+
// the pacer refills tokens at x1.25 speed, so we shrink the window to cancel out the speedup
180+
// (otherwise the actual sending rate could be higher than `max_bytes_per_second`)
181+
let adjusted_rate_window = (rate_window / 1.25).round();
182+
183+
Ord::min(window, Ord::max(adjusted_rate_window as u64, 1))
184+
}
185+
150186
/// Period of traffic to batch together on a reasonably fast connection
151187
const TARGET_BURST_INTERVAL: Duration = Duration::from_millis(2);
152188

@@ -173,17 +209,17 @@ mod tests {
173209
let rtt = Duration::from_micros(400);
174210

175211
assert!(
176-
Pacer::new(rtt, 30000, 1500, new_instant)
212+
Pacer::new(rtt, 30000, 1500, None, new_instant)
177213
.delay(Duration::from_micros(0), 0, 1500, 1, old_instant)
178214
.is_none()
179215
);
180216
assert!(
181-
Pacer::new(rtt, 30000, 1500, new_instant)
217+
Pacer::new(rtt, 30000, 1500, None, new_instant)
182218
.delay(Duration::from_micros(0), 1600, 1500, 1, old_instant)
183219
.is_none()
184220
);
185221
assert!(
186-
Pacer::new(rtt, 30000, 1500, new_instant)
222+
Pacer::new(rtt, 30000, 1500, None, new_instant)
187223
.delay(Duration::from_micros(0), 1500, 1500, 3000, old_instant)
188224
.is_none()
189225
);
@@ -196,18 +232,18 @@ mod tests {
196232
let rtt = Duration::from_millis(50);
197233
let now = Instant::now();
198234

199-
let pacer = Pacer::new(rtt, window, mtu, now);
235+
let pacer = Pacer::new(rtt, window, mtu, None, now);
200236
assert_eq!(
201237
pacer.capacity,
202238
(window as u128 * TARGET_BURST_INTERVAL.as_nanos() / rtt.as_nanos()) as u64
203239
);
204240
assert_eq!(pacer.tokens, pacer.capacity);
205241

206-
let pacer = Pacer::new(Duration::from_millis(0), window, mtu, now);
242+
let pacer = Pacer::new(Duration::from_millis(0), window, mtu, None, now);
207243
assert_eq!(pacer.capacity, MAX_BURST_SIZE * mtu as u64);
208244
assert_eq!(pacer.tokens, pacer.capacity);
209245

210-
let pacer = Pacer::new(rtt, 1, mtu, now);
246+
let pacer = Pacer::new(rtt, 1, mtu, None, now);
211247
assert_eq!(pacer.capacity, mtu as u64);
212248
assert_eq!(pacer.tokens, pacer.capacity);
213249
}
@@ -219,7 +255,7 @@ mod tests {
219255
let rtt = Duration::from_millis(50);
220256
let now = Instant::now();
221257

222-
let mut pacer = Pacer::new(rtt, window, mtu, now);
258+
let mut pacer = Pacer::new(rtt, window, mtu, None, now);
223259
assert_eq!(
224260
pacer.capacity,
225261
(window as u128 * TARGET_BURST_INTERVAL.as_nanos() / rtt.as_nanos()) as u64
@@ -258,7 +294,7 @@ mod tests {
258294
let rtt = Duration::from_millis(50);
259295
let old_instant = Instant::now();
260296

261-
let mut pacer = Pacer::new(rtt, window, mtu, old_instant);
297+
let mut pacer = Pacer::new(rtt, window, mtu, None, old_instant);
262298
let packet_capacity = pacer.capacity / mtu as u64;
263299

264300
for _ in 0..packet_capacity {
@@ -321,4 +357,38 @@ mod tests {
321357
);
322358
assert_eq!(pacer.tokens, pacer.capacity);
323359
}
360+
361+
#[test]
362+
fn computes_pause_correctly_for_rate_limited() {
363+
let window = 2_000_000u64;
364+
let mtu = 1000;
365+
let rtt = Duration::from_millis(50);
366+
let old_instant = Instant::now();
367+
368+
let mut pacer = Pacer::new(rtt, window, mtu, Some(2_000), old_instant);
369+
assert_eq!(
370+
pacer.delay(rtt, 1_000, mtu, window, old_instant),
371+
None,
372+
"When capacity is available packets should be sent immediately"
373+
);
374+
pacer.on_transmit(mtu);
375+
376+
let actual_delay = pacer
377+
.delay(rtt, 1_000, mtu, window, old_instant)
378+
.expect("Send must be delayed")
379+
.duration_since(old_instant);
380+
381+
let expected_delay = Duration::from_millis(500);
382+
let diff = actual_delay.abs_diff(expected_delay);
383+
384+
// Allow up to 2ns difference due to rounding
385+
assert!(
386+
diff < Duration::from_nanos(2),
387+
"expected ≈ {expected_delay:?}, got {actual_delay:?} (diff {diff:?})"
388+
);
389+
390+
// Should be able to send after a while
391+
let now = old_instant + expected_delay / 2;
392+
assert_eq!(pacer.delay(rtt, 500, mtu, window, now), None);
393+
}
324394
}

quinn-proto/src/connection/paths.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ impl PathData {
7575
config.initial_rtt,
7676
congestion.initial_window(),
7777
config.get_initial_mtu(),
78+
config.max_outgoing_bytes_per_second,
7879
now,
7980
),
8081
congestion,
@@ -118,7 +119,13 @@ impl PathData {
118119
Self {
119120
remote,
120121
rtt: prev.rtt,
121-
pacing: Pacer::new(smoothed_rtt, congestion.window(), prev.current_mtu(), now),
122+
pacing: Pacer::new(
123+
smoothed_rtt,
124+
congestion.window(),
125+
prev.current_mtu(),
126+
prev.pacing.max_bytes_per_second(),
127+
now,
128+
),
122129
sending_ecn: true,
123130
congestion,
124131
challenge: None,

0 commit comments

Comments
 (0)