migrate the MTU discoverer tests away from Ginkgo (#4907)

This commit is contained in:
Marten Seemann 2025-01-21 04:17:15 -08:00 committed by GitHub
parent bea70c6489
commit 302308335c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 179 additions and 173 deletions

View file

@ -22,7 +22,7 @@ type mtuDiscoverer interface {
const ( const (
// At some point, we have to stop searching for a higher MTU. // At some point, we have to stop searching for a higher MTU.
// We're happy to send a packet that's 10 bytes smaller than the actual MTU. // We're happy to send a packet that's 10 bytes smaller than the actual MTU.
maxMTUDiff = 20 maxMTUDiff protocol.ByteCount = 20
// send a probe packet every mtuProbeDelay RTTs // send a probe packet every mtuProbeDelay RTTs
mtuProbeDelay = 5 mtuProbeDelay = 5
// Once maxLostMTUProbes MTU probe packets larger than a certain size are lost, // Once maxLostMTUProbes MTU probe packets larger than a certain size are lost,

View file

@ -2,200 +2,206 @@ package quic
import ( import (
"fmt" "fmt"
"math/rand/v2"
"testing"
"time" "time"
"golang.org/x/exp/rand"
"github.com/quic-go/quic-go/internal/protocol" "github.com/quic-go/quic-go/internal/protocol"
"github.com/quic-go/quic-go/internal/utils" "github.com/quic-go/quic-go/internal/utils"
"github.com/quic-go/quic-go/logging" "github.com/quic-go/quic-go/logging"
. "github.com/onsi/ginkgo/v2" "github.com/stretchr/testify/require"
. "github.com/onsi/gomega"
) )
var _ = Describe("MTU Discoverer", func() { func TestMTUDiscovererTiming(t *testing.T) {
const ( const rtt = 100 * time.Millisecond
rtt = 100 * time.Millisecond var rttStats utils.RTTStats
startMTU protocol.ByteCount = 1000 rttStats.UpdateRTT(rtt, 0)
maxMTU protocol.ByteCount = 2000 d := newMTUDiscoverer(&rttStats, 1000, 2000, func(s protocol.ByteCount) {}, nil)
)
var ( now := time.Now()
d *mtuFinder require.False(t, d.ShouldSendProbe(now))
rttStats *utils.RTTStats d.Start(now)
now time.Time require.False(t, d.ShouldSendProbe(now))
discoveredMTU protocol.ByteCount require.False(t, d.ShouldSendProbe(now.Add(rtt*9/2)))
) now = now.Add(5 * rtt)
r := rand.New(rand.NewSource(uint64(GinkgoRandomSeed()))) require.True(t, d.ShouldSendProbe(now))
BeforeEach(func() { // only a single outstanding probe packet is permitted
rttStats = &utils.RTTStats{} ping, _ := d.GetPing(now)
rttStats.SetInitialRTT(rtt) require.False(t, d.ShouldSendProbe(now))
Expect(rttStats.SmoothedRTT()).To(Equal(rtt)) now = now.Add(5 * rtt)
d = newMTUDiscoverer( require.False(t, d.ShouldSendProbe(now))
rttStats, ping.Handler.OnLost(ping.Frame)
startMTU, require.True(t, d.ShouldSendProbe(now))
maxMTU, }
func(s protocol.ByteCount) { discoveredMTU = s },
nil,
)
d.Start(time.Now())
now = time.Now()
})
It("only allows a probe 5 RTTs after the handshake completes", func() { func TestMTUDiscovererAckAndLoss(t *testing.T) {
Expect(d.ShouldSendProbe(now)).To(BeFalse()) var mtu protocol.ByteCount
Expect(d.ShouldSendProbe(now.Add(rtt * 9 / 2))).To(BeFalse()) d := newMTUDiscoverer(&utils.RTTStats{}, 1000, 2000, func(s protocol.ByteCount) { mtu = s }, nil)
Expect(d.ShouldSendProbe(now.Add(rtt * 5))).To(BeTrue()) // we use an RTT of 0 here, so we don't have to advance the timer on every step
}) now := time.Now()
ping, size := d.GetPing(now)
require.Equal(t, protocol.ByteCount(1500), size)
// the MTU is reduced if the frame is lost
ping.Handler.OnLost(ping.Frame)
require.Zero(t, mtu) // no change to the MTU yet
It("doesn't allow a probe if another probe is still in flight", func() { require.True(t, d.ShouldSendProbe(now))
ping, _ := d.GetPing(time.Now()) ping, size = d.GetPing(now)
Expect(d.ShouldSendProbe(now.Add(10 * rtt))).To(BeFalse()) require.Equal(t, protocol.ByteCount(1250), size)
ping.Handler.OnLost(ping.Frame) ping.Handler.OnAcked(ping.Frame)
Expect(d.ShouldSendProbe(now.Add(10 * rtt))).To(BeTrue()) require.Equal(t, protocol.ByteCount(1250), mtu) // the MTU is increased
})
It("tries a lower size when a probe is lost", func() { // Even though the 1500 byte MTU probe packet was lost, we try again with a higher MTU.
ping, size := d.GetPing(time.Now()) // This protects against regular (non-MTU-related) packet loss.
Expect(size).To(Equal(protocol.ByteCount(1500))) require.True(t, d.ShouldSendProbe(now))
ping.Handler.OnLost(ping.Frame) ping, size = d.GetPing(now)
_, size = d.GetPing(time.Now()) require.Greater(t, size, protocol.ByteCount(1500))
Expect(size).To(Equal(protocol.ByteCount(1250))) ping.Handler.OnAcked(ping.Frame)
}) require.Equal(t, size, mtu)
It("tries a higher size and calls the callback when a probe is acknowledged", func() { // We continue probing until the MTU is close to the maximum.
ping, size := d.GetPing(time.Now()) var steps int
Expect(size).To(Equal(protocol.ByteCount(1500))) oldSize := size
for d.ShouldSendProbe(now) {
ping, size = d.GetPing(now)
require.Greater(t, size, oldSize)
oldSize = size
ping.Handler.OnAcked(ping.Frame) ping.Handler.OnAcked(ping.Frame)
Expect(discoveredMTU).To(Equal(protocol.ByteCount(1500))) steps++
_, size = d.GetPing(time.Now()) require.Less(t, steps, 10)
Expect(size).To(Equal(protocol.ByteCount(1750))) }
}) require.Less(t, 2000-maxMTUDiff, size)
}
It("stops discovery after getting close enough to the MTU", func() { func TestMTUDiscovererMTUDiscovery(t *testing.T) {
var sizes []protocol.ByteCount for i := range 5 {
t := now.Add(5 * rtt) t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
for d.ShouldSendProbe(t) { testMTUDiscovererMTUDiscovery(t)
ping, size := d.GetPing(time.Now()) })
fmt.Println("sending", size) }
}
func testMTUDiscovererMTUDiscovery(t *testing.T) {
const rtt = 100 * time.Millisecond
const startMTU protocol.ByteCount = 1000
var rttStats utils.RTTStats
rttStats.UpdateRTT(rtt, 0)
maxMTU := protocol.ByteCount(rand.IntN(int(3000-startMTU))) + startMTU + 1
currentMTU := startMTU
var tracedMTU protocol.ByteCount
var tracerDone bool
d := newMTUDiscoverer(
&rttStats,
startMTU, maxMTU,
func(s protocol.ByteCount) { currentMTU = s },
&logging.ConnectionTracer{
UpdatedMTU: func(mtu logging.ByteCount, done bool) {
tracedMTU = mtu
tracerDone = done
},
},
)
now := time.Now()
d.Start(now)
realMTU := protocol.ByteCount(rand.IntN(int(maxMTU-startMTU))) + startMTU
t.Logf("MTU: %d, max: %d", realMTU, maxMTU)
now = now.Add(mtuProbeDelay * rtt)
var probes []protocol.ByteCount
for d.ShouldSendProbe(now) {
require.Less(t, len(probes), 25, fmt.Sprintf("too many iterations: %v", probes))
ping, size := d.GetPing(now)
probes = append(probes, size)
if size <= realMTU {
ping.Handler.OnAcked(ping.Frame) ping.Handler.OnAcked(ping.Frame)
sizes = append(sizes, size) } else {
t = t.Add(5 * rtt) ping.Handler.OnLost(ping.Frame)
} }
Expect(sizes).To(Equal([]protocol.ByteCount{1500, 1750, 1875, 1937, 1968, 1984})) now = now.Add(mtuProbeDelay * rtt)
Expect(d.ShouldSendProbe(t.Add(10 * rtt))).To(BeFalse()) }
}) diff := realMTU - currentMTU
require.GreaterOrEqual(t, diff, protocol.ByteCount(0))
if maxMTU > currentMTU+maxMTU {
require.Equal(t, currentMTU, tracedMTU)
require.True(t, tracerDone)
}
t.Logf("MTU discovered: %d (diff: %d)", currentMTU, diff)
t.Logf("probes sent (%d): %v", len(probes), probes)
require.LessOrEqual(t, diff, maxMTUDiff)
}
It("doesn't do discovery before being started", func() { func TestMTUDiscovererWithRandomLoss(t *testing.T) {
d := newMTUDiscoverer(rttStats, startMTU, protocol.MaxByteCount, func(s protocol.ByteCount) {}, nil) for i := range 5 {
for i := 0; i < 5; i++ { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
Expect(d.ShouldSendProbe(time.Now())).To(BeFalse()) testMTUDiscovererWithRandomLoss(t)
} })
}) }
}
It("finds the MTU", MustPassRepeatedly(300), func() {
maxMTU := protocol.ByteCount(r.Intn(int(3000-startMTU))) + startMTU + 1
currentMTU := startMTU
var tracedMTU protocol.ByteCount
var tracerDone bool
d := newMTUDiscoverer(
rttStats,
startMTU,
maxMTU,
func(s protocol.ByteCount) { currentMTU = s },
&logging.ConnectionTracer{
UpdatedMTU: func(mtu logging.ByteCount, done bool) {
tracedMTU = mtu
tracerDone = done
},
},
)
d.Start(time.Now())
now := time.Now()
realMTU := protocol.ByteCount(r.Intn(int(maxMTU-startMTU))) + startMTU
fmt.Fprintf(GinkgoWriter, "MTU: %d, max: %d\n", realMTU, maxMTU)
t := now.Add(mtuProbeDelay * rtt)
var probes []protocol.ByteCount
for d.ShouldSendProbe(t) {
if len(probes) > 24 {
Fail(fmt.Sprintf("too many iterations: %v", probes))
}
ping, size := d.GetPing(time.Now())
probes = append(probes, size)
if size <= realMTU {
ping.Handler.OnAcked(ping.Frame)
} else {
ping.Handler.OnLost(ping.Frame)
}
t = t.Add(mtuProbeDelay * rtt)
}
diff := realMTU - currentMTU
Expect(diff).To(BeNumerically(">=", 0))
if maxMTU > currentMTU+maxMTU {
Expect(tracedMTU).To(Equal(currentMTU))
Expect(tracerDone).To(BeTrue())
}
fmt.Fprintf(GinkgoWriter, "MTU discovered: %d (diff: %d)\n", currentMTU, diff)
fmt.Fprintf(GinkgoWriter, "probes sent (%d): %v\n", len(probes), probes)
Expect(diff).To(BeNumerically("<=", maxMTUDiff))
})
func testMTUDiscovererWithRandomLoss(t *testing.T) {
const rtt = 100 * time.Millisecond
const startMTU protocol.ByteCount = 1000
const maxRandomLoss = maxLostMTUProbes - 1 const maxRandomLoss = maxLostMTUProbes - 1
It(fmt.Sprintf("finds the MTU, with up to %d packets lost", maxRandomLoss), MustPassRepeatedly(500), func() {
maxMTU := protocol.ByteCount(r.Intn(int(3000-startMTU))) + startMTU + 1 rttStats := &utils.RTTStats{}
currentMTU := startMTU rttStats.SetInitialRTT(rtt)
var tracedMTU protocol.ByteCount require.Equal(t, rtt, rttStats.SmoothedRTT())
var tracerDone bool
d := newMTUDiscoverer( maxMTU := protocol.ByteCount(rand.IntN(int(3000-startMTU))) + startMTU + 1
rttStats, currentMTU := startMTU
startMTU, var tracedMTU protocol.ByteCount
maxMTU, var tracerDone bool
func(s protocol.ByteCount) { currentMTU = s },
&logging.ConnectionTracer{ d := newMTUDiscoverer(
UpdatedMTU: func(mtu logging.ByteCount, done bool) { rttStats,
tracedMTU = mtu startMTU,
tracerDone = done maxMTU,
}, func(s protocol.ByteCount) { currentMTU = s },
&logging.ConnectionTracer{
UpdatedMTU: func(mtu logging.ByteCount, done bool) {
tracedMTU = mtu
tracerDone = done
}, },
) },
d.Start(time.Now()) )
now := time.Now() d.Start(time.Now())
realMTU := protocol.ByteCount(r.Intn(int(maxMTU-startMTU))) + startMTU now := time.Now()
fmt.Fprintf(GinkgoWriter, "MTU: %d, max: %d\n", realMTU, maxMTU) realMTU := protocol.ByteCount(rand.IntN(int(maxMTU-startMTU))) + startMTU
t := now.Add(mtuProbeDelay * rtt) t.Logf("MTU: %d, max: %d", realMTU, maxMTU)
var probes, randomLosses []protocol.ByteCount now = now.Add(mtuProbeDelay * rtt)
for d.ShouldSendProbe(t) { var probes, randomLosses []protocol.ByteCount
if len(probes) > 32 {
Fail(fmt.Sprintf("too many iterations: %v", probes)) for d.ShouldSendProbe(now) {
require.Less(t, len(probes), 32, fmt.Sprintf("too many iterations: %v", probes))
ping, size := d.GetPing(now)
probes = append(probes, size)
packetFits := size <= realMTU
var acked bool
if packetFits {
randomLoss := rand.IntN(maxLostMTUProbes) == 0 && len(randomLosses) < maxRandomLoss
if randomLoss {
randomLosses = append(randomLosses, size)
} else {
ping.Handler.OnAcked(ping.Frame)
acked = true
} }
ping, size := d.GetPing(time.Now())
probes = append(probes, size)
packetFits := size <= realMTU
var acked bool
if packetFits {
randomLoss := r.Intn(maxLostMTUProbes) == 0 && len(randomLosses) < maxRandomLoss
if randomLoss {
randomLosses = append(randomLosses, size)
} else {
ping.Handler.OnAcked(ping.Frame)
acked = true
}
}
if !acked {
ping.Handler.OnLost(ping.Frame)
}
t = t.Add(mtuProbeDelay * rtt)
} }
diff := realMTU - currentMTU if !acked {
Expect(diff).To(BeNumerically(">=", 0)) ping.Handler.OnLost(ping.Frame)
if maxMTU > currentMTU+maxMTU {
Expect(tracedMTU).To(Equal(currentMTU))
Expect(tracerDone).To(BeTrue())
} }
fmt.Fprintf(GinkgoWriter, "MTU discovered with random losses %v: %d (diff: %d)\n", randomLosses, currentMTU, diff) now = now.Add(mtuProbeDelay * rtt)
fmt.Fprintf(GinkgoWriter, "probes sent (%d): %v\n", len(probes), probes) }
Expect(diff).To(BeNumerically("<=", maxMTUDiff))
}) diff := realMTU - currentMTU
}) require.GreaterOrEqual(t, diff, protocol.ByteCount(0))
if maxMTU > currentMTU+maxMTU {
require.Equal(t, currentMTU, tracedMTU)
require.True(t, tracerDone)
}
t.Logf("MTU discovered with random losses %v: %d (diff: %d)", randomLosses, currentMTU, diff)
t.Logf("probes sent (%d): %v", len(probes), probes)
require.LessOrEqual(t, diff, maxMTUDiff)
}