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
1 changed files with 71 additions and 103 deletions

View File

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