Skip to content

Commit 5b0dc2d

Browse files
author
Bryan C. Mills
committed
netutil: make LimitListener tests more robust
In CL 372495 I cleaned up TestLimitListener so that it would not fail spuriously. However, upon further thought I realized that the original test was actually checking two different properties (steady-state saturation, and actual overload), and the cleaned-up test was only checking one of those (overload). This change adds a separate test for steady-state saturation, and makes the overload test more robust to spurious connections (which could occur, for example, if another test running on the machine accidentally dials this test's open port). The test cleanup also revealed a bad interaction with an existing bug in the js/wasm net.TCPListener implementation (filed as golang/go#50216), for which I have added a workaround in (*limitListener).Accept. For golang/go#22926 Change-Id: I727050a8254f527c7455de296ed3525b6dc90141 Reviewed-on: https://go-review.googlesource.com/c/net/+/372714 Trust: Bryan Mills <[email protected]> Run-TryBot: Bryan Mills <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent fe4d628 commit 5b0dc2d

File tree

2 files changed

+159
-42
lines changed

2 files changed

+159
-42
lines changed

netutil/listen.go

+19-6
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,27 @@ func (l *limitListener) acquire() bool {
4242
func (l *limitListener) release() { <-l.sem }
4343

4444
func (l *limitListener) Accept() (net.Conn, error) {
45-
acquired := l.acquire()
46-
// If the semaphore isn't acquired because the listener was closed, expect
47-
// that this call to accept won't block, but immediately return an error.
45+
if !l.acquire() {
46+
// If the semaphore isn't acquired because the listener was closed, expect
47+
// that this call to accept won't block, but immediately return an error.
48+
// If it instead returns a spurious connection (due to a bug in the
49+
// Listener, such as https://golang.org/issue/50216), we immediately close
50+
// it and try again. Some buggy Listener implementations (like the one in
51+
// the aforementioned issue) seem to assume that Accept will be called to
52+
// completion, and may otherwise fail to clean up the client end of pending
53+
// connections.
54+
for {
55+
c, err := l.Listener.Accept()
56+
if err != nil {
57+
return nil, err
58+
}
59+
c.Close()
60+
}
61+
}
62+
4863
c, err := l.Listener.Accept()
4964
if err != nil {
50-
if acquired {
51-
l.release()
52-
}
65+
l.release()
5366
return nil, err
5467
}
5568
return &limitListenerConn{Conn: c, release: l.release}, nil

netutil/listen_test.go

+140-36
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
"time"
1616
)
1717

18-
func TestLimitListener(t *testing.T) {
18+
func TestLimitListenerOverload(t *testing.T) {
1919
const (
2020
max = 5
2121
attempts = max * 2
@@ -30,6 +30,7 @@ func TestLimitListener(t *testing.T) {
3030

3131
var wg sync.WaitGroup
3232
wg.Add(1)
33+
saturated := make(chan struct{})
3334
go func() {
3435
defer wg.Done()
3536

@@ -40,69 +41,174 @@ func TestLimitListener(t *testing.T) {
4041
break
4142
}
4243
accepted++
44+
if accepted == max {
45+
close(saturated)
46+
}
4347
io.WriteString(c, msg)
4448

45-
defer c.Close() // Leave c open until the listener is closed.
49+
// Leave c open until the listener is closed.
50+
defer c.Close()
4651
}
47-
if accepted > max {
48-
t.Errorf("accepted %d simultaneous connections; want at most %d", accepted, max)
52+
t.Logf("with limit %d, accepted %d simultaneous connections", max, accepted)
53+
// The listener accounts open connections based on Listener-side Close
54+
// calls, so even if the client hangs up early (for example, because it
55+
// was a random dial from another process instead of from this test), we
56+
// should not end up accepting more connections than expected.
57+
if accepted != max {
58+
t.Errorf("want exactly %d", max)
4959
}
5060
}()
5161

52-
// connc keeps the client end of the dialed connections alive until the
53-
// test completes.
54-
connc := make(chan []net.Conn, 1)
55-
connc <- nil
56-
5762
dialCtx, cancelDial := context.WithCancel(context.Background())
5863
defer cancelDial()
5964
dialer := &net.Dialer{}
6065

61-
var served int32
66+
var dialed, served int32
67+
var pendingDials sync.WaitGroup
6268
for n := attempts; n > 0; n-- {
6369
wg.Add(1)
70+
pendingDials.Add(1)
6471
go func() {
6572
defer wg.Done()
6673

6774
c, err := dialer.DialContext(dialCtx, l.Addr().Network(), l.Addr().String())
75+
pendingDials.Done()
6876
if err != nil {
6977
t.Log(err)
7078
return
7179
}
80+
atomic.AddInt32(&dialed, 1)
7281
defer c.Close()
7382

74-
// Keep this end of the connection alive until after the Listener
75-
// finishes.
76-
conns := append(<-connc, c)
77-
if len(conns) == max {
78-
go func() {
79-
// Give the server a bit of time to make sure it doesn't exceed its
80-
// limit after serving this connection, then cancel the remaining
81-
// Dials (if any).
82-
time.Sleep(10 * time.Millisecond)
83-
cancelDial()
84-
l.Close()
85-
}()
86-
}
87-
connc <- conns
88-
89-
b := make([]byte, len(msg))
90-
if n, err := c.Read(b); n < len(b) {
83+
// The kernel may queue more than max connections (allowing their dials to
84+
// succeed), but only max of them should actually be accepted by the
85+
// server. We can distinguish the two based on whether the listener writes
86+
// anything to the connection — a connection that was queued but not
87+
// accepted will be closed without transferring any data.
88+
if b, err := io.ReadAll(c); len(b) < len(msg) {
9189
t.Log(err)
9290
return
9391
}
9492
atomic.AddInt32(&served, 1)
9593
}()
9694
}
95+
96+
// Give the server a bit of time after it saturates to make sure it doesn't
97+
// exceed its limit after serving this connection, then cancel the remaining
98+
// dials (if any).
99+
<-saturated
100+
time.Sleep(10 * time.Millisecond)
101+
cancelDial()
102+
// Wait for the dials to complete to ensure that the port isn't reused before
103+
// the dials are actually attempted.
104+
pendingDials.Wait()
105+
l.Close()
97106
wg.Wait()
98107

99-
conns := <-connc
100-
for _, c := range conns {
101-
c.Close()
108+
t.Logf("served %d simultaneous connections (of %d dialed, %d attempted)", served, dialed, attempts)
109+
110+
// If some other process (such as a port scan or another test) happens to dial
111+
// the listener at the same time, the listener could end up burning its quota
112+
// on that, resulting in fewer than max test connections being served.
113+
// But the number served certainly cannot be greater.
114+
if served > max {
115+
t.Errorf("expected at most %d served", max)
102116
}
103-
t.Logf("with limit %d, served %d connections (of %d dialed, %d attempted)", max, served, len(conns), attempts)
104-
if served != max {
105-
t.Errorf("expected exactly %d served", max)
117+
}
118+
119+
func TestLimitListenerSaturation(t *testing.T) {
120+
const (
121+
max = 5
122+
attemptsPerWave = max * 2
123+
waves = 10
124+
msg = "bye\n"
125+
)
126+
127+
l, err := net.Listen("tcp", "127.0.0.1:0")
128+
if err != nil {
129+
t.Fatal(err)
130+
}
131+
l = LimitListener(l, max)
132+
133+
acceptDone := make(chan struct{})
134+
defer func() {
135+
l.Close()
136+
<-acceptDone
137+
}()
138+
go func() {
139+
defer close(acceptDone)
140+
141+
var open, peakOpen int32
142+
var (
143+
saturated = make(chan struct{})
144+
saturatedOnce sync.Once
145+
)
146+
var wg sync.WaitGroup
147+
for {
148+
c, err := l.Accept()
149+
if err != nil {
150+
break
151+
}
152+
if n := atomic.AddInt32(&open, 1); n > peakOpen {
153+
peakOpen = n
154+
if n == max {
155+
saturatedOnce.Do(func() {
156+
// Wait a bit to make sure the listener doesn't exceed its limit
157+
// after accepting this connection, then allow the in-flight
158+
// connections to write out and close.
159+
time.AfterFunc(10*time.Millisecond, func() { close(saturated) })
160+
})
161+
}
162+
}
163+
wg.Add(1)
164+
go func() {
165+
<-saturated
166+
io.WriteString(c, msg)
167+
atomic.AddInt32(&open, -1)
168+
c.Close()
169+
wg.Done()
170+
}()
171+
}
172+
wg.Wait()
173+
174+
t.Logf("with limit %d, accepted a peak of %d simultaneous connections", max, peakOpen)
175+
if peakOpen > max {
176+
t.Errorf("want at most %d", max)
177+
}
178+
}()
179+
180+
for wave := 0; wave < waves; wave++ {
181+
var dialed, served int32
182+
var wg sync.WaitGroup
183+
for n := attemptsPerWave; n > 0; n-- {
184+
wg.Add(1)
185+
go func() {
186+
defer wg.Done()
187+
188+
c, err := net.Dial(l.Addr().Network(), l.Addr().String())
189+
if err != nil {
190+
t.Log(err)
191+
return
192+
}
193+
atomic.AddInt32(&dialed, 1)
194+
defer c.Close()
195+
196+
if b, err := io.ReadAll(c); len(b) < len(msg) {
197+
t.Log(err)
198+
return
199+
}
200+
atomic.AddInt32(&served, 1)
201+
}()
202+
}
203+
wg.Wait()
204+
205+
t.Logf("served %d connections (of %d dialed, %d attempted)", served, dialed, attemptsPerWave)
206+
// We expect that the kernel can queue at least attemptsPerWave
207+
// connections at a time (since it's only a small number), so every
208+
// connection should eventually be served.
209+
if served != attemptsPerWave {
210+
t.Errorf("expected %d served", attemptsPerWave)
211+
}
106212
}
107213
}
108214

@@ -160,9 +266,7 @@ func TestLimitListenerClose(t *testing.T) {
160266

161267
// Allow the subsequent Accept to block before closing the listener.
162268
// (Accept should unblock and return.)
163-
timer := time.AfterFunc(10*time.Millisecond, func() {
164-
ln.Close()
165-
})
269+
timer := time.AfterFunc(10*time.Millisecond, func() { ln.Close() })
166270

167271
c, err = ln.Accept()
168272
if err == nil {

0 commit comments

Comments
 (0)