1use std::time::Duration;
2
3use crate::error::ConfigValidationError;
4use crate::protocol::constants;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum AckNackFlushProfile {
8 LowLatency,
9 Balanced,
10 Throughput,
11 Custom,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum AckNackPriority {
16 NackFirst,
17 AckFirst,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum BackpressureMode {
22 Delay,
23 Shed,
24 Disconnect,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum CongestionProfile {
29 Conservative,
30 HighLatency,
31 Custom,
32}
33
34#[derive(Debug, Clone, Copy)]
35pub struct AckNackFlushSettings {
36 pub ack_flush_interval: Duration,
37 pub nack_flush_interval: Duration,
38 pub ack_max_ranges_per_datagram: usize,
39 pub nack_max_ranges_per_datagram: usize,
40 pub ack_nack_priority: AckNackPriority,
41}
42
43#[derive(Debug, Clone, Copy)]
44pub struct CongestionSettings {
45 pub resend_rto: Duration,
46 pub min_resend_rto: Duration,
47 pub max_resend_rto: Duration,
48 pub initial_congestion_window: f64,
49 pub min_congestion_window: f64,
50 pub max_congestion_window: f64,
51 pub congestion_slow_start_threshold: f64,
52 pub congestion_additive_gain: f64,
53 pub congestion_multiplicative_decrease_nack: f64,
54 pub congestion_multiplicative_decrease_timeout: f64,
55 pub congestion_high_rtt_threshold_ms: f64,
56 pub congestion_high_rtt_additive_scale: f64,
57 pub congestion_nack_backoff_cooldown: Duration,
58}
59
60impl AckNackFlushProfile {
61 pub fn settings(self) -> AckNackFlushSettings {
62 match self {
63 Self::LowLatency => AckNackFlushSettings {
64 ack_flush_interval: Duration::from_millis(4),
65 nack_flush_interval: Duration::from_millis(1),
66 ack_max_ranges_per_datagram: 24,
67 nack_max_ranges_per_datagram: 64,
68 ack_nack_priority: AckNackPriority::NackFirst,
69 },
70 Self::Balanced => AckNackFlushSettings {
71 ack_flush_interval: Duration::from_millis(10),
72 nack_flush_interval: Duration::from_millis(2),
73 ack_max_ranges_per_datagram: 48,
74 nack_max_ranges_per_datagram: 96,
75 ack_nack_priority: AckNackPriority::NackFirst,
76 },
77 Self::Throughput => AckNackFlushSettings {
78 ack_flush_interval: Duration::from_millis(24),
79 nack_flush_interval: Duration::from_millis(4),
80 ack_max_ranges_per_datagram: 96,
81 nack_max_ranges_per_datagram: 128,
82 ack_nack_priority: AckNackPriority::NackFirst,
83 },
84 Self::Custom => AckNackFlushSettings {
85 ack_flush_interval: Duration::from_millis(10),
86 nack_flush_interval: Duration::from_millis(2),
87 ack_max_ranges_per_datagram: 48,
88 nack_max_ranges_per_datagram: 96,
89 ack_nack_priority: AckNackPriority::NackFirst,
90 },
91 }
92 }
93}
94
95impl CongestionProfile {
96 pub fn settings(self) -> CongestionSettings {
97 match self {
98 Self::Conservative => CongestionSettings {
99 resend_rto: Duration::from_millis(250),
100 min_resend_rto: Duration::from_millis(80),
101 max_resend_rto: Duration::from_millis(2_000),
102 initial_congestion_window: 64.0,
103 min_congestion_window: 8.0,
104 max_congestion_window: 1024.0,
105 congestion_slow_start_threshold: 128.0,
106 congestion_additive_gain: 1.0,
107 congestion_multiplicative_decrease_nack: 0.85,
108 congestion_multiplicative_decrease_timeout: 0.6,
109 congestion_high_rtt_threshold_ms: 180.0,
110 congestion_high_rtt_additive_scale: 0.6,
111 congestion_nack_backoff_cooldown: Duration::from_millis(50),
112 },
113 Self::HighLatency => CongestionSettings {
114 resend_rto: Duration::from_millis(350),
115 min_resend_rto: Duration::from_millis(120),
116 max_resend_rto: Duration::from_millis(3_000),
117 initial_congestion_window: 48.0,
118 min_congestion_window: 8.0,
119 max_congestion_window: 768.0,
120 congestion_slow_start_threshold: 96.0,
121 congestion_additive_gain: 0.85,
122 congestion_multiplicative_decrease_nack: 0.92,
123 congestion_multiplicative_decrease_timeout: 0.75,
124 congestion_high_rtt_threshold_ms: 140.0,
125 congestion_high_rtt_additive_scale: 0.85,
126 congestion_nack_backoff_cooldown: Duration::from_millis(100),
127 },
128 Self::Custom => Self::Conservative.settings(),
129 }
130 }
131}
132
133#[derive(Debug, Clone)]
134pub struct SessionTunables {
135 pub ack_nack_flush_profile: AckNackFlushProfile,
136 pub congestion_profile: CongestionProfile,
137 pub ack_flush_interval: Duration,
138 pub nack_flush_interval: Duration,
139 pub ack_max_ranges_per_datagram: usize,
140 pub nack_max_ranges_per_datagram: usize,
141 pub ack_nack_priority: AckNackPriority,
142 pub ack_queue_capacity: usize,
143 pub backpressure_mode: BackpressureMode,
144 pub reliable_window: usize,
145 pub split_ttl: Duration,
146 pub max_split_parts: u32,
147 pub max_concurrent_splits: usize,
148 pub max_ordering_channels: usize,
149 pub max_ordered_pending_per_channel: usize,
150 pub max_order_gap: u32,
151 pub resend_rto: Duration,
152 pub min_resend_rto: Duration,
153 pub max_resend_rto: Duration,
154 pub initial_congestion_window: f64,
155 pub min_congestion_window: f64,
156 pub max_congestion_window: f64,
157 pub congestion_slow_start_threshold: f64,
158 pub congestion_additive_gain: f64,
159 pub congestion_multiplicative_decrease_nack: f64,
160 pub congestion_multiplicative_decrease_timeout: f64,
161 pub congestion_high_rtt_threshold_ms: f64,
162 pub congestion_high_rtt_additive_scale: f64,
163 pub congestion_nack_backoff_cooldown: Duration,
164 pub pacing_enabled: bool,
165 pub pacing_start_full: bool,
166 pub pacing_gain: f64,
167 pub pacing_min_rate_bytes_per_sec: f64,
168 pub pacing_max_rate_bytes_per_sec: f64,
169 pub pacing_max_burst_bytes: usize,
170 pub outgoing_queue_max_frames: usize,
171 pub outgoing_queue_max_bytes: usize,
172 pub outgoing_queue_soft_ratio: f64,
173 pub best_effort_zeroize_dropped_payloads: bool,
176}
177
178impl Default for SessionTunables {
179 fn default() -> Self {
180 let ack_nack_profile = AckNackFlushProfile::Balanced;
181 let ack_nack_settings = ack_nack_profile.settings();
182 let congestion_profile = CongestionProfile::Conservative;
183 let congestion_settings = congestion_profile.settings();
184 Self {
185 ack_nack_flush_profile: ack_nack_profile,
186 congestion_profile,
187 ack_flush_interval: ack_nack_settings.ack_flush_interval,
188 nack_flush_interval: ack_nack_settings.nack_flush_interval,
189 ack_max_ranges_per_datagram: ack_nack_settings.ack_max_ranges_per_datagram,
190 nack_max_ranges_per_datagram: ack_nack_settings.nack_max_ranges_per_datagram,
191 ack_nack_priority: ack_nack_settings.ack_nack_priority,
192 ack_queue_capacity: 1024,
193 backpressure_mode: BackpressureMode::Shed,
194 reliable_window: constants::MAX_ACK_SEQUENCES as usize,
195 split_ttl: Duration::from_millis(constants::SPLIT_REASSEMBLY_TTL_MS),
196 max_split_parts: constants::MAX_SPLIT_PARTS,
197 max_concurrent_splits: constants::MAX_INFLIGHT_SPLIT_COMPOUNDS_PER_PEER,
198 max_ordering_channels: 16,
199 max_ordered_pending_per_channel: 2048,
200 max_order_gap: constants::MAX_ACK_SEQUENCES as u32,
201 resend_rto: congestion_settings.resend_rto,
202 min_resend_rto: congestion_settings.min_resend_rto,
203 max_resend_rto: congestion_settings.max_resend_rto,
204 initial_congestion_window: congestion_settings.initial_congestion_window,
205 min_congestion_window: congestion_settings.min_congestion_window,
206 max_congestion_window: congestion_settings.max_congestion_window,
207 congestion_slow_start_threshold: congestion_settings.congestion_slow_start_threshold,
208 congestion_additive_gain: congestion_settings.congestion_additive_gain,
209 congestion_multiplicative_decrease_nack: congestion_settings
210 .congestion_multiplicative_decrease_nack,
211 congestion_multiplicative_decrease_timeout: congestion_settings
212 .congestion_multiplicative_decrease_timeout,
213 congestion_high_rtt_threshold_ms: congestion_settings.congestion_high_rtt_threshold_ms,
214 congestion_high_rtt_additive_scale: congestion_settings
215 .congestion_high_rtt_additive_scale,
216 congestion_nack_backoff_cooldown: congestion_settings.congestion_nack_backoff_cooldown,
217 pacing_enabled: true,
218 pacing_start_full: true,
219 pacing_gain: 1.0,
220 pacing_min_rate_bytes_per_sec: 24.0 * 1024.0,
221 pacing_max_rate_bytes_per_sec: 32.0 * 1024.0 * 1024.0,
222 pacing_max_burst_bytes: 128 * 1024,
223 outgoing_queue_max_frames: 8192,
224 outgoing_queue_max_bytes: 8 * 1024 * 1024,
225 outgoing_queue_soft_ratio: 0.85,
226 best_effort_zeroize_dropped_payloads: false,
227 }
228 }
229}
230
231impl SessionTunables {
232 pub fn resolved_ack_nack_flush_settings(&self) -> AckNackFlushSettings {
233 match self.ack_nack_flush_profile {
234 AckNackFlushProfile::LowLatency
235 | AckNackFlushProfile::Balanced
236 | AckNackFlushProfile::Throughput => self.ack_nack_flush_profile.settings(),
237 AckNackFlushProfile::Custom => AckNackFlushSettings {
238 ack_flush_interval: self.ack_flush_interval,
239 nack_flush_interval: self.nack_flush_interval,
240 ack_max_ranges_per_datagram: self.ack_max_ranges_per_datagram,
241 nack_max_ranges_per_datagram: self.nack_max_ranges_per_datagram,
242 ack_nack_priority: self.ack_nack_priority,
243 },
244 }
245 }
246
247 pub fn resolved_congestion_settings(&self) -> CongestionSettings {
248 match self.congestion_profile {
249 CongestionProfile::Conservative | CongestionProfile::HighLatency => {
250 self.congestion_profile.settings()
251 }
252 CongestionProfile::Custom => CongestionSettings {
253 resend_rto: self.resend_rto,
254 min_resend_rto: self.min_resend_rto,
255 max_resend_rto: self.max_resend_rto,
256 initial_congestion_window: self.initial_congestion_window,
257 min_congestion_window: self.min_congestion_window,
258 max_congestion_window: self.max_congestion_window,
259 congestion_slow_start_threshold: self.congestion_slow_start_threshold,
260 congestion_additive_gain: self.congestion_additive_gain,
261 congestion_multiplicative_decrease_nack: self
262 .congestion_multiplicative_decrease_nack,
263 congestion_multiplicative_decrease_timeout: self
264 .congestion_multiplicative_decrease_timeout,
265 congestion_high_rtt_threshold_ms: self.congestion_high_rtt_threshold_ms,
266 congestion_high_rtt_additive_scale: self.congestion_high_rtt_additive_scale,
267 congestion_nack_backoff_cooldown: self.congestion_nack_backoff_cooldown,
268 },
269 }
270 }
271
272 pub fn validate(&self) -> Result<(), ConfigValidationError> {
273 let ack_nack = self.resolved_ack_nack_flush_settings();
274 let congestion = self.resolved_congestion_settings();
275 if ack_nack.ack_flush_interval.is_zero() {
276 return Err(ConfigValidationError::new(
277 "SessionTunables",
278 "ack_flush_interval",
279 "must be > 0",
280 ));
281 }
282 if ack_nack.nack_flush_interval.is_zero() {
283 return Err(ConfigValidationError::new(
284 "SessionTunables",
285 "nack_flush_interval",
286 "must be > 0",
287 ));
288 }
289 if ack_nack.ack_max_ranges_per_datagram == 0 {
290 return Err(ConfigValidationError::new(
291 "SessionTunables",
292 "ack_max_ranges_per_datagram",
293 "must be >= 1",
294 ));
295 }
296 if ack_nack.nack_max_ranges_per_datagram == 0 {
297 return Err(ConfigValidationError::new(
298 "SessionTunables",
299 "nack_max_ranges_per_datagram",
300 "must be >= 1",
301 ));
302 }
303
304 if self.ack_queue_capacity == 0 {
305 return Err(ConfigValidationError::new(
306 "SessionTunables",
307 "ack_queue_capacity",
308 "must be >= 1",
309 ));
310 }
311 if self.reliable_window == 0 {
312 return Err(ConfigValidationError::new(
313 "SessionTunables",
314 "reliable_window",
315 "must be >= 1",
316 ));
317 }
318 if self.split_ttl.is_zero() {
319 return Err(ConfigValidationError::new(
320 "SessionTunables",
321 "split_ttl",
322 "must be > 0",
323 ));
324 }
325 if self.max_split_parts == 0 {
326 return Err(ConfigValidationError::new(
327 "SessionTunables",
328 "max_split_parts",
329 "must be >= 1",
330 ));
331 }
332 if self.max_concurrent_splits == 0 {
333 return Err(ConfigValidationError::new(
334 "SessionTunables",
335 "max_concurrent_splits",
336 "must be >= 1",
337 ));
338 }
339 if self.max_ordering_channels == 0 {
340 return Err(ConfigValidationError::new(
341 "SessionTunables",
342 "max_ordering_channels",
343 "must be >= 1",
344 ));
345 }
346 if self.max_ordered_pending_per_channel == 0 {
347 return Err(ConfigValidationError::new(
348 "SessionTunables",
349 "max_ordered_pending_per_channel",
350 "must be >= 1",
351 ));
352 }
353 if self.max_order_gap == 0 {
354 return Err(ConfigValidationError::new(
355 "SessionTunables",
356 "max_order_gap",
357 "must be >= 1",
358 ));
359 }
360 if congestion.min_resend_rto.is_zero() {
361 return Err(ConfigValidationError::new(
362 "SessionTunables",
363 "min_resend_rto",
364 "must be > 0",
365 ));
366 }
367 if congestion.max_resend_rto.is_zero() {
368 return Err(ConfigValidationError::new(
369 "SessionTunables",
370 "max_resend_rto",
371 "must be > 0",
372 ));
373 }
374 if congestion.min_resend_rto > congestion.max_resend_rto {
375 return Err(ConfigValidationError::new(
376 "SessionTunables",
377 "min_resend_rto",
378 "must be <= max_resend_rto",
379 ));
380 }
381 if congestion.resend_rto < congestion.min_resend_rto
382 || congestion.resend_rto > congestion.max_resend_rto
383 {
384 return Err(ConfigValidationError::new(
385 "SessionTunables",
386 "resend_rto",
387 "must be within [min_resend_rto, max_resend_rto]",
388 ));
389 }
390
391 validate_positive_f64(
392 congestion.initial_congestion_window,
393 "initial_congestion_window",
394 )?;
395 validate_positive_f64(congestion.min_congestion_window, "min_congestion_window")?;
396 validate_positive_f64(congestion.max_congestion_window, "max_congestion_window")?;
397 if congestion.min_congestion_window > congestion.max_congestion_window {
398 return Err(ConfigValidationError::new(
399 "SessionTunables",
400 "min_congestion_window",
401 "must be <= max_congestion_window",
402 ));
403 }
404 if congestion.initial_congestion_window < congestion.min_congestion_window
405 || congestion.initial_congestion_window > congestion.max_congestion_window
406 {
407 return Err(ConfigValidationError::new(
408 "SessionTunables",
409 "initial_congestion_window",
410 "must be within [min_congestion_window, max_congestion_window]",
411 ));
412 }
413 if congestion.congestion_slow_start_threshold < congestion.min_congestion_window
414 || congestion.congestion_slow_start_threshold > congestion.max_congestion_window
415 {
416 return Err(ConfigValidationError::new(
417 "SessionTunables",
418 "congestion_slow_start_threshold",
419 "must be within [min_congestion_window, max_congestion_window]",
420 ));
421 }
422 validate_positive_f64(
423 congestion.congestion_additive_gain,
424 "congestion_additive_gain",
425 )?;
426 validate_fraction(
427 congestion.congestion_multiplicative_decrease_nack,
428 "congestion_multiplicative_decrease_nack",
429 )?;
430 validate_fraction(
431 congestion.congestion_multiplicative_decrease_timeout,
432 "congestion_multiplicative_decrease_timeout",
433 )?;
434 validate_positive_f64(
435 congestion.congestion_high_rtt_threshold_ms,
436 "congestion_high_rtt_threshold_ms",
437 )?;
438 if !congestion.congestion_high_rtt_additive_scale.is_finite()
439 || congestion.congestion_high_rtt_additive_scale <= 0.0
440 || congestion.congestion_high_rtt_additive_scale > 1.0
441 {
442 return Err(ConfigValidationError::new(
443 "SessionTunables",
444 "congestion_high_rtt_additive_scale",
445 "must be finite and within (0, 1]",
446 ));
447 }
448 if congestion.congestion_nack_backoff_cooldown.is_zero() {
449 return Err(ConfigValidationError::new(
450 "SessionTunables",
451 "congestion_nack_backoff_cooldown",
452 "must be > 0",
453 ));
454 }
455
456 validate_positive_f64(self.pacing_gain, "pacing_gain")?;
457 validate_positive_f64(
458 self.pacing_min_rate_bytes_per_sec,
459 "pacing_min_rate_bytes_per_sec",
460 )?;
461 validate_positive_f64(
462 self.pacing_max_rate_bytes_per_sec,
463 "pacing_max_rate_bytes_per_sec",
464 )?;
465 if self.pacing_min_rate_bytes_per_sec > self.pacing_max_rate_bytes_per_sec {
466 return Err(ConfigValidationError::new(
467 "SessionTunables",
468 "pacing_min_rate_bytes_per_sec",
469 "must be <= pacing_max_rate_bytes_per_sec",
470 ));
471 }
472 if self.pacing_max_burst_bytes == 0 {
473 return Err(ConfigValidationError::new(
474 "SessionTunables",
475 "pacing_max_burst_bytes",
476 "must be >= 1",
477 ));
478 }
479 if self.outgoing_queue_max_frames == 0 {
480 return Err(ConfigValidationError::new(
481 "SessionTunables",
482 "outgoing_queue_max_frames",
483 "must be >= 1",
484 ));
485 }
486 if self.outgoing_queue_max_bytes == 0 {
487 return Err(ConfigValidationError::new(
488 "SessionTunables",
489 "outgoing_queue_max_bytes",
490 "must be >= 1",
491 ));
492 }
493 if !self.outgoing_queue_soft_ratio.is_finite()
494 || self.outgoing_queue_soft_ratio <= 0.0
495 || self.outgoing_queue_soft_ratio >= 1.0
496 {
497 return Err(ConfigValidationError::new(
498 "SessionTunables",
499 "outgoing_queue_soft_ratio",
500 "must be finite and within (0, 1)",
501 ));
502 }
503
504 Ok(())
505 }
506}
507
508fn validate_positive_f64(value: f64, field: &'static str) -> Result<(), ConfigValidationError> {
509 if !value.is_finite() || value <= 0.0 {
510 return Err(ConfigValidationError::new(
511 "SessionTunables",
512 field,
513 "must be finite and > 0",
514 ));
515 }
516 Ok(())
517}
518
519fn validate_fraction(value: f64, field: &'static str) -> Result<(), ConfigValidationError> {
520 if !value.is_finite() || value <= 0.0 || value >= 1.0 {
521 return Err(ConfigValidationError::new(
522 "SessionTunables",
523 field,
524 "must be finite and within (0, 1)",
525 ));
526 }
527 Ok(())
528}
529
530#[cfg(test)]
531mod tests {
532 use std::time::Duration;
533
534 use super::{AckNackFlushProfile, AckNackPriority, CongestionProfile, SessionTunables};
535
536 #[test]
537 fn validate_accepts_default_values() {
538 SessionTunables::default()
539 .validate()
540 .expect("default tunables must be valid");
541 }
542
543 #[test]
544 fn validate_rejects_zero_ack_queue_capacity() {
545 let tunables = SessionTunables {
546 ack_queue_capacity: 0,
547 ..SessionTunables::default()
548 };
549 let err = tunables
550 .validate()
551 .expect_err("ack_queue_capacity=0 must be rejected");
552 assert_eq!(err.config, "SessionTunables");
553 assert_eq!(err.field, "ack_queue_capacity");
554 }
555
556 #[test]
557 fn validate_rejects_zero_custom_ack_flush_interval() {
558 let tunables = SessionTunables {
559 ack_nack_flush_profile: AckNackFlushProfile::Custom,
560 ack_flush_interval: Duration::ZERO,
561 ..SessionTunables::default()
562 };
563 let err = tunables
564 .validate()
565 .expect_err("ack_flush_interval=0 must be rejected for custom policy");
566 assert_eq!(err.config, "SessionTunables");
567 assert_eq!(err.field, "ack_flush_interval");
568 }
569
570 #[test]
571 fn profile_resolution_uses_profile_defaults_when_not_custom() {
572 let tunables = SessionTunables {
573 ack_nack_flush_profile: AckNackFlushProfile::LowLatency,
574 ack_flush_interval: Duration::from_secs(99),
575 nack_flush_interval: Duration::from_secs(99),
576 ack_max_ranges_per_datagram: 1,
577 nack_max_ranges_per_datagram: 1,
578 ack_nack_priority: AckNackPriority::AckFirst,
579 ..SessionTunables::default()
580 };
581
582 let resolved = tunables.resolved_ack_nack_flush_settings();
583 assert_eq!(resolved.ack_flush_interval, Duration::from_millis(4));
584 assert_eq!(resolved.nack_flush_interval, Duration::from_millis(1));
585 assert_eq!(resolved.ack_max_ranges_per_datagram, 24);
586 assert_eq!(resolved.nack_max_ranges_per_datagram, 64);
587 assert_eq!(resolved.ack_nack_priority, AckNackPriority::NackFirst);
588 }
589
590 #[test]
591 fn congestion_profile_resolution_uses_profile_defaults_when_not_custom() {
592 let tunables = SessionTunables {
593 congestion_profile: CongestionProfile::HighLatency,
594 resend_rto: Duration::from_millis(10),
595 min_resend_rto: Duration::from_millis(5),
596 max_resend_rto: Duration::from_millis(20),
597 initial_congestion_window: 1.0,
598 min_congestion_window: 1.0,
599 max_congestion_window: 2.0,
600 congestion_slow_start_threshold: 1.0,
601 congestion_additive_gain: 9.0,
602 congestion_multiplicative_decrease_nack: 0.2,
603 congestion_multiplicative_decrease_timeout: 0.2,
604 congestion_high_rtt_threshold_ms: 9.0,
605 congestion_high_rtt_additive_scale: 0.2,
606 congestion_nack_backoff_cooldown: Duration::from_millis(1),
607 ..SessionTunables::default()
608 };
609
610 let resolved = tunables.resolved_congestion_settings();
611 assert_eq!(resolved.resend_rto, Duration::from_millis(350));
612 assert_eq!(resolved.min_resend_rto, Duration::from_millis(120));
613 assert_eq!(resolved.max_resend_rto, Duration::from_millis(3_000));
614 assert!((resolved.congestion_multiplicative_decrease_nack - 0.92).abs() < f64::EPSILON);
615 assert!((resolved.congestion_multiplicative_decrease_timeout - 0.75).abs() < f64::EPSILON);
616 }
617
618 #[test]
619 fn validate_ignores_manual_congestion_fields_when_profile_is_not_custom() {
620 let tunables = SessionTunables {
621 congestion_profile: CongestionProfile::Conservative,
622 min_resend_rto: Duration::from_millis(500),
623 max_resend_rto: Duration::from_millis(100),
624 ..SessionTunables::default()
625 };
626
627 tunables
628 .validate()
629 .expect("manual congestion fields must be ignored for non-custom congestion profile");
630 }
631
632 #[test]
633 fn validate_rejects_invalid_custom_congestion_ranges() {
634 let tunables = SessionTunables {
635 congestion_profile: CongestionProfile::Custom,
636 min_resend_rto: Duration::from_millis(500),
637 max_resend_rto: Duration::from_millis(100),
638 ..SessionTunables::default()
639 };
640 let err = tunables
641 .validate()
642 .expect_err("custom congestion profile must reject invalid ranges");
643 assert_eq!(err.config, "SessionTunables");
644 assert_eq!(err.field, "min_resend_rto");
645 }
646}