1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
|
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package leaktest provides tools to detect leaked goroutines in tests.
// To use it, call "defer leaktest.Check(t)()" at the beginning of each
// test that may use goroutines.
// copied out of the cockroachdb source tree with slight modifications to be
// more re-useable
package leaktest
import (
"context"
"fmt"
"runtime"
"sort"
"strconv"
"strings"
"time"
)
type goroutine struct {
id uint64
stack string
}
type goroutineByID []*goroutine
func (g goroutineByID) Len() int { return len(g) }
func (g goroutineByID) Less(i, j int) bool { return g[i].id < g[j].id }
func (g goroutineByID) Swap(i, j int) { g[i], g[j] = g[j], g[i] }
func interestingGoroutine(g string) (*goroutine, error) {
sl := strings.SplitN(g, "\n", 2)
if len(sl) != 2 {
return nil, fmt.Errorf("error parsing stack: %q", g)
}
stack := strings.TrimSpace(sl[1])
if strings.HasPrefix(stack, "testing.RunTests") {
return nil, nil
}
if stack == "" ||
// Ignore HTTP keep alives
strings.Contains(stack, ").readLoop(") ||
strings.Contains(stack, ").writeLoop(") ||
// Below are the stacks ignored by the upstream leaktest code.
strings.Contains(stack, "testing.Main(") ||
strings.Contains(stack, "testing.(*T).Run(") ||
strings.Contains(stack, "runtime.goexit") ||
strings.Contains(stack, "created by runtime.gc") ||
strings.Contains(stack, "interestingGoroutines") ||
strings.Contains(stack, "runtime.MHeap_Scavenger") ||
strings.Contains(stack, "signal.signal_recv") ||
strings.Contains(stack, "sigterm.handler") ||
strings.Contains(stack, "runtime_mcall") ||
strings.Contains(stack, "goroutine in C code") {
return nil, nil
}
// Parse the goroutine's ID from the header line.
h := strings.SplitN(sl[0], " ", 3)
if len(h) < 3 {
return nil, fmt.Errorf("error parsing stack header: %q", sl[0])
}
id, err := strconv.ParseUint(h[1], 10, 64)
if err != nil {
return nil, fmt.Errorf("error parsing goroutine id: %s", err)
}
return &goroutine{id: id, stack: strings.TrimSpace(g)}, nil
}
// interestingGoroutines returns all goroutines we care about for the purpose
// of leak checking. It excludes testing or runtime ones.
func interestingGoroutines(t ErrorReporter) []*goroutine {
buf := make([]byte, 2<<20)
buf = buf[:runtime.Stack(buf, true)]
var gs []*goroutine
for _, g := range strings.Split(string(buf), "\n\n") {
gr, err := interestingGoroutine(g)
if err != nil {
t.Errorf("leaktest: %s", err)
continue
} else if gr == nil {
continue
}
gs = append(gs, gr)
}
sort.Sort(goroutineByID(gs))
return gs
}
// ErrorReporter is a tiny subset of a testing.TB to make testing not such a
// massive pain
type ErrorReporter interface {
Errorf(format string, args ...interface{})
}
// Check snapshots the currently-running goroutines and returns a
// function to be run at the end of tests to see whether any
// goroutines leaked, waiting up to 5 seconds in error conditions
func Check(t ErrorReporter) func() {
return CheckTimeout(t, 5*time.Second)
}
// CheckTimeout is the same as Check, but with a configurable timeout
func CheckTimeout(t ErrorReporter, dur time.Duration) func() {
ctx, cancel := context.WithCancel(context.Background())
fn := CheckContext(ctx, t)
return func() {
timer := time.AfterFunc(dur, cancel)
fn()
// Remember to clean up the timer and context
timer.Stop()
cancel()
}
}
// CheckContext is the same as Check, but uses a context.Context for
// cancellation and timeout control
func CheckContext(ctx context.Context, t ErrorReporter) func() {
orig := map[uint64]bool{}
for _, g := range interestingGoroutines(t) {
orig[g.id] = true
}
return func() {
var leaked []string
for {
select {
case <-ctx.Done():
t.Errorf("leaktest: timed out checking goroutines")
default:
leaked = make([]string, 0)
for _, g := range interestingGoroutines(t) {
if !orig[g.id] {
leaked = append(leaked, g.stack)
}
}
if len(leaked) == 0 {
return
}
// don't spin needlessly
time.Sleep(time.Millisecond * 50)
continue
}
break
}
for _, g := range leaked {
t.Errorf("leaktest: leaked goroutine: %v", g)
}
}
}
|