net: use synthetic network in TestDialParallel

TestDialParallel is testing the Happy Eyeballs algorithm implementation,
which dials IPv4 and IPv6 addresses in parallel with the preferred
address family getting a head start. This test doesn't care about
the actual network operations, just the handling of the parallel
connections.

Use testHookDialTCP to replace socket creation with a function that
returns successfully, with an error, or after context cancellation
as required.

Limit tests of elapsed times to a check that the fallback deadline
has been exceeded in cases where this is expected.

This should fix persistent test flakiness.

Fixes #52173.

Change-Id: Ic93f270fccb63b24a91105a4d541479fc33a2de4
Reviewed-on: https://go-review.googlesource.com/c/go/+/410754
Auto-Submit: Damien Neil <dneil@google.com>
Reviewed-by: Ian Lance Taylor <iant@google.com>
Run-TryBot: Damien Neil <dneil@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
This commit is contained in:
Damien Neil 2022-06-06 15:52:19 -07:00 committed by Gopher Robot
parent 19d71acd97
commit a7551fe245

View file

@ -9,6 +9,8 @@ package net
import (
"bufio"
"context"
"errors"
"fmt"
"internal/testenv"
"io"
"os"
@ -175,31 +177,9 @@ func dialClosedPort(t *testing.T) (dialLatency time.Duration) {
}
func TestDialParallel(t *testing.T) {
testenv.MustHaveExternalNetwork(t)
if !supportsIPv4() || !supportsIPv6() {
t.Skip("both IPv4 and IPv6 are required")
}
closedPortDelay := dialClosedPort(t)
const instant time.Duration = 0
const fallbackDelay = 200 * time.Millisecond
// Some cases will run quickly when "connection refused" is fast,
// or trigger the fallbackDelay on Windows. This value holds the
// lesser of the two delays.
var closedPortOrFallbackDelay time.Duration
if closedPortDelay < fallbackDelay {
closedPortOrFallbackDelay = closedPortDelay
} else {
closedPortOrFallbackDelay = fallbackDelay
}
origTestHookDialTCP := testHookDialTCP
defer func() { testHookDialTCP = origTestHookDialTCP }()
testHookDialTCP = slowDialTCP
nCopies := func(s string, n int) []string {
out := make([]string, n)
for i := 0; i < n; i++ {
@ -223,31 +203,21 @@ func TestDialParallel(t *testing.T) {
// Primary is slow; fallback should kick in.
{[]string{slowDst4}, []string{"::1"}, "", true, fallbackDelay},
// Skip a "connection refused" in the primary thread.
{[]string{"127.0.0.1", "::1"}, []string{}, "tcp4", true, closedPortDelay},
{[]string{"::1", "127.0.0.1"}, []string{}, "tcp6", true, closedPortDelay},
{[]string{"127.0.0.1", "::1"}, []string{}, "tcp4", true, instant},
{[]string{"::1", "127.0.0.1"}, []string{}, "tcp6", true, instant},
// Skip a "connection refused" in the fallback thread.
{[]string{slowDst4, slowDst6}, []string{"::1", "127.0.0.1"}, "tcp6", true, fallbackDelay + closedPortDelay},
{[]string{slowDst4, slowDst6}, []string{"::1", "127.0.0.1"}, "tcp6", true, fallbackDelay},
// Primary refused, fallback without delay.
{[]string{"127.0.0.1"}, []string{"::1"}, "tcp4", true, closedPortOrFallbackDelay},
{[]string{"::1"}, []string{"127.0.0.1"}, "tcp6", true, closedPortOrFallbackDelay},
{[]string{"127.0.0.1"}, []string{"::1"}, "tcp4", true, instant},
{[]string{"::1"}, []string{"127.0.0.1"}, "tcp6", true, instant},
// Everything is refused.
{[]string{"127.0.0.1"}, []string{}, "tcp4", false, closedPortDelay},
{[]string{"127.0.0.1"}, []string{}, "tcp4", false, instant},
// Nothing to do; fail instantly.
{[]string{}, []string{}, "", false, instant},
// Connecting to tons of addresses should not trip the deadline.
{nCopies("::1", 1000), []string{}, "", true, instant},
}
handler := func(dss *dualStackServer, ln Listener) {
for {
c, err := ln.Accept()
if err != nil {
return
}
c.Close()
}
}
// Convert a list of IP strings into TCPAddrs.
makeAddrs := func(ips []string, port string) addrList {
var out addrList
@ -262,76 +232,74 @@ func TestDialParallel(t *testing.T) {
}
for i, tt := range testCases {
dss, err := newDualStackServer()
if err != nil {
t.Fatal(err)
}
defer dss.teardown()
if err := dss.buildup(handler); err != nil {
t.Fatal(err)
}
if tt.teardownNetwork != "" {
// Destroy one of the listening sockets, creating an unreachable port.
dss.teardownNetwork(tt.teardownNetwork)
}
i, tt := i, tt
t.Run(fmt.Sprint(i), func(t *testing.T) {
origTestHookDialTCP := testHookDialTCP
defer func() { testHookDialTCP = origTestHookDialTCP }()
testHookDialTCP = func(ctx context.Context, network string, laddr, raddr *TCPAddr) (*TCPConn, error) {
n := "tcp6"
if raddr.IP.To4() != nil {
n = "tcp4"
}
if n == tt.teardownNetwork {
return nil, errors.New("unreachable")
}
if r := raddr.IP.String(); r == slowDst4 || r == slowDst6 {
<-ctx.Done()
return nil, ctx.Err()
}
return &TCPConn{}, nil
}
primaries := makeAddrs(tt.primaries, dss.port)
fallbacks := makeAddrs(tt.fallbacks, dss.port)
d := Dialer{
FallbackDelay: fallbackDelay,
}
startTime := time.Now()
sd := &sysDialer{
Dialer: d,
network: "tcp",
address: "?",
}
c, err := sd.dialParallel(context.Background(), primaries, fallbacks)
elapsed := time.Since(startTime)
primaries := makeAddrs(tt.primaries, "80")
fallbacks := makeAddrs(tt.fallbacks, "80")
d := Dialer{
FallbackDelay: fallbackDelay,
}
const forever = 60 * time.Minute
if tt.expectElapsed == instant {
d.FallbackDelay = forever
}
startTime := time.Now()
sd := &sysDialer{
Dialer: d,
network: "tcp",
address: "?",
}
c, err := sd.dialParallel(context.Background(), primaries, fallbacks)
elapsed := time.Since(startTime)
if c != nil {
c.Close()
}
if c != nil {
c.Close()
}
if tt.expectOk && err != nil {
t.Errorf("#%d: got %v; want nil", i, err)
} else if !tt.expectOk && err == nil {
t.Errorf("#%d: got nil; want non-nil", i)
}
if tt.expectOk && err != nil {
t.Errorf("#%d: got %v; want nil", i, err)
} else if !tt.expectOk && err == nil {
t.Errorf("#%d: got nil; want non-nil", i)
}
// We used to always use 95 milliseconds as the slop,
// but that was flaky on Windows. See issue 35616.
slop := 95 * time.Millisecond
if half := tt.expectElapsed / 2; half > slop {
slop = half
}
expectElapsedMin := tt.expectElapsed - slop
expectElapsedMax := tt.expectElapsed + slop
if elapsed < expectElapsedMin {
t.Errorf("#%d: got %v; want >= %v", i, elapsed, expectElapsedMin)
} else if elapsed > expectElapsedMax {
t.Errorf("#%d: got %v; want <= %v", i, elapsed, expectElapsedMax)
}
if elapsed < tt.expectElapsed || elapsed >= forever {
t.Errorf("#%d: got %v; want >= %v, < forever", i, elapsed, tt.expectElapsed)
}
// Repeat each case, ensuring that it can be canceled quickly.
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go func() {
time.Sleep(5 * time.Millisecond)
cancel()
wg.Done()
}()
startTime = time.Now()
c, err = sd.dialParallel(ctx, primaries, fallbacks)
if c != nil {
c.Close()
}
elapsed = time.Now().Sub(startTime)
if elapsed > 100*time.Millisecond {
t.Errorf("#%d (cancel): got %v; want <= 100ms", i, elapsed)
}
wg.Wait()
// Repeat each case, ensuring that it can be canceled.
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go func() {
time.Sleep(5 * time.Millisecond)
cancel()
wg.Done()
}()
// Ignore errors, since all we care about is that the
// call can be canceled.
c, _ = sd.dialParallel(ctx, primaries, fallbacks)
if c != nil {
c.Close()
}
wg.Wait()
})
}
}