test(gopher): verify cancel goroutine exits when Download returns
This commit is contained in:
@@ -6,9 +6,11 @@ package gopher
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -594,3 +596,78 @@ func TestGopherResumeInformation(t *testing.T) {
|
||||
t.Error("expected .goget.meta to be deleted after successful resume")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGopherCancelGoroutineExits verifies that the ctx-cancellation
|
||||
// goroutine spawned by Download does not outlive Download itself.
|
||||
// The goroutine used to wait forever on <-ctx.Done() when the parent
|
||||
// context was never cancelled; the done-channel bound added with M3
|
||||
// must let every iteration return to the runtime goroutine pool.
|
||||
func TestGopherCancelGoroutineExits(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
|
||||
go func() {
|
||||
// Drain connections so the listener does not backpressure
|
||||
// the test loop. We do not read or write — a quick accept
|
||||
// and close is enough for Download to complete.
|
||||
for {
|
||||
conn, lerr := listener.Accept()
|
||||
if lerr != nil {
|
||||
return
|
||||
}
|
||||
go func(c net.Conn) {
|
||||
// Reply with the response header so the client
|
||||
// reaches downloadText and spawns its goroutine.
|
||||
defer c.Close()
|
||||
fmt.Fprintf(c, "0hi\t/x\th\t70\r\n.\r\n")
|
||||
// Block until the peer closes — this lets the
|
||||
// client-side goroutine sit on ReadString until
|
||||
// it's raced by done, exercising the bound.
|
||||
buf := make([]byte, 1)
|
||||
c.Read(buf)
|
||||
}(conn)
|
||||
}
|
||||
}()
|
||||
|
||||
// Warm up: a single download is enough to settle the runtime
|
||||
// goroutine pool (timer, GC, poll, etc.) before we measure.
|
||||
proto := NewProtocol()
|
||||
u, _ := url.Parse("gopher://127.0.0.1:" + strconv.Itoa(port) + "/x")
|
||||
req := core.DownloadRequest{URL: u, Writer: &stringWriter{&strings.Builder{}}}
|
||||
if _, dlErr := proto.Download(context.Background(), &req); dlErr != nil {
|
||||
t.Fatalf("warmup: %v", dlErr)
|
||||
}
|
||||
|
||||
// Settle baseline after warmup.
|
||||
runtime.GC()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
runtime.GC()
|
||||
baseline := runtime.NumGoroutine()
|
||||
|
||||
const iterations = 50
|
||||
for i := 0; i < iterations; i++ {
|
||||
if _, dlErr := proto.Download(context.Background(), &req); dlErr != nil {
|
||||
t.Fatalf("download %d: %v", i, dlErr)
|
||||
}
|
||||
}
|
||||
|
||||
// After every successful Download returns, its cancellation
|
||||
// goroutine must have exited. Allow a brief settle window for
|
||||
// the runtime to reclaim them.
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
runtime.GC()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
// One tolerance for any incidental stdlib goroutine that
|
||||
// the Go runtime occasionally spins up between calls.
|
||||
if runtime.NumGoroutine() <= baseline+1 {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("goroutine leak: baseline=%d, after %d downloads = %d",
|
||||
baseline, iterations, runtime.NumGoroutine())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user