blob: a5398352474f02a4e157fe8c10af9468396f5bc5 [file] [log] [blame]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.21
package quic
import (
"context"
"log/slog"
"math"
"time"
)
// ccReno is the NewReno-based congestion controller defined in RFC 9002.
// https://www.rfc-editor.org/rfc/rfc9002.html#section-7
type ccReno struct {
maxDatagramSize int
// Maximum number of bytes allowed to be in flight.
congestionWindow int
// Sum of size of all packets that contain at least one ack-eliciting
// or PADDING frame (i.e., any non-ACK frame), and have neither been
// acknowledged nor declared lost.
bytesInFlight int
// When the congestion window is below the slow start threshold,
// the controller is in slow start.
slowStartThreshold int
// The time the current recovery period started, or zero when not
// in a recovery period.
recoveryStartTime time.Time
// Accumulated count of bytes acknowledged in congestion avoidance.
congestionPendingAcks int
// When entering a recovery period, we are allowed to send one packet
// before reducing the congestion window. sendOnePacketInRecovery is
// true if we haven't sent that packet yet.
sendOnePacketInRecovery bool
// inRecovery is set when we are in the recovery state.
inRecovery bool
// underutilized is set if the congestion window is underutilized
// due to insufficient application data, flow control limits, or
// anti-amplification limits.
underutilized bool
// ackLastLoss is the sent time of the newest lost packet processed
// in the current batch.
ackLastLoss time.Time
// Data tracking the duration of the most recently handled sequence of
// contiguous lost packets. If this exceeds the persistent congestion duration,
// persistent congestion is declared.
//
// https://www.rfc-editor.org/rfc/rfc9002#section-7.6
persistentCongestion [numberSpaceCount]struct {
start time.Time // send time of first lost packet
end time.Time // send time of last lost packet
next packetNumber // one plus the number of the last lost packet
}
}
func newReno(maxDatagramSize int) *ccReno {
c := &ccReno{
maxDatagramSize: maxDatagramSize,
}
// https://www.rfc-editor.org/rfc/rfc9002.html#section-7.2-1
c.congestionWindow = min(10*maxDatagramSize, max(14720, c.minimumCongestionWindow()))
// https://www.rfc-editor.org/rfc/rfc9002.html#section-7.3.1-1
c.slowStartThreshold = math.MaxInt
for space := range c.persistentCongestion {
c.persistentCongestion[space].next = -1
}
return c
}
// canSend reports whether the congestion controller permits sending
// a maximum-size datagram at this time.
//
// "An endpoint MUST NOT send a packet if it would cause bytes_in_flight [...]
// to be larger than the congestion window [...]"
// https://www.rfc-editor.org/rfc/rfc9002#section-7-7
//
// For simplicity and efficiency, we don't permit sending undersized datagrams.
func (c *ccReno) canSend() bool {
if c.sendOnePacketInRecovery {
return true
}
return c.bytesInFlight+c.maxDatagramSize <= c.congestionWindow
}
// setUnderutilized indicates that the congestion window is underutilized.
//
// The congestion window is underutilized if bytes in flight is smaller than
// the congestion window and sending is not pacing limited; that is, the
// congestion controller permits sending data, but no data is sent.
//
// https://www.rfc-editor.org/rfc/rfc9002#section-7.8
func (c *ccReno) setUnderutilized(log *slog.Logger, v bool) {
if c.underutilized == v {
return
}
oldState := c.state()
c.underutilized = v
if logEnabled(log, QLogLevelPacket) {
logCongestionStateUpdated(log, oldState, c.state())
}
}
// packetSent indicates that a packet has been sent.
func (c *ccReno) packetSent(now time.Time, log *slog.Logger, space numberSpace, sent *sentPacket) {
if !sent.inFlight {
return
}
c.bytesInFlight += sent.size
if c.sendOnePacketInRecovery {
c.sendOnePacketInRecovery = false
}
}
// Acked and lost packets are processed in batches
// resulting from either a received ACK frame or
// the loss detection timer expiring.
//
// A batch consists of zero or more calls to packetAcked and packetLost,
// followed by a single call to packetBatchEnd.
//
// Acks may be reported in any order, but lost packets must
// be reported in strictly increasing order.
// packetAcked indicates that a packet has been newly acknowledged.
func (c *ccReno) packetAcked(now time.Time, sent *sentPacket) {
if !sent.inFlight {
return
}
c.bytesInFlight -= sent.size
if c.underutilized {
// https://www.rfc-editor.org/rfc/rfc9002.html#section-7.8
return
}
if sent.time.Before(c.recoveryStartTime) {
// In recovery, and this packet was sent before we entered recovery.
// (If this packet was sent after we entered recovery, receiving an ack
// for it moves us out of recovery into congestion avoidance.)
// https://www.rfc-editor.org/rfc/rfc9002.html#section-7.3.2
return
}
c.congestionPendingAcks += sent.size
}
// packetLost indicates that a packet has been newly marked as lost.
// Lost packets must be reported in increasing order.
func (c *ccReno) packetLost(now time.Time, space numberSpace, sent *sentPacket, rtt *rttState) {
// Record state to check for persistent congestion.
// https://www.rfc-editor.org/rfc/rfc9002#section-7.6
//
// Note that this relies on always receiving loss events in increasing order:
// All packets prior to the one we're examining now have either been
// acknowledged or declared lost.
isValidPersistentCongestionSample := (sent.ackEliciting &&
!rtt.firstSampleTime.IsZero() &&
!sent.time.Before(rtt.firstSampleTime))
if isValidPersistentCongestionSample {
// This packet either extends an existing range of lost packets,
// or starts a new one.
if sent.num != c.persistentCongestion[space].next {
c.persistentCongestion[space].start = sent.time
}
c.persistentCongestion[space].end = sent.time
c.persistentCongestion[space].next = sent.num + 1
} else {
// This packet cannot establish persistent congestion on its own.
// However, if we have an existing range of lost packets,
// this does not break it.
if sent.num == c.persistentCongestion[space].next {
c.persistentCongestion[space].next = sent.num + 1
}
}
if !sent.inFlight {
return
}
c.bytesInFlight -= sent.size
if sent.time.After(c.ackLastLoss) {
c.ackLastLoss = sent.time
}
}
// packetBatchEnd is called at the end of processing a batch of acked or lost packets.
func (c *ccReno) packetBatchEnd(now time.Time, log *slog.Logger, space numberSpace, rtt *rttState, maxAckDelay time.Duration) {
if logEnabled(log, QLogLevelPacket) {
oldState := c.state()
defer func() { logCongestionStateUpdated(log, oldState, c.state()) }()
}
if !c.ackLastLoss.IsZero() && !c.ackLastLoss.Before(c.recoveryStartTime) {
// Enter the recovery state.
// https://www.rfc-editor.org/rfc/rfc9002.html#section-7.3.2
c.recoveryStartTime = now
c.slowStartThreshold = c.congestionWindow / 2
c.congestionWindow = max(c.slowStartThreshold, c.minimumCongestionWindow())
c.sendOnePacketInRecovery = true
// Clear congestionPendingAcks to avoid increasing the congestion
// window based on acks in a frame that sends us into recovery.
c.congestionPendingAcks = 0
c.inRecovery = true
} else if c.congestionPendingAcks > 0 {
// We are in slow start or congestion avoidance.
c.inRecovery = false
if c.congestionWindow < c.slowStartThreshold {
// When the congestion window is less than the slow start threshold,
// we are in slow start and increase the window by the number of
// bytes acknowledged.
d := min(c.slowStartThreshold-c.congestionWindow, c.congestionPendingAcks)
c.congestionWindow += d
c.congestionPendingAcks -= d
}
// When the congestion window is at or above the slow start threshold,
// we are in congestion avoidance.
//
// RFC 9002 does not specify an algorithm here. The following is
// the recommended algorithm from RFC 5681, in which we increment
// the window by the maximum datagram size every time the number
// of bytes acknowledged reaches cwnd.
for c.congestionPendingAcks > c.congestionWindow {
c.congestionPendingAcks -= c.congestionWindow
c.congestionWindow += c.maxDatagramSize
}
}
if !c.ackLastLoss.IsZero() {
// Check for persistent congestion.
// https://www.rfc-editor.org/rfc/rfc9002.html#section-7.6
//
// "A sender [...] MAY use state for just the packet number space that
// was acknowledged."
// https://www.rfc-editor.org/rfc/rfc9002#section-7.6.2-5
//
// For simplicity, we consider each number space independently.
const persistentCongestionThreshold = 3
d := (rtt.smoothedRTT + max(4*rtt.rttvar, timerGranularity) + maxAckDelay) *
persistentCongestionThreshold
start := c.persistentCongestion[space].start
end := c.persistentCongestion[space].end
if end.Sub(start) >= d {
c.congestionWindow = c.minimumCongestionWindow()
c.recoveryStartTime = time.Time{}
rtt.establishPersistentCongestion()
}
}
c.ackLastLoss = time.Time{}
}
// packetDiscarded indicates that the keys for a packet's space have been discarded.
func (c *ccReno) packetDiscarded(sent *sentPacket) {
// https://www.rfc-editor.org/rfc/rfc9002#section-6.2.2-3
if sent.inFlight {
c.bytesInFlight -= sent.size
}
}
func (c *ccReno) minimumCongestionWindow() int {
// https://www.rfc-editor.org/rfc/rfc9002.html#section-7.2-4
return 2 * c.maxDatagramSize
}
func logCongestionStateUpdated(log *slog.Logger, oldState, newState congestionState) {
if oldState == newState {
return
}
log.LogAttrs(context.Background(), QLogLevelPacket,
"recovery:congestion_state_updated",
slog.String("old", oldState.String()),
slog.String("new", newState.String()),
)
}
type congestionState string
func (s congestionState) String() string { return string(s) }
const (
congestionSlowStart = congestionState("slow_start")
congestionCongestionAvoidance = congestionState("congestion_avoidance")
congestionApplicationLimited = congestionState("application_limited")
congestionRecovery = congestionState("recovery")
)
func (c *ccReno) state() congestionState {
switch {
case c.inRecovery:
return congestionRecovery
case c.underutilized:
return congestionApplicationLimited
case c.congestionWindow < c.slowStartThreshold:
return congestionSlowStart
default:
return congestionCongestionAvoidance
}
}