Skip to main content

raknet_rust/session/
tunables.rs

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    /// Best-effort zeroize for payload buffers that are dropped or abandoned
174    /// before successful delivery. This may add CPU cost under heavy shedding.
175    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}