Skip to content

Commit

Permalink
net: use synthetic network in TestDialParallel
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
neild authored and gopherbot committed Jun 7, 2022
1 parent 19d71ac commit a7551fe
Showing 1 changed file with 71 additions and 103 deletions.
174 changes: 71 additions & 103 deletions src/net/dial_test.go
Expand Up @@ -9,6 +9,8 @@ package net
import (
"bufio"
"context"
"errors"
"fmt"
"internal/testenv"
"io"
"os"
Expand Down Expand Up @@ -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++ {
Expand All @@ -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
Expand All @@ -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()
})
}
}

Expand Down

0 comments on commit a7551fe

Please sign in to comment.