test(gemini): verify cancel goroutine exits when Download returns
This commit is contained in:
@@ -16,6 +16,7 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -450,3 +451,81 @@ func TestSaveGeminiResumeNoop(t *testing.T) {
|
||||
// Should not panic.
|
||||
})
|
||||
}
|
||||
|
||||
// TestGeminiCancelGoroutineExits verifies that the ctx-cancellation
|
||||
// goroutine spawned by Gemini downloadWithRedirects 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
|
||||
// must let every iteration return to the runtime goroutine pool.
|
||||
func TestGeminiCancelGoroutineExits(t *testing.T) {
|
||||
cfg := testTLSConfig()
|
||||
listener, err := tls.Listen("tcp", "127.0.0.1:0", cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, lerr := listener.Accept()
|
||||
if lerr != nil {
|
||||
return
|
||||
}
|
||||
go func(c net.Conn) {
|
||||
defer c.Close()
|
||||
r := bufio.NewReader(c)
|
||||
r.ReadString('\n')
|
||||
// Brief response that Gemini Download can chew
|
||||
// through quickly, spawning the cancellation
|
||||
// goroutine and returning. The goroutine must
|
||||
// exit via the done channel on return.
|
||||
c.Write([]byte("20 application/octet-stream\r\n"))
|
||||
c.Write([]byte("hi"))
|
||||
}(conn)
|
||||
}
|
||||
}()
|
||||
|
||||
// Warm up to settle the runtime goroutine pool (timer, GC, etc.).
|
||||
proto := NewProtocol()
|
||||
proto.TLSInsecure = true
|
||||
u, _ := url.Parse("gemini://127.0.0.1:" + strconv.Itoa(port) + "/warmup")
|
||||
if _, dlErr := proto.Download(context.Background(), &core.DownloadRequest{
|
||||
URL: u,
|
||||
Writer: &stringWriter{&strings.Builder{}},
|
||||
}); dlErr != nil {
|
||||
t.Fatalf("warmup: %v", dlErr)
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
runtime.GC()
|
||||
baseline := runtime.NumGoroutine()
|
||||
|
||||
const iterations = 50
|
||||
for i := 0; i < iterations; i++ {
|
||||
u, _ := url.Parse("gemini://127.0.0.1:" + strconv.Itoa(port) + "/x")
|
||||
if _, dlErr := proto.Download(context.Background(), &core.DownloadRequest{
|
||||
URL: u,
|
||||
Writer: &stringWriter{&strings.Builder{}},
|
||||
}); 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