Skip to content

tcp: implement the RFC 4015 response to spurious loss recovery - #14458

Open
davidbell217 wants to merge 1 commit into
google:masterfrom
davidbell217:rfc4015-spurious-recovery-response
Open

tcp: implement the RFC 4015 response to spurious loss recovery#14458
davidbell217 wants to merge 1 commit into
google:masterfrom
davidbell217:rfc4015-spurious-recovery-response

Conversation

@davidbell217

Copy link
Copy Markdown

Fixes #14102 (cc @nybidari).

Implements the RFC 4015 congestion control response to spurious recovery, per the notes on the issue:

  • Capture: pipe_prev is captured at all five recovery entry points (detectLoss, RACK reorder timer, TLP loss, RTO expiry) before HandleLossDetected()/HandleRTOExpired() cut Ssthresh — at enterRecovery() it's already too late. SndCwnd stands in for FlightSize (both packet-counted; Outstanding is already decimated by the ACKs that trigger detection). A never-reduced Ssthresh (InitialSsthresh) is not restored: it holds no pipe estimate, and restoring it re-created the collapse on our fixture.
  • Response: on exit from a recovery flagged spurious, Ssthresh = pipe_prev (only if that raises it) and SndCwnd = FlightSize + IW. cwnd is not restored directly — a direct restore burst-collapsed 24/24 runs, as warned in the issue.
  • Both exit paths, one site: every recovery (fast/SACK via leaveRecovery, pure RTO directly) ends at the Open transition in handleRcvdSegment, so the response applies exactly there — after the ACK-removal loop, where Outstanding is the true FlightSize and the allowed send is bounded by IW. The capture is consumed on use, so it applies at most once per episode.
  • State: one new plain savable int on sender; nothing added to TCPSenderState.
  • Tests: TestDetectSpuriousRecoveryWithRTO and TestSACKDetectSpuriousRecoveryWithDupACK now drive the connection out of recovery and assert Ssthresh/SndCwnd are restored. Both fail without the fix (Ssthresh = 2, want = 10, one per exit path).

One enabling detection fix: detectSpuriousRecovery now ignores ACKs with no Timestamps option (!SendTSOk || TSEcr == 0). Previously TSEcr = 0 compared as < RetransmitTS and marked a genuine recovery spurious — harmless while only a metric consumed the flag, but TestSACKRecovery (whose crafted ACKs carry no TS option) fails once a response does.

Benchmark, on the fixture from #14102 (195 ms RTT, ±15 ms jitter, 3% of packets delayed +30 ms, no loss; 24 runs/arm; #14092's fix applied in both arms; pinned = run exceeds 15 s):

pinned median
detection only 4/24 10.8–11.0 s
+ RFC 4015 response 0/24 ~3 s

Episodes still fire either way; the response stops them compounding. A clean fixture (3% reorder, no jitter) shows no regression.

Scope: RFC 4015 specifies the response for spurious timeouts; applying it to fast/SACK recovery too matches netstack's detection, which deliberately doesn't differentiate. Step (8) (SND.NXT <- SND.MAX), steps (10)–(11) (RTT/RTO restoration), and the ECE guard (netstack has no ECN congestion response) are out of scope.

RFC 3522 Eifel detection sets sender.spuriousRecovery and the
SpuriousRecovery metrics, but nothing consumed the signal: leaveRecovery
unconditionally deflated SndCwnd to Ssthresh, so a recovery the sender
itself proved spurious still cost a permanent multiplicative decrease,
and under sustained path jitter the sender ratchets its throughput down
episode after episode.

Capture pipe_prev (RFC 4015 Section 3 step (0)) at every loss-recovery
entry point before the congestion controller reduces Ssthresh, and on
exit from a recovery detected spurious restore Ssthresh to it and
slow-start back from FlightSize + IW (step (9)) instead of keeping the
deflation. The response is applied at the single point every recovery
exits through — the Open transition in handleRcvdSegment — after the
ACK-removal loop, when Outstanding is the true FlightSize, so the send
it allows is bounded by IW. The capture is consumed on use, so the
response applies at most once per episode.

detectSpuriousRecovery now ignores ACKs carrying no Timestamps option:
a TSEcr of zero previously compared as smaller than RetransmitTS and
marked a genuine recovery spurious, which was harmless while the flag
only fed a metric but wrong once a response consumes it.

Extend the two spurious-recovery e2e tests to drive the connection out
of recovery on each exit path and assert Ssthresh/SndCwnd are restored;
both extensions fail without the fix.

Fixes google#14102

Assisted-by: Claude Code
@davidbell217
davidbell217 force-pushed the rfc4015-spurious-recovery-response branch from be53c9a to 190e04d Compare August 27, 2026 20:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant