1mod cubic;
14
15use core::cmp::Ordering;
16use core::num::{NonZeroU8, NonZeroU32};
17use core::time::Duration;
18
19use netstack3_base::{EffectiveMss, Instant, Mss, SackBlocks, SeqNum, WindowSize};
20
21use crate::internal::sack_scoreboard::SackScoreboard;
22
23pub(crate) const DUP_ACK_THRESHOLD: u8 = 3;
27
28#[derive(Debug)]
30struct CongestionControlParams {
31 ssthresh: u32,
33 cwnd: u32,
35 mss: EffectiveMss,
37}
38
39impl CongestionControlParams {
40 fn with_mss(mss: EffectiveMss) -> Self {
41 let mss_u32 = u32::from(mss);
42 let cwnd = if mss_u32 > 2190 {
52 mss_u32 * 2
53 } else if mss_u32 > 1095 {
54 mss_u32 * 3
55 } else {
56 mss_u32 * 4
57 };
58 Self { cwnd, ssthresh: u32::MAX, mss }
59 }
60
61 fn rounded_cwnd(&self) -> CongestionWindow {
62 CongestionWindow::new(self.cwnd, self.mss)
63 }
64}
65
66mod cwnd {
67 use super::*;
68 #[derive(Debug, Copy, Clone)]
73 #[cfg_attr(test, derive(Eq, PartialEq))]
74 pub(crate) struct CongestionWindow {
75 cwnd: u32,
76 mss: EffectiveMss,
77 }
78
79 impl CongestionWindow {
80 pub(super) fn new(cwnd: u32, mss: EffectiveMss) -> Self {
81 let mss_u32 = u32::from(mss);
82 Self { cwnd: cwnd / mss_u32 * mss_u32, mss }
83 }
84
85 pub(crate) fn cwnd(&self) -> u32 {
86 self.cwnd
87 }
88
89 pub(crate) fn mss(&self) -> EffectiveMss {
90 self.mss
91 }
92 }
93}
94pub(crate) use cwnd::CongestionWindow;
95
96#[derive(Debug)]
104pub(crate) struct CongestionControl<I> {
105 params: CongestionControlParams,
106 sack_scoreboard: SackScoreboard,
107 algorithm: LossBasedAlgorithm<I>,
108 loss_recovery: Option<LossRecovery>,
110}
111
112#[derive(Debug)]
114enum LossBasedAlgorithm<I> {
115 Cubic(cubic::Cubic<I, true >),
116}
117
118impl<I: Instant> LossBasedAlgorithm<I> {
119 fn on_loss_detected(&mut self, params: &mut CongestionControlParams) {
125 match self {
126 LossBasedAlgorithm::Cubic(cubic) => cubic.on_loss_detected(params),
127 }
128 }
129
130 fn on_ack(
131 &mut self,
132 params: &mut CongestionControlParams,
133 bytes_acked: NonZeroU32,
134 now: I,
135 rtt: Duration,
136 ) {
137 match self {
138 LossBasedAlgorithm::Cubic(cubic) => cubic.on_ack(params, bytes_acked, now, rtt),
139 }
140 }
141
142 fn on_retransmission_timeout(&mut self, params: &mut CongestionControlParams) {
143 match self {
144 LossBasedAlgorithm::Cubic(cubic) => cubic.on_retransmission_timeout(params),
145 }
146 }
147}
148
149impl<I: Instant> CongestionControl<I> {
150 pub(super) fn preprocess_ack(
163 &mut self,
164 seg_ack: SeqNum,
165 snd_nxt: SeqNum,
166 seg_sack_blocks: &SackBlocks,
167 ) -> Option<bool> {
168 let Self { params, algorithm: _, loss_recovery, sack_scoreboard } = self;
169 let high_rxt = loss_recovery.as_ref().and_then(|lr| match lr {
170 LossRecovery::FastRecovery(_) => None,
171 LossRecovery::SackRecovery(sack_recovery) => sack_recovery.high_rxt(),
172 });
173 let is_dup_ack =
174 sack_scoreboard.process_ack(seg_ack, snd_nxt, high_rxt, seg_sack_blocks, params.mss);
175 (!seg_sack_blocks.is_empty()).then_some(is_dup_ack)
176 }
177
178 pub(super) fn on_will_send_segment(&mut self, seg_len: u32) {
184 let Self { params: _, sack_scoreboard, algorithm: _, loss_recovery: _ } = self;
185 sack_scoreboard.increment_pipe(seg_len);
191 }
192
193 pub(super) fn on_ack(
200 &mut self,
201 seg_ack: SeqNum,
202 bytes_acked: NonZeroU32,
203 now: I,
204 rtt: Option<Duration>,
205 ) -> bool {
206 let Self { params, algorithm, loss_recovery, sack_scoreboard: _ } = self;
207 let outcome = match loss_recovery {
209 None => LossRecoveryOnAckOutcome::None,
210 Some(LossRecovery::FastRecovery(fast_recovery)) => fast_recovery.on_ack(params),
211 Some(LossRecovery::SackRecovery(sack_recovery)) => sack_recovery.on_ack(seg_ack),
212 };
213
214 let recovered = match outcome {
215 LossRecoveryOnAckOutcome::None => false,
216 LossRecoveryOnAckOutcome::Discard { recovered } => {
217 *loss_recovery = None;
218 recovered
219 }
220 };
221
222 if let Some(rtt) = rtt {
228 algorithm.on_ack(params, bytes_acked, now, rtt);
229 }
230 recovered
231 }
232
233 pub(super) fn on_dup_ack(
238 &mut self,
239 seg_ack: SeqNum,
240 snd_nxt: SeqNum,
241 ) -> Option<LossRecoveryMode> {
242 let Self { params, algorithm, loss_recovery, sack_scoreboard } = self;
243 match loss_recovery {
244 None => {
245 if sack_scoreboard.has_sack_info() {
247 let mut sack_recovery = SackRecovery::new();
248 let started_loss_recovery = sack_recovery
249 .on_dup_ack(seg_ack, snd_nxt, sack_scoreboard)
250 .apply(params, algorithm);
251 *loss_recovery = Some(LossRecovery::SackRecovery(sack_recovery));
252 started_loss_recovery.then_some(LossRecoveryMode::SackRecovery)
253 } else {
254 *loss_recovery = Some(LossRecovery::FastRecovery(FastRecovery::new()));
255 None
256 }
257 }
258 Some(LossRecovery::SackRecovery(sack_recovery)) => sack_recovery
259 .on_dup_ack(seg_ack, snd_nxt, sack_scoreboard)
260 .apply(params, algorithm)
261 .then_some(LossRecoveryMode::SackRecovery),
262 Some(LossRecovery::FastRecovery(fast_recovery)) => fast_recovery
263 .on_dup_ack(params, algorithm, seg_ack)
264 .then_some(LossRecoveryMode::FastRecovery),
265 }
266 }
267
268 pub(super) fn on_retransmission_timeout(&mut self, snd_nxt: SeqNum) {
273 let Self { params, algorithm, loss_recovery, sack_scoreboard } = self;
274 sack_scoreboard.on_retransmission_timeout();
275 let discard_loss_recovery = match loss_recovery {
276 None | Some(LossRecovery::FastRecovery(_)) => true,
277 Some(LossRecovery::SackRecovery(sack_recovery)) => {
278 sack_recovery.on_retransmission_timeout(snd_nxt)
279 }
280 };
281 if discard_loss_recovery {
282 *loss_recovery = None;
283 }
284 algorithm.on_retransmission_timeout(params);
285 }
286
287 pub(super) fn slow_start_threshold(&self) -> u32 {
288 self.params.ssthresh
289 }
290
291 #[cfg(test)]
292 pub(super) fn pipe(&self) -> u32 {
293 self.sack_scoreboard.pipe()
294 }
295
296 #[cfg(test)]
298 pub(super) fn inflate_cwnd(&mut self, inflation: u32) {
299 self.params.cwnd += inflation;
300 }
301
302 pub(super) fn cubic_with_mss(mss: EffectiveMss) -> Self {
303 Self {
304 params: CongestionControlParams::with_mss(mss),
305 algorithm: LossBasedAlgorithm::Cubic(Default::default()),
306 loss_recovery: None,
307 sack_scoreboard: SackScoreboard::default(),
308 }
309 }
310
311 pub(super) fn mss(&self) -> EffectiveMss {
312 self.params.mss
313 }
314
315 pub(super) fn update_mss(&mut self, mss: Mss, snd_una: SeqNum, snd_nxt: SeqNum) {
316 let Self { params, sack_scoreboard, algorithm: _, loss_recovery } = self;
317 let orig = u32::from(params.mss);
318 params.mss.update_mss(mss);
319
320 if params.ssthresh == u32::MAX {
331 params.cwnd = params.cwnd.saturating_div(orig).saturating_mul(u32::from(params.mss));
332 }
333
334 let high_rxt = loss_recovery.as_ref().and_then(|lr| match lr {
338 LossRecovery::FastRecovery(_) => None,
339 LossRecovery::SackRecovery(sack_recovery) => sack_recovery.high_rxt(),
340 });
341 sack_scoreboard.on_mss_update(snd_una, snd_nxt, high_rxt, params.mss);
342 }
343
344 pub(super) fn inspect_cwnd(&self) -> CongestionWindow {
349 self.params.rounded_cwnd()
350 }
351
352 pub(super) fn inspect_loss_recovery_mode(&self) -> Option<LossRecoveryMode> {
362 self.loss_recovery.as_ref().map(|lr| lr.mode())
363 }
364
365 pub(super) fn in_slow_start(&self) -> bool {
367 self.params.cwnd < self.params.ssthresh
368 }
369
370 pub(super) fn poll_send(
385 &mut self,
386 snd_una: SeqNum,
387 snd_nxt: SeqNum,
388 snd_wnd: WindowSize,
389 available_bytes: usize,
390 ) -> Option<CongestionControlSendOutcome> {
391 let Self { params, algorithm: _, loss_recovery, sack_scoreboard } = self;
392 let cwnd = params.rounded_cwnd();
393
394 match loss_recovery {
395 None => {
396 let pipe = sack_scoreboard.pipe();
397 let congestion_window = cwnd.cwnd();
398 let available_window = congestion_window.saturating_sub(pipe);
399 let congestion_limit = available_window.min(cwnd.mss().into());
400 Some(CongestionControlSendOutcome {
401 next_seg: snd_nxt,
402 congestion_limit,
403 congestion_window,
404 loss_recovery: LossRecoverySegment::No,
405 })
406 }
407 Some(LossRecovery::FastRecovery(fast_recovery)) => {
408 Some(fast_recovery.poll_send(cwnd, sack_scoreboard.pipe(), snd_nxt))
409 }
410 Some(LossRecovery::SackRecovery(sack_recovery)) => sack_recovery.poll_send(
411 cwnd,
412 snd_una,
413 snd_nxt,
414 snd_wnd,
415 available_bytes,
416 sack_scoreboard,
417 ),
418 }
419 }
420}
421
422#[derive(Debug)]
425#[cfg_attr(test, derive(Copy, Clone, Eq, PartialEq))]
426pub(super) enum LossRecoverySegment {
427 Yes {
429 rearm_retransmit: bool,
446 mode: LossRecoveryMode,
448 },
449 No,
451}
452
453#[derive(Debug)]
455#[cfg_attr(test, derive(Eq, PartialEq))]
456pub(super) struct CongestionControlSendOutcome {
457 pub next_seg: SeqNum,
459 pub congestion_limit: u32,
465 pub congestion_window: u32,
470 pub loss_recovery: LossRecoverySegment,
472}
473
474#[derive(Debug)]
476pub enum LossRecovery {
477 FastRecovery(FastRecovery),
478 SackRecovery(SackRecovery),
479}
480
481impl LossRecovery {
482 fn mode(&self) -> LossRecoveryMode {
483 match self {
484 LossRecovery::FastRecovery(_) => LossRecoveryMode::FastRecovery,
485 LossRecovery::SackRecovery(_) => LossRecoveryMode::SackRecovery,
486 }
487 }
488}
489
490#[derive(Debug)]
493#[cfg_attr(test, derive(Copy, Clone, Eq, PartialEq))]
494pub enum LossRecoveryMode {
495 FastRecovery,
496 SackRecovery,
497}
498
499#[derive(Debug)]
500#[cfg_attr(test, derive(Eq, PartialEq))]
501enum LossRecoveryOnAckOutcome {
502 None,
503 Discard { recovered: bool },
504}
505
506#[derive(Debug)]
509pub struct FastRecovery {
510 fast_retransmit: Option<SeqNum>,
512 dup_acks: NonZeroU8,
526}
527
528impl FastRecovery {
529 fn new() -> Self {
530 Self { dup_acks: NonZeroU8::new(1).unwrap(), fast_retransmit: None }
531 }
532
533 fn poll_send(
534 &mut self,
535 cwnd: CongestionWindow,
536 used_congestion_window: u32,
537 snd_nxt: SeqNum,
538 ) -> CongestionControlSendOutcome {
539 let Self { fast_retransmit, dup_acks } = self;
540 let congestion_window = if dup_acks.get() < DUP_ACK_THRESHOLD {
555 cwnd.cwnd().saturating_add(u32::from(dup_acks.get()) * u32::from(cwnd.mss()))
556 } else {
557 cwnd.cwnd()
558 };
559
560 let (next_seg, loss_recovery, congestion_limit) = match fast_retransmit.take() {
563 Some(f) => (
570 f,
571 LossRecoverySegment::Yes {
572 rearm_retransmit: false,
573 mode: LossRecoveryMode::FastRecovery,
574 },
575 cwnd.mss().into(),
576 ),
577 None => (
580 snd_nxt,
581 LossRecoverySegment::No,
582 congestion_window.saturating_sub(used_congestion_window).min(cwnd.mss().into()),
583 ),
584 };
585 CongestionControlSendOutcome {
586 next_seg,
587 congestion_limit,
588 congestion_window,
589 loss_recovery,
590 }
591 }
592
593 fn on_ack(&mut self, params: &mut CongestionControlParams) -> LossRecoveryOnAckOutcome {
594 let recovered = self.dup_acks.get() >= DUP_ACK_THRESHOLD;
595 if recovered {
596 params.cwnd = params.ssthresh;
601 }
602 LossRecoveryOnAckOutcome::Discard { recovered }
603 }
604
605 fn on_dup_ack<I: Instant>(
609 &mut self,
610 params: &mut CongestionControlParams,
611 loss_based: &mut LossBasedAlgorithm<I>,
612 seg_ack: SeqNum,
613 ) -> bool {
614 self.dup_acks = self.dup_acks.saturating_add(1);
615
616 match self.dup_acks.get().cmp(&DUP_ACK_THRESHOLD) {
617 Ordering::Less => false,
618 Ordering::Equal => {
619 loss_based.on_loss_detected(params);
620 self.fast_retransmit = Some(seg_ack);
627 params.cwnd =
628 params.ssthresh + u32::from(DUP_ACK_THRESHOLD) * u32::from(params.mss);
629 true
630 }
631 Ordering::Greater => {
632 params.cwnd = params.cwnd.saturating_add(u32::from(params.mss));
638 false
639 }
640 }
641 }
642}
643
644#[derive(Debug)]
646#[cfg_attr(test, derive(Eq, PartialEq, Copy, Clone))]
647enum SackRecoveryState {
648 InRecovery(SackInRecoveryState),
650 PostRto { recovery_point: SeqNum },
652 NotInRecovery,
654}
655
656#[derive(Debug)]
658#[cfg_attr(test, derive(Eq, PartialEq, Copy, Clone))]
659struct SackInRecoveryState {
660 recovery_point: SeqNum,
663 high_rxt: SeqNum,
670 rescue_rxt: Option<SeqNum>,
676}
677
678#[derive(Debug)]
682pub(crate) struct SackRecovery {
683 dup_acks: u8,
690 recovery: SackRecoveryState,
694}
695
696impl SackRecovery {
697 fn new() -> Self {
698 Self {
699 dup_acks: 0,
702 recovery: SackRecoveryState::NotInRecovery,
703 }
704 }
705
706 fn high_rxt(&self) -> Option<SeqNum> {
707 match &self.recovery {
708 SackRecoveryState::InRecovery(SackInRecoveryState {
709 recovery_point: _,
710 high_rxt,
711 rescue_rxt: _,
712 }) => Some(*high_rxt),
713 SackRecoveryState::PostRto { recovery_point: _ } | SackRecoveryState::NotInRecovery => {
714 None
715 }
716 }
717 }
718
719 fn on_ack(&mut self, seg_ack: SeqNum) -> LossRecoveryOnAckOutcome {
720 let Self { dup_acks, recovery } = self;
721 match recovery {
722 SackRecoveryState::InRecovery(SackInRecoveryState {
723 recovery_point,
724 high_rxt: _,
725 rescue_rxt: _,
726 })
727 | SackRecoveryState::PostRto { recovery_point } => {
728 if seg_ack.after_or_eq(*recovery_point) {
733 LossRecoveryOnAckOutcome::Discard {
734 recovered: matches!(recovery, SackRecoveryState::InRecovery(_)),
735 }
736 } else {
737 *dup_acks = 0;
741 LossRecoveryOnAckOutcome::None
742 }
743 }
744 SackRecoveryState::NotInRecovery => {
745 LossRecoveryOnAckOutcome::Discard { recovered: false }
748 }
749 }
750 }
751
752 fn on_dup_ack(
754 &mut self,
755 seq_ack: SeqNum,
756 snd_nxt: SeqNum,
757 sack_scoreboard: &SackScoreboard,
758 ) -> SackDupAckOutcome {
759 let Self { dup_acks, recovery } = self;
760 match recovery {
761 SackRecoveryState::InRecovery(_) | SackRecoveryState::PostRto { .. } => {
762 return SackDupAckOutcome(false);
764 }
765 SackRecoveryState::NotInRecovery => (),
766 }
767 *dup_acks += 1;
768 if *dup_acks >= DUP_ACK_THRESHOLD || sack_scoreboard.is_first_hole_lost() {
773 *recovery = SackRecoveryState::InRecovery(SackInRecoveryState {
778 recovery_point: snd_nxt,
779 high_rxt: seq_ack,
780 rescue_rxt: None,
781 });
782 SackDupAckOutcome(true)
783 } else {
784 SackDupAckOutcome(false)
785 }
786 }
787
788 pub(crate) fn on_retransmission_timeout(&mut self, snd_nxt: SeqNum) -> bool {
804 let Self { dup_acks: _, recovery } = self;
805 match recovery {
806 SackRecoveryState::InRecovery(SackInRecoveryState { .. }) => {
807 *recovery = SackRecoveryState::PostRto { recovery_point: snd_nxt };
808 false
809 }
810 SackRecoveryState::PostRto { recovery_point: _ } => {
811 false
816 }
817 SackRecoveryState::NotInRecovery => {
818 true
820 }
821 }
822 }
823
824 fn poll_send(
828 &mut self,
829 cwnd: CongestionWindow,
830 snd_una: SeqNum,
831 snd_nxt: SeqNum,
832 snd_wnd: WindowSize,
833 available_bytes: usize,
834 sack_scoreboard: &SackScoreboard,
835 ) -> Option<CongestionControlSendOutcome> {
836 let Self { dup_acks: _, recovery } = self;
837
838 let pipe = sack_scoreboard.pipe();
839 let congestion_window = cwnd.cwnd();
840 let available_window = congestion_window.saturating_sub(pipe);
841 if available_window < cwnd.mss().into() {
847 return None;
848 }
849 let congestion_limit = available_window.min(cwnd.mss().into());
850
851 let SackInRecoveryState { recovery_point, high_rxt, rescue_rxt } = match recovery {
861 SackRecoveryState::InRecovery(sack_in_recovery_state) => sack_in_recovery_state,
862 SackRecoveryState::PostRto { recovery_point: _ } | SackRecoveryState::NotInRecovery => {
863 return Some(CongestionControlSendOutcome {
864 next_seg: snd_nxt,
865 congestion_limit,
866 congestion_window,
867 loss_recovery: LossRecoverySegment::No,
868 });
869 }
870 };
871
872 let rearm_retransmit = |next_seg: SeqNum| next_seg.before(*recovery_point);
896
897 let first_unsacked_range =
910 sack_scoreboard.first_unsacked_range_from(snd_una.latest(*high_rxt));
911
912 if let Some(first_hole) = &first_unsacked_range {
913 if *first_hole.meta() {
915 let hole_size = first_hole.len();
916 let congestion_limit = congestion_limit.min(hole_size);
917 *high_rxt = first_hole.start() + congestion_limit;
918
919 if rescue_rxt.is_none() {
929 *rescue_rxt = Some(*high_rxt);
930 }
931
932 return Some(CongestionControlSendOutcome {
933 next_seg: first_hole.start(),
934 congestion_limit,
935 congestion_window,
936 loss_recovery: LossRecoverySegment::Yes {
937 rearm_retransmit: rearm_retransmit(first_hole.start()),
938 mode: LossRecoveryMode::SackRecovery,
939 },
940 });
941 }
942 }
943
944 let total_sent = u32::try_from(snd_nxt - snd_una).unwrap();
952 if available_bytes > usize::try_from(total_sent).unwrap() && u32::from(snd_wnd) > total_sent
953 {
954 return Some(CongestionControlSendOutcome {
955 next_seg: snd_nxt,
956 congestion_limit,
959 congestion_window,
960 loss_recovery: LossRecoverySegment::Yes {
965 rearm_retransmit: false,
966 mode: LossRecoveryMode::SackRecovery,
967 },
968 });
969 }
970
971 if let Some(first_hole) = first_unsacked_range {
979 let hole_size = first_hole.len();
980 let congestion_limit = congestion_limit.min(hole_size);
981 *high_rxt = first_hole.start() + congestion_limit;
982
983 return Some(CongestionControlSendOutcome {
984 next_seg: first_hole.start(),
985 congestion_limit,
986 congestion_window,
987 loss_recovery: LossRecoverySegment::Yes {
988 rearm_retransmit: rearm_retransmit(first_hole.start()),
989 mode: LossRecoveryMode::SackRecovery,
990 },
991 });
992 }
993
994 if rescue_rxt.is_none_or(|rescue_rxt| snd_una.after_or_eq(rescue_rxt)) {
1005 if let Some(right_edge) = sack_scoreboard.right_edge() {
1006 let left = right_edge.latest(snd_nxt - congestion_limit);
1007 let congestion_limit = u32::try_from(snd_nxt - left).unwrap_or(0);
1012 if congestion_limit > 0 {
1013 *rescue_rxt = Some(*recovery_point);
1014 return Some(CongestionControlSendOutcome {
1015 next_seg: left,
1016 congestion_limit,
1017 congestion_window,
1018 loss_recovery: LossRecoverySegment::Yes {
1021 rearm_retransmit: true,
1022 mode: LossRecoveryMode::SackRecovery,
1023 },
1024 });
1025 }
1026 }
1027 }
1028
1029 None
1030 }
1031}
1032
1033#[derive(Debug)]
1043#[cfg_attr(test, derive(Eq, PartialEq))]
1044struct SackDupAckOutcome(bool);
1045
1046impl SackDupAckOutcome {
1047 fn apply<I: Instant>(
1052 self,
1053 params: &mut CongestionControlParams,
1054 algorithm: &mut LossBasedAlgorithm<I>,
1055 ) -> bool {
1056 let Self(loss_recovery) = self;
1057 if loss_recovery {
1058 algorithm.on_loss_detected(params);
1059 }
1060 loss_recovery
1061 }
1062}
1063
1064#[cfg(test)]
1065mod test {
1066 use core::ops::Range;
1067
1068 use assert_matches::assert_matches;
1069 use netstack3_base::testutil::FakeInstant;
1070 use netstack3_base::{EffectiveMss, MssSizeLimiters, SackBlock};
1071 use test_case::{test_case, test_matrix};
1072
1073 use super::*;
1074 use crate::internal::testutil;
1075
1076 const MSS_1: EffectiveMss =
1077 EffectiveMss::from_mss(Mss::DEFAULT_IPV4, MssSizeLimiters { timestamp_enabled: false });
1078 const MSS_2: EffectiveMss =
1079 EffectiveMss::from_mss(Mss::DEFAULT_IPV6, MssSizeLimiters { timestamp_enabled: false });
1080
1081 enum StartingAck {
1082 One,
1083 Wraparound,
1084 WraparoundAfter(u32),
1085 }
1086
1087 impl StartingAck {
1088 fn into_seqnum(self, mss: EffectiveMss) -> SeqNum {
1089 let mss = u32::from(mss);
1090 match self {
1091 StartingAck::One => SeqNum::new(1),
1092 StartingAck::Wraparound => SeqNum::new((mss / 2).wrapping_sub(mss)),
1093 StartingAck::WraparoundAfter(n) => SeqNum::new((mss / 2).wrapping_sub(n * mss)),
1094 }
1095 }
1096 }
1097
1098 impl SackRecovery {
1099 #[track_caller]
1100 fn assert_in_recovery(&mut self) -> &mut SackInRecoveryState {
1101 assert_matches!(&mut self.recovery, SackRecoveryState::InRecovery(s) => s)
1102 }
1103 }
1104
1105 impl<I> CongestionControl<I> {
1106 #[track_caller]
1107 fn assert_sack_recovery(&mut self) -> &mut SackRecovery {
1108 assert_matches!(&mut self.loss_recovery, Some(LossRecovery::SackRecovery(s)) => s)
1109 }
1110 }
1111
1112 fn nth_segment_from(base: SeqNum, mss: EffectiveMss, n: u32) -> Range<SeqNum> {
1113 let mss = u32::from(mss);
1114 let start = base + n * mss;
1115 Range { start, end: start + mss }
1116 }
1117
1118 fn nth_range(base: SeqNum, mss: EffectiveMss, range: Range<u32>) -> Range<SeqNum> {
1119 let mss = u32::from(mss);
1120 let Range { start, end } = range;
1121 let start = base + start * mss;
1122 let end = base + end * mss;
1123 Range { start, end }
1124 }
1125
1126 #[test]
1127 fn no_recovery_before_reaching_threshold() {
1128 let mut congestion_control = CongestionControl::cubic_with_mss(MSS_1);
1129 let old_cwnd = congestion_control.params.cwnd;
1130 assert_eq!(congestion_control.params.ssthresh, u32::MAX);
1131 assert_eq!(congestion_control.on_dup_ack(SeqNum::new(0), SeqNum::new(1)), None);
1132 assert!(!congestion_control.on_ack(
1133 SeqNum::new(1),
1134 NonZeroU32::new(1).unwrap(),
1135 FakeInstant::from(Duration::from_secs(0)),
1136 Some(Duration::from_secs(1)),
1137 ));
1138 assert_eq!(old_cwnd + 1, congestion_control.params.cwnd);
1142 }
1143
1144 #[test]
1145 fn preprocess_ack_result() {
1146 let ack = SeqNum::new(1);
1147 let snd_nxt = SeqNum::new(100);
1148 let mut congestion_control = CongestionControl::<FakeInstant>::cubic_with_mss(MSS_1);
1149 assert_eq!(congestion_control.preprocess_ack(ack, snd_nxt, &SackBlocks::EMPTY), None);
1150 assert_eq!(
1151 congestion_control.preprocess_ack(ack, snd_nxt, &testutil::sack_blocks([10..20])),
1152 Some(true)
1153 );
1154 assert_eq!(congestion_control.preprocess_ack(ack, snd_nxt, &SackBlocks::EMPTY), None);
1155 assert_eq!(
1156 congestion_control.preprocess_ack(ack, snd_nxt, &testutil::sack_blocks([10..20])),
1157 Some(false)
1158 );
1159 assert_eq!(
1160 congestion_control.preprocess_ack(
1161 ack,
1162 snd_nxt,
1163 &testutil::sack_blocks([10..20, 20..30])
1164 ),
1165 Some(true)
1166 );
1167 }
1168
1169 #[test_case(DUP_ACK_THRESHOLD-1; "no loss")]
1170 #[test_case(DUP_ACK_THRESHOLD; "exact threshold")]
1171 #[test_case(DUP_ACK_THRESHOLD+1; "over threshold")]
1172 fn sack_recovery_enter_exit_loss_dupacks(dup_acks: u8) {
1173 let mut congestion_control = CongestionControl::cubic_with_mss(MSS_1);
1174 let mss = congestion_control.mss();
1175
1176 let ack = SeqNum::new(1);
1177 let snd_nxt = nth_segment_from(ack, mss, 10).end;
1178
1179 let expect_recovery =
1180 SackInRecoveryState { recovery_point: snd_nxt, high_rxt: ack, rescue_rxt: None };
1181
1182 let mut sack = SackBlock::try_from(nth_segment_from(ack, mss, 1)).unwrap();
1183 for n in 1..=dup_acks {
1184 assert_eq!(
1185 congestion_control.preprocess_ack(ack, snd_nxt, &[sack].into_iter().collect()),
1186 Some(true)
1187 );
1188 assert_eq!(
1189 congestion_control.on_dup_ack(ack, snd_nxt),
1190 (n == DUP_ACK_THRESHOLD).then_some(LossRecoveryMode::SackRecovery)
1191 );
1192 let sack_recovery = congestion_control.assert_sack_recovery();
1193 assert_eq!(sack_recovery.dup_acks, n.min(DUP_ACK_THRESHOLD));
1195
1196 let expect_recovery = if n >= DUP_ACK_THRESHOLD {
1197 SackRecoveryState::InRecovery(expect_recovery.clone())
1198 } else {
1199 SackRecoveryState::NotInRecovery
1200 };
1201 assert_eq!(congestion_control.assert_sack_recovery().recovery, expect_recovery);
1202
1203 let (start, end) = sack.into_parts();
1204 sack = SackBlock::try_new(start, end + u32::from(mss) / 4).unwrap();
1207 }
1208
1209 let end = sack.right();
1210 let bytes_acked = NonZeroU32::new(u32::try_from(end - ack).unwrap()).unwrap();
1211 let ack = end;
1212 assert_eq!(congestion_control.preprocess_ack(ack, snd_nxt, &SackBlocks::EMPTY), None);
1213
1214 let now = FakeInstant::default();
1215 let rtt = Some(Duration::from_millis(1));
1216
1217 assert_eq!(congestion_control.on_ack(ack, bytes_acked, now, rtt), false);
1219 if dup_acks >= DUP_ACK_THRESHOLD {
1220 assert_eq!(
1221 congestion_control.assert_sack_recovery().recovery,
1222 SackRecoveryState::InRecovery(expect_recovery)
1223 );
1224 } else {
1225 assert_matches!(congestion_control.loss_recovery, None);
1226 }
1227
1228 let bytes_acked = NonZeroU32::new(u32::try_from(snd_nxt - ack).unwrap()).unwrap();
1230 let ack = snd_nxt;
1231 assert_eq!(
1232 congestion_control.on_ack(ack, bytes_acked, now, rtt),
1233 dup_acks >= DUP_ACK_THRESHOLD
1234 );
1235 assert_matches!(congestion_control.loss_recovery, None);
1236
1237 let snd_nxt = snd_nxt + 20;
1239 let ack = ack + 10;
1240 assert_eq!(congestion_control.preprocess_ack(ack, snd_nxt, &SackBlocks::EMPTY), None);
1241 assert_eq!(congestion_control.on_ack(ack, bytes_acked, now, rtt), false);
1242 assert_matches!(congestion_control.loss_recovery, None);
1243 }
1244
1245 #[test]
1246 fn sack_recovery_enter_loss_single_dupack() {
1247 let mut congestion_control = CongestionControl::<FakeInstant>::cubic_with_mss(MSS_1);
1248
1249 let snd_nxt = SeqNum::new(100);
1252 let ack = SeqNum::new(0);
1253 assert_eq!(
1254 congestion_control.preprocess_ack(
1255 ack,
1256 snd_nxt,
1257 &testutil::sack_blocks([5..15, 25..35, 45..55])
1258 ),
1259 Some(true)
1260 );
1261 assert_eq!(
1262 congestion_control.on_dup_ack(ack, snd_nxt),
1263 Some(LossRecoveryMode::SackRecovery)
1264 );
1265 assert_eq!(
1266 congestion_control.assert_sack_recovery().recovery,
1267 SackRecoveryState::InRecovery(SackInRecoveryState {
1268 recovery_point: snd_nxt,
1269 high_rxt: ack,
1270 rescue_rxt: None
1271 })
1272 );
1273 }
1274
1275 #[test]
1276 fn sack_recovery_poll_send_not_recovery() {
1277 let mut scoreboard = SackScoreboard::default();
1278 let mut recovery = SackRecovery::new();
1279 let mss = MSS_1;
1280 let cwnd_mss = 10u32;
1281 let cwnd = CongestionWindow::new(cwnd_mss * u32::from(mss), mss);
1282 let snd_una = SeqNum::new(1);
1283
1284 let in_flight = 5u32;
1285 let snd_nxt = nth_segment_from(snd_una, mss, in_flight).start;
1286
1287 let snd_wnd = WindowSize::ZERO;
1291 let available_bytes = 0;
1292
1293 let sack_block = SackBlock::try_from(nth_segment_from(snd_una, mss, 1)).unwrap();
1294 assert!(scoreboard.process_ack(
1295 snd_una,
1296 snd_nxt,
1297 None,
1298 &[sack_block].into_iter().collect(),
1299 mss
1300 ));
1301
1302 let wnd_used = in_flight - 1;
1305 assert_eq!(scoreboard.pipe(), wnd_used * u32::from(mss));
1306 let wnd_available = cwnd_mss - wnd_used;
1307
1308 for i in 0..wnd_available {
1309 let snd_nxt = snd_nxt + i * u32::from(mss);
1310 assert_eq!(
1311 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1312 Some(CongestionControlSendOutcome {
1313 next_seg: snd_nxt,
1314 congestion_limit: mss.into(),
1315 congestion_window: cwnd.cwnd(),
1316 loss_recovery: LossRecoverySegment::No,
1317 })
1318 );
1319 scoreboard.increment_pipe(mss.into());
1320 }
1321
1322 assert_eq!(scoreboard.pipe(), cwnd.cwnd());
1324 assert_eq!(
1326 recovery.poll_send(
1327 cwnd,
1328 snd_una,
1329 snd_nxt + (wnd_used + 1) * u32::from(mss),
1330 snd_wnd,
1331 available_bytes,
1332 &scoreboard
1333 ),
1334 None
1335 );
1336 }
1337
1338 #[test_matrix(
1339 [MSS_1, MSS_2],
1340 [1, 3, 5],
1341 [StartingAck::One, StartingAck::Wraparound]
1342 )]
1343 fn sack_recovery_next_seg_rule_1(mss: EffectiveMss, lost_segments: u32, snd_una: StartingAck) {
1344 let mut scoreboard = SackScoreboard::default();
1345 let mut recovery = SackRecovery::new();
1346
1347 let snd_una = snd_una.into_seqnum(mss);
1348
1349 let sacked_segments = u32::from(DUP_ACK_THRESHOLD);
1350 let sacked_range = lost_segments..(lost_segments + sacked_segments);
1351 let in_flight = sacked_range.end + 5;
1352 let snd_nxt = nth_segment_from(snd_una, mss, in_flight).start;
1353
1354 let cwnd_mss = in_flight - sacked_segments - 1;
1357 let cwnd = CongestionWindow::new(cwnd_mss * u32::from(mss), mss);
1358
1359 let snd_wnd = WindowSize::ZERO;
1362 let available_bytes = 0;
1363
1364 let sack_block = SackBlock::try_from(nth_range(snd_una, mss, sacked_range)).unwrap();
1365 assert!(scoreboard.process_ack(
1366 snd_una,
1367 snd_nxt,
1368 None,
1369 &[sack_block].into_iter().collect(),
1370 mss
1371 ));
1372
1373 assert_eq!(cwnd.cwnd() - scoreboard.pipe(), (lost_segments - 1) * u32::from(mss));
1376 assert_eq!(recovery.on_dup_ack(snd_una, snd_nxt, &scoreboard), SackDupAckOutcome(true));
1378
1379 for i in 0..(lost_segments - 1) {
1380 let next_seg = snd_una + i * u32::from(mss);
1381 assert_eq!(
1382 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1383 Some(CongestionControlSendOutcome {
1384 next_seg,
1385 congestion_limit: mss.into(),
1386 congestion_window: cwnd.cwnd(),
1387 loss_recovery: LossRecoverySegment::Yes {
1388 rearm_retransmit: true,
1389 mode: LossRecoveryMode::SackRecovery,
1390 },
1391 })
1392 );
1393 scoreboard.increment_pipe(mss.into());
1394 assert_eq!(
1395 recovery.recovery,
1396 SackRecoveryState::InRecovery(SackInRecoveryState {
1397 recovery_point: snd_nxt,
1398 high_rxt: nth_segment_from(snd_una, mss, i).end,
1399 rescue_rxt: Some(snd_una + u32::from(mss)),
1402 })
1403 );
1404 }
1405 assert_eq!(
1407 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1408 None
1409 );
1410 }
1411
1412 #[test_matrix(
1413 [MSS_1, MSS_2],
1414 [1, 3, 5],
1415 [StartingAck::One, StartingAck::Wraparound]
1416 )]
1417 fn sack_recovery_next_seg_rule_2(mss: EffectiveMss, expect_send: u32, snd_una: StartingAck) {
1418 let mut scoreboard = SackScoreboard::default();
1419 let mut recovery = SackRecovery::new();
1420
1421 let snd_una = snd_una.into_seqnum(mss);
1422
1423 let lost_segments = 1;
1424 let sacked_segments = u32::from(DUP_ACK_THRESHOLD);
1425 let sacked_range = lost_segments..(lost_segments + sacked_segments);
1426 let in_flight = sacked_range.end + 5;
1427 let mut snd_nxt = nth_segment_from(snd_una, mss, in_flight).start;
1428
1429 let sack_block = SackBlock::try_from(nth_range(snd_una, mss, sacked_range)).unwrap();
1430 assert!(scoreboard.process_ack(
1431 snd_una,
1432 snd_nxt,
1433 None,
1434 &[sack_block].into_iter().collect(),
1435 mss
1436 ));
1437
1438 let cwnd = CongestionWindow::new(scoreboard.pipe() + expect_send * u32::from(mss), mss);
1441 assert_eq!(recovery.on_dup_ack(snd_una, snd_nxt, &scoreboard), SackDupAckOutcome(true));
1443 let recovery_state = recovery.assert_in_recovery();
1445 recovery_state.high_rxt = nth_segment_from(snd_una, mss, lost_segments - 1).end;
1446 recovery_state.rescue_rxt = Some(snd_nxt);
1448 let state_snapshot = recovery_state.clone();
1449
1450 let baseline = u32::try_from(snd_nxt - snd_una).unwrap();
1452 for (snd_wnd, available_bytes) in [(0, 0), (1, 0), (0, 1)] {
1454 let snd_wnd = WindowSize::from_u32(baseline + snd_wnd).unwrap();
1455 let available_bytes = usize::try_from(baseline + available_bytes).unwrap();
1456 assert_eq!(
1457 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1458 None
1459 );
1460 assert_eq!(recovery.recovery, SackRecoveryState::InRecovery(state_snapshot));
1461 }
1462
1463 let baseline = baseline + (expect_send - 1) * u32::from(mss) + 1;
1464 let snd_wnd = WindowSize::from_u32(baseline).unwrap();
1465 let available_bytes = usize::try_from(baseline).unwrap();
1466 for _ in 0..expect_send {
1467 assert_eq!(
1468 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1469 Some(CongestionControlSendOutcome {
1470 next_seg: snd_nxt,
1471 congestion_limit: mss.into(),
1472 congestion_window: cwnd.cwnd(),
1473 loss_recovery: LossRecoverySegment::Yes {
1474 rearm_retransmit: false,
1475 mode: LossRecoveryMode::SackRecovery,
1476 },
1477 })
1478 );
1479 assert_eq!(recovery.recovery, SackRecoveryState::InRecovery(state_snapshot));
1480 scoreboard.increment_pipe(mss.into());
1481 snd_nxt = snd_nxt + u32::from(mss);
1482 }
1483 let snd_wnd = WindowSize::MAX;
1485 let available_bytes = usize::MAX;
1486 assert_eq!(
1487 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1488 None
1489 );
1490 assert_eq!(recovery.recovery, SackRecoveryState::InRecovery(state_snapshot));
1491 }
1492
1493 #[test_matrix(
1494 [MSS_1, MSS_2],
1495 [1, 3, 5],
1496 [StartingAck::One, StartingAck::Wraparound]
1497 )]
1498 fn sack_recovery_next_seg_rule_3(
1499 mss: EffectiveMss,
1500 not_lost_segments: u32,
1501 snd_una: StartingAck,
1502 ) {
1503 let mut scoreboard = SackScoreboard::default();
1504 let mut recovery = SackRecovery::new();
1505
1506 let snd_una = snd_una.into_seqnum(mss);
1507
1508 let first_lost_block = 1;
1509 let first_sacked_segments = u32::from(DUP_ACK_THRESHOLD);
1510 let first_sacked_range = first_lost_block..(first_lost_block + first_sacked_segments);
1511
1512 let sacked_segments = 1;
1515 let sacked_range_start = first_sacked_range.end + not_lost_segments;
1516 let sacked_range = sacked_range_start..(sacked_range_start + sacked_segments);
1517
1518 let in_flight = sacked_range.end + 5;
1519 let snd_nxt = nth_segment_from(snd_una, mss, in_flight).start;
1520
1521 let sack_block1 =
1522 SackBlock::try_from(nth_range(snd_una, mss, first_sacked_range.clone())).unwrap();
1523 let sack_block2 = SackBlock::try_from(nth_range(snd_una, mss, sacked_range)).unwrap();
1524 assert!(scoreboard.process_ack(
1525 snd_una,
1526 snd_nxt,
1527 None,
1528 &[sack_block1, sack_block2].into_iter().collect(),
1529 mss
1530 ));
1531
1532 let expect_send = (not_lost_segments - 1).max(1);
1535 let cwnd_mss =
1536 in_flight - first_sacked_segments - sacked_segments - first_lost_block + expect_send;
1537 let cwnd = CongestionWindow::new(cwnd_mss * u32::from(mss), mss);
1538
1539 let snd_wnd = WindowSize::ZERO;
1541 let available_bytes = 0;
1542
1543 assert_eq!(cwnd.cwnd() - scoreboard.pipe(), expect_send * u32::from(mss));
1546 assert_eq!(recovery.on_dup_ack(snd_una, snd_nxt, &scoreboard), SackDupAckOutcome(true));
1548 for i in 0..first_lost_block {
1551 assert_eq!(
1552 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1553 Some(CongestionControlSendOutcome {
1554 next_seg: nth_segment_from(snd_una, mss, i).start,
1555 congestion_limit: mss.into(),
1556 congestion_window: cwnd.cwnd(),
1557 loss_recovery: LossRecoverySegment::Yes {
1558 rearm_retransmit: true,
1559 mode: LossRecoveryMode::SackRecovery,
1560 },
1561 })
1562 );
1563 }
1564 let expect_recovery = SackInRecoveryState {
1565 recovery_point: snd_nxt,
1566 high_rxt: nth_segment_from(snd_una, mss, first_sacked_range.start).start,
1567 rescue_rxt: Some(nth_segment_from(snd_una, mss, 0).end),
1568 };
1569 assert_eq!(recovery.recovery, SackRecoveryState::InRecovery(expect_recovery));
1570
1571 for i in 0..expect_send {
1572 let next_seg = snd_una + (first_sacked_range.end + i) * u32::from(mss);
1573 assert_eq!(
1574 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1575 Some(CongestionControlSendOutcome {
1576 next_seg,
1577 congestion_limit: mss.into(),
1578 congestion_window: cwnd.cwnd(),
1579 loss_recovery: LossRecoverySegment::Yes {
1580 rearm_retransmit: true,
1581 mode: LossRecoveryMode::SackRecovery,
1582 },
1583 })
1584 );
1585 scoreboard.increment_pipe(mss.into());
1586 assert_eq!(
1587 recovery.recovery,
1588 SackRecoveryState::InRecovery(SackInRecoveryState {
1589 high_rxt: next_seg + u32::from(mss),
1590 ..expect_recovery
1591 })
1592 );
1593 }
1594 assert_eq!(
1596 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1597 None
1598 );
1599 }
1600
1601 #[test_matrix(
1602 [MSS_1, MSS_2],
1603 [0, 1, 3],
1604 [StartingAck::One, StartingAck::Wraparound]
1605 )]
1606 fn sack_recovery_next_seg_rule_4(
1607 mss: EffectiveMss,
1608 right_edge_segments: u32,
1609 snd_una: StartingAck,
1610 ) {
1611 let mut scoreboard = SackScoreboard::default();
1612 let mut recovery = SackRecovery::new();
1613
1614 let snd_una = snd_una.into_seqnum(mss);
1615
1616 let lost_segments = 1;
1617 let sacked_segments = u32::from(DUP_ACK_THRESHOLD);
1618 let sacked_range = lost_segments..(lost_segments + sacked_segments);
1619 let in_flight = sacked_range.end + right_edge_segments + 2;
1620 let snd_nxt = nth_segment_from(snd_una, mss, in_flight).start;
1621
1622 let snd_wnd = WindowSize::ZERO;
1624 let available_bytes = 0;
1625
1626 let sack_block =
1627 SackBlock::try_from(nth_range(snd_una, mss, sacked_range.clone())).unwrap();
1628 assert!(scoreboard.process_ack(
1629 snd_una,
1630 snd_nxt,
1631 None,
1632 &[sack_block].into_iter().collect(),
1633 mss
1634 ));
1635
1636 let cwnd = CongestionWindow::new((in_flight + 500) * u32::from(mss), mss);
1639
1640 assert_eq!(recovery.on_dup_ack(snd_una, snd_nxt, &scoreboard), SackDupAckOutcome(true));
1642 for i in 0..lost_segments {
1645 let next_seg = snd_una + i * u32::from(mss);
1646 assert_eq!(
1647 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1648 Some(CongestionControlSendOutcome {
1649 next_seg,
1650 congestion_limit: mss.into(),
1651 congestion_window: cwnd.cwnd(),
1652 loss_recovery: LossRecoverySegment::Yes {
1653 rearm_retransmit: true,
1654 mode: LossRecoveryMode::SackRecovery,
1655 },
1656 })
1657 );
1658 }
1659 let expect_recovery = SackInRecoveryState {
1660 recovery_point: snd_nxt,
1661 high_rxt: nth_segment_from(snd_una, mss, lost_segments).start,
1662 rescue_rxt: Some(nth_segment_from(snd_una, mss, 0).end),
1665 };
1666 assert_eq!(recovery.recovery, SackRecoveryState::InRecovery(expect_recovery));
1667
1668 assert_eq!(
1671 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1672 None
1673 );
1674 let snd_una = nth_segment_from(snd_una, mss, sacked_range.end).start;
1676 let sack_block = SackBlock::try_from(nth_range(snd_una, mss, 1..2)).unwrap();
1677 assert!(scoreboard.process_ack(
1678 snd_una,
1679 snd_nxt,
1680 Some(expect_recovery.high_rxt),
1681 &[sack_block].into_iter().collect(),
1682 mss
1683 ));
1684 assert_eq!(recovery.on_ack(snd_una), LossRecoveryOnAckOutcome::None);
1685 assert_eq!(recovery.recovery, SackRecoveryState::InRecovery(expect_recovery));
1686 assert_eq!(
1688 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1689 Some(CongestionControlSendOutcome {
1690 next_seg: snd_una,
1691 congestion_limit: mss.into(),
1692 congestion_window: cwnd.cwnd(),
1693 loss_recovery: LossRecoverySegment::Yes {
1694 rearm_retransmit: true,
1695 mode: LossRecoveryMode::SackRecovery,
1696 },
1697 })
1698 );
1699 let expect_recovery =
1700 SackInRecoveryState { high_rxt: snd_una + u32::from(mss), ..expect_recovery };
1701 assert_eq!(recovery.recovery, SackRecoveryState::InRecovery(expect_recovery));
1702
1703 if right_edge_segments > 0 {
1705 assert_eq!(
1706 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1707 Some(CongestionControlSendOutcome {
1708 next_seg: snd_nxt - u32::from(mss),
1709 congestion_limit: mss.into(),
1710 congestion_window: cwnd.cwnd(),
1711 loss_recovery: LossRecoverySegment::Yes {
1712 rearm_retransmit: true,
1713 mode: LossRecoveryMode::SackRecovery,
1714 },
1715 })
1716 );
1717 assert_eq!(
1718 recovery.recovery,
1719 SackRecoveryState::InRecovery(SackInRecoveryState {
1720 rescue_rxt: Some(expect_recovery.recovery_point),
1721 ..expect_recovery
1722 })
1723 );
1724 }
1725
1726 assert_eq!(
1728 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1729 None
1730 );
1731 }
1732
1733 #[test_matrix(
1734 [MSS_1, MSS_2],
1735 [
1736 StartingAck::One,
1737 StartingAck::WraparoundAfter(1),
1738 StartingAck::WraparoundAfter(2),
1739 StartingAck::WraparoundAfter(3),
1740 StartingAck::WraparoundAfter(4)
1741 ]
1742 )]
1743 fn sack_recovery_all_rules(mss: EffectiveMss, snd_una: StartingAck) {
1744 let snd_una = snd_una.into_seqnum(mss);
1745
1746 let mut scoreboard = SackScoreboard::default();
1749 let first_sacked_range = 1..(u32::from(DUP_ACK_THRESHOLD) + 1);
1750 let first_sack_block =
1751 SackBlock::try_from(nth_range(snd_una, mss, first_sacked_range.clone())).unwrap();
1752
1753 let second_sacked_range = (first_sacked_range.end + 1)..(first_sacked_range.end + 2);
1754 let second_sack_block =
1755 SackBlock::try_from(nth_range(snd_una, mss, second_sacked_range.clone())).unwrap();
1756
1757 let snd_nxt = nth_segment_from(snd_una, mss, second_sacked_range.end + 1).start;
1758
1759 let high_rxt = snd_una;
1763 let rescue_rxt = Some(snd_una);
1764
1765 assert!(scoreboard.process_ack(
1766 snd_una,
1767 snd_nxt,
1768 Some(high_rxt),
1769 &[first_sack_block, second_sack_block].into_iter().collect(),
1770 mss
1771 ));
1772
1773 let recovery_state = SackInRecoveryState { recovery_point: snd_nxt, high_rxt, rescue_rxt };
1776 let mut recovery = SackRecovery {
1777 dup_acks: DUP_ACK_THRESHOLD,
1778 recovery: SackRecoveryState::InRecovery(recovery_state),
1779 };
1780
1781 let cwnd = CongestionWindow::new(scoreboard.pipe() + u32::from(mss), mss);
1785
1786 let available = u32::try_from(snd_nxt - snd_una).unwrap() + 1;
1789 let snd_wnd = WindowSize::from_u32(available).unwrap();
1790 let available_bytes = usize::try_from(available).unwrap();
1791
1792 assert_eq!(
1794 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1795 Some(CongestionControlSendOutcome {
1796 next_seg: snd_una,
1797 congestion_limit: u32::from(mss),
1798 congestion_window: cwnd.cwnd(),
1799 loss_recovery: LossRecoverySegment::Yes {
1800 rearm_retransmit: true,
1801 mode: LossRecoveryMode::SackRecovery,
1802 },
1803 })
1804 );
1805 let recovery_state =
1806 SackInRecoveryState { high_rxt: snd_una + u32::from(mss), ..recovery_state };
1807 assert_eq!(recovery.recovery, SackRecoveryState::InRecovery(recovery_state));
1808
1809 assert_eq!(
1811 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1812 Some(CongestionControlSendOutcome {
1813 next_seg: snd_nxt,
1814 congestion_limit: u32::from(mss),
1815 congestion_window: cwnd.cwnd(),
1816 loss_recovery: LossRecoverySegment::Yes {
1817 rearm_retransmit: false,
1818 mode: LossRecoveryMode::SackRecovery,
1819 },
1820 })
1821 );
1822 let snd_nxt = snd_nxt + u32::from(mss);
1824 assert_eq!(recovery.recovery, SackRecoveryState::InRecovery(recovery_state));
1826
1827 assert_eq!(
1829 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1830 Some(CongestionControlSendOutcome {
1831 next_seg: nth_segment_from(snd_una, mss, first_sacked_range.end).start,
1832 congestion_limit: u32::from(mss),
1833 congestion_window: cwnd.cwnd(),
1834 loss_recovery: LossRecoverySegment::Yes {
1835 rearm_retransmit: true,
1836 mode: LossRecoveryMode::SackRecovery,
1837 },
1838 })
1839 );
1840 let recovery_state = SackInRecoveryState {
1841 high_rxt: nth_segment_from(snd_una, mss, second_sacked_range.start).start,
1842 ..recovery_state
1843 };
1844 assert_eq!(recovery.recovery, SackRecoveryState::InRecovery(recovery_state));
1845
1846 assert_eq!(
1848 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1849 Some(CongestionControlSendOutcome {
1850 next_seg: snd_nxt - u32::from(mss),
1851 congestion_limit: u32::from(mss),
1852 congestion_window: cwnd.cwnd(),
1853 loss_recovery: LossRecoverySegment::Yes {
1854 rearm_retransmit: true,
1855 mode: LossRecoveryMode::SackRecovery,
1856 },
1857 })
1858 );
1859 let recovery_state = SackInRecoveryState {
1860 rescue_rxt: Some(recovery_state.recovery_point),
1861 ..recovery_state
1862 };
1863 assert_eq!(recovery.recovery, SackRecoveryState::InRecovery(recovery_state));
1864
1865 assert_eq!(
1867 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1868 None
1869 );
1870 assert!(cwnd.cwnd() - scoreboard.pipe() >= u32::from(mss));
1871 }
1872
1873 #[test]
1874 fn sack_rto() {
1875 let mss = MSS_1;
1876 let mut congestion_control = CongestionControl::<FakeInstant>::cubic_with_mss(mss);
1877
1878 let rto_snd_nxt = SeqNum::new(50);
1879 congestion_control.loss_recovery = Some(LossRecovery::SackRecovery(SackRecovery {
1881 dup_acks: DUP_ACK_THRESHOLD - 1,
1882 recovery: SackRecoveryState::NotInRecovery,
1883 }));
1884 congestion_control.on_retransmission_timeout(rto_snd_nxt);
1885 assert_matches!(congestion_control.loss_recovery, None);
1886
1887 congestion_control.loss_recovery = Some(LossRecovery::SackRecovery(SackRecovery {
1889 dup_acks: DUP_ACK_THRESHOLD,
1890 recovery: SackRecoveryState::InRecovery(SackInRecoveryState {
1891 recovery_point: SeqNum::new(10),
1892 high_rxt: SeqNum::new(0),
1893 rescue_rxt: None,
1894 }),
1895 }));
1896 congestion_control.on_retransmission_timeout(rto_snd_nxt);
1897 assert_eq!(
1898 congestion_control.assert_sack_recovery().recovery,
1899 SackRecoveryState::PostRto { recovery_point: rto_snd_nxt }
1900 );
1901
1902 let snd_una = SeqNum::new(0);
1903 let snd_nxt = SeqNum::new(10);
1904 assert_eq!(
1907 congestion_control.poll_send(snd_una, snd_nxt, WindowSize::ZERO, 0),
1908 Some(CongestionControlSendOutcome {
1909 next_seg: snd_nxt,
1910 congestion_limit: u32::from(mss),
1911 congestion_window: congestion_control.inspect_cwnd().cwnd(),
1912 loss_recovery: LossRecoverySegment::No,
1913 })
1914 );
1915 for _ in 0..DUP_ACK_THRESHOLD {
1917 assert_eq!(congestion_control.on_dup_ack(snd_una, snd_nxt), None);
1918 }
1919
1920 let now = FakeInstant::default();
1921 let rtt = Some(Duration::from_millis(1));
1922
1923 let bytes_acked = NonZeroU32::new(u32::try_from(snd_nxt - snd_una).unwrap()).unwrap();
1926 let snd_una = snd_nxt;
1927 assert!(!congestion_control.on_ack(snd_una, bytes_acked, now, rtt));
1928 assert_eq!(
1929 congestion_control.assert_sack_recovery().recovery,
1930 SackRecoveryState::PostRto { recovery_point: rto_snd_nxt }
1931 );
1932
1933 let bytes_acked = NonZeroU32::new(u32::try_from(rto_snd_nxt - snd_una).unwrap()).unwrap();
1935 let snd_una = rto_snd_nxt;
1936
1937 assert_eq!(congestion_control.on_ack(snd_una, bytes_acked, now, rtt), false);
1940 assert_matches!(congestion_control.loss_recovery, None);
1941 }
1942
1943 #[test]
1944 fn dont_rearm_rto_past_recovery_point() {
1945 let mut scoreboard = SackScoreboard::default();
1946 let mss = MSS_1;
1947 let snd_una = SeqNum::new(1);
1948
1949 let recovery_point = nth_segment_from(snd_una, mss, 100).start;
1950 let snd_nxt = recovery_point + 100 * u32::from(mss);
1951
1952 let mut recovery = SackRecovery {
1953 dup_acks: DUP_ACK_THRESHOLD,
1954 recovery: SackRecoveryState::InRecovery(SackInRecoveryState {
1955 recovery_point,
1956 high_rxt: recovery_point,
1957 rescue_rxt: Some(recovery_point),
1958 }),
1959 };
1960
1961 let block1 = nth_range(snd_una, mss, 101..110);
1962 let block2 = nth_range(snd_una, mss, 111..112);
1963 assert!(
1964 scoreboard.process_ack(
1965 snd_una,
1966 snd_nxt,
1967 recovery.high_rxt(),
1968 &[SackBlock::try_from(block1).unwrap(), SackBlock::try_from(block2).unwrap()]
1969 .into_iter()
1970 .collect(),
1971 mss,
1972 )
1973 );
1974
1975 let cwnd = CongestionWindow::new(u32::MAX, mss);
1976
1977 let snd_wnd = WindowSize::ZERO;
1978 let available_bytes = 0;
1979
1980 assert_eq!(
1981 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1982 Some(CongestionControlSendOutcome {
1983 next_seg: nth_segment_from(snd_una, mss, 100).start,
1984 congestion_limit: mss.into(),
1985 congestion_window: cwnd.cwnd(),
1986 loss_recovery: LossRecoverySegment::Yes {
1987 rearm_retransmit: false,
1988 mode: LossRecoveryMode::SackRecovery,
1989 }
1990 })
1991 );
1992 assert_eq!(
1993 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
1994 Some(CongestionControlSendOutcome {
1995 next_seg: nth_segment_from(snd_una, mss, 110).start,
1996 congestion_limit: mss.into(),
1997 congestion_window: cwnd.cwnd(),
1998 loss_recovery: LossRecoverySegment::Yes {
1999 rearm_retransmit: false,
2000 mode: LossRecoveryMode::SackRecovery,
2001 }
2002 })
2003 );
2004 }
2005
2006 #[test]
2010 fn sack_snd_nxt_rewind() {
2011 let mut scoreboard = SackScoreboard::default();
2012 let mss = MSS_1;
2013 let snd_una = SeqNum::new(1);
2014
2015 let recovery_point = nth_segment_from(snd_una, mss, 100).start;
2016 let snd_nxt = nth_segment_from(recovery_point, mss, 100).start;
2017
2018 let mut recovery = SackRecovery {
2019 dup_acks: DUP_ACK_THRESHOLD,
2020 recovery: SackRecoveryState::InRecovery(SackInRecoveryState {
2021 recovery_point,
2022 high_rxt: recovery_point,
2023 rescue_rxt: None,
2024 }),
2025 };
2026 let sack_block = SackBlock::try_from(nth_range(snd_una, mss, 1..5)).unwrap();
2027 assert!(scoreboard.process_ack(
2028 snd_una,
2029 snd_nxt,
2030 Some(recovery_point),
2031 &[sack_block].into_iter().collect(),
2032 mss,
2033 ));
2034 let snd_nxt = snd_una;
2036
2037 let cwnd = CongestionWindow::new(u32::MAX, mss);
2038 let snd_wnd = WindowSize::ZERO;
2039 let available_bytes = 0;
2040
2041 assert_eq!(
2042 recovery.poll_send(cwnd, snd_una, snd_nxt, snd_wnd, available_bytes, &scoreboard),
2043 None
2044 );
2045 }
2046}