internal/poll: inject a hook into the runtime finalizer to count the closed pipes

Fixes #48066

Change-Id: Icd6974dfcc496c054bb096e5d70de6e135984517
Reviewed-on: https://go-review.googlesource.com/c/go/+/349774
Reviewed-by: Bryan C. Mills <bcmills@google.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Trust: Bryan C. Mills <bcmills@google.com>
Run-TryBot: Bryan C. Mills <bcmills@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
This commit is contained in:
Andy Pan 2021-09-15 12:37:20 +08:00 committed by Ian Lance Taylor
parent 323c6f74d3
commit 74e384f50d
1 changed files with 35 additions and 25 deletions

View File

@ -6,23 +6,25 @@ package poll_test
import ( import (
"internal/poll" "internal/poll"
"internal/syscall/unix"
"runtime" "runtime"
"syscall" "sync"
"sync/atomic"
"testing" "testing"
"time" "time"
) )
// checkPipes returns true if all pipes are closed properly, false otherwise. var closeHook atomic.Value // func(fd int)
func checkPipes(fds []int) bool {
for _, fd := range fds { func init() {
// Check if each pipe fd has been closed. closeFunc := poll.CloseFunc
_, _, errno := syscall.Syscall(unix.FcntlSyscall, uintptr(fd), syscall.F_GETPIPE_SZ, 0) poll.CloseFunc = func(fd int) (err error) {
if errno == 0 { if v := closeHook.Load(); v != nil {
return false if hook := v.(func(int)); hook != nil {
hook(fd)
} }
} }
return true return closeFunc(fd)
}
} }
func TestSplicePipePool(t *testing.T) { func TestSplicePipePool(t *testing.T) {
@ -30,16 +32,22 @@ func TestSplicePipePool(t *testing.T) {
var ( var (
p *poll.SplicePipe p *poll.SplicePipe
ps []*poll.SplicePipe ps []*poll.SplicePipe
fds []int allFDs []int
pendingFDs sync.Map // fd → struct{}{}
err error err error
) )
closeHook.Store(func(fd int) { pendingFDs.Delete(fd) })
t.Cleanup(func() { closeHook.Store((func(int))(nil)) })
for i := 0; i < N; i++ { for i := 0; i < N; i++ {
p, _, err = poll.GetPipe() p, _, err = poll.GetPipe()
if err != nil { if err != nil {
t.Skip("failed to create pipe, skip this test") t.Skipf("failed to create pipe due to error(%v), skip this test", err)
} }
_, pwfd := poll.GetPipeFds(p) _, pwfd := poll.GetPipeFds(p)
fds = append(fds, pwfd) allFDs = append(allFDs, pwfd)
pendingFDs.Store(pwfd, struct{}{})
ps = append(ps, p) ps = append(ps, p)
} }
for _, p = range ps { for _, p = range ps {
@ -62,19 +70,21 @@ func TestSplicePipePool(t *testing.T) {
for { for {
runtime.GC() runtime.GC()
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
if checkPipes(fds) {
// Detect whether all pipes are closed properly.
var leakedFDs []int
pendingFDs.Range(func(k, v interface{}) bool {
leakedFDs = append(leakedFDs, k.(int))
return true
})
if len(leakedFDs) == 0 {
break break
} }
select { select {
case <-expiredTime.C: case <-expiredTime.C:
t.Logf("descriptors to check: %v", fds) t.Logf("all descriptors: %v", allFDs)
for _, fd := range fds { t.Fatalf("leaked descriptors: %v", leakedFDs)
_, _, errno := syscall.Syscall(unix.FcntlSyscall, uintptr(fd), syscall.F_GETPIPE_SZ, 0)
if errno == 0 {
t.Errorf("descriptor %d still open", fd)
}
}
t.Fatal("at least one pipe is still open")
default: default:
} }
} }