1package testhelper 2 3import ( 4 "fmt" 5 "os" 6 "os/exec" 7 "strings" 8 "syscall" 9 "time" 10 11 "gitlab.com/gitlab-org/gitaly/v14/internal/command/commandcounter" 12 "gitlab.com/gitlab-org/gitaly/v14/internal/helper/text" 13 "go.uber.org/goleak" 14) 15 16// mustHaveNoGoroutines panics if it finds any Goroutines running. 17func mustHaveNoGoroutines() { 18 if err := goleak.Find( 19 // opencensus has a "defaultWorker" which is started by the package's 20 // `init()` function. There is no way to stop this worker, so it will leak 21 // whenever we import the package. 22 goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"), 23 // The Ruby server's load balancer is registered in the `init()` function 24 // of our "rubyserver/balancer" package. Ideally we'd clean this up 25 // eventually, but the pragmatic approach is to just wait until we remove 26 // the Ruby sidecar altogether. 27 goleak.IgnoreTopFunction("google.golang.org/grpc.(*ccBalancerWrapper).watcher"), 28 goleak.IgnoreTopFunction("gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/rubyserver/balancer.(*builder).monitor"), 29 // labkit's logger spawns a Goroutine which cannot be closed when calling 30 // `Initialize()`. 31 goleak.IgnoreTopFunction("gitlab.com/gitlab-org/labkit/log.listenForSignalHangup"), 32 // The backchannel code is somehow stock on closing its connections. I have no clue 33 // why that is, but we should investigate. 34 goleak.IgnoreTopFunction("gitlab.com/gitlab-org/gitaly/v14/internal/backchannel.clientHandshake.serve.func4"), 35 ); err != nil { 36 panic(fmt.Errorf("goroutines running: %w", err)) 37 } 38} 39 40// mustHaveNoChildProcess panics if it finds a running or finished child 41// process. It waits for 2 seconds for processes to be cleaned up by other 42// goroutines. 43func mustHaveNoChildProcess() { 44 waitDone := make(chan struct{}) 45 go func() { 46 commandcounter.WaitAllDone() 47 close(waitDone) 48 }() 49 50 select { 51 case <-waitDone: 52 case <-time.After(2 * time.Second): 53 } 54 55 if err := mustFindNoFinishedChildProcess(); err != nil { 56 panic(err) 57 } 58 59 if err := mustFindNoRunningChildProcess(); err != nil { 60 panic(err) 61 } 62} 63 64func mustFindNoFinishedChildProcess() error { 65 // Wait4(pid int, wstatus *WaitStatus, options int, rusage *Rusage) (wpid int, err error) 66 // 67 // We use pid -1 to wait for any child. We don't care about wstatus or 68 // rusage. Use WNOHANG to return immediately if there is no child waiting 69 // to be reaped. 70 wpid, err := syscall.Wait4(-1, nil, syscall.WNOHANG, nil) 71 if err == nil && wpid > 0 { 72 return fmt.Errorf("wait4 found child process %d", wpid) 73 } 74 75 return nil 76} 77 78func mustFindNoRunningChildProcess() error { 79 pgrep := exec.Command("pgrep", "-P", fmt.Sprintf("%d", os.Getpid())) 80 desc := fmt.Sprintf("%q", strings.Join(pgrep.Args, " ")) 81 82 out, err := pgrep.Output() 83 if err == nil { 84 pidsComma := strings.Replace(text.ChompBytes(out), "\n", ",", -1) 85 psOut, _ := exec.Command("ps", "-o", "pid,args", "-p", pidsComma).Output() 86 return fmt.Errorf("found running child processes %s:\n%s", pidsComma, psOut) 87 } 88 89 exitError, ok := err.(*exec.ExitError) 90 if !ok { 91 return fmt.Errorf("expected ExitError, got %T", err) 92 } 93 94 if exitError.ExitCode() == 1 { 95 return nil 96 } 97 98 return fmt.Errorf("%s: %w", desc, err) 99} 100