You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
183 lines
5.3 KiB
183 lines
5.3 KiB
// 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.
|
|
|
|
// Copyright 2016 PingCAP, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//go:build leak
|
|
// +build leak
|
|
|
|
package testleak
|
|
|
|
import (
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pingcap/check"
|
|
)
|
|
|
|
func interestingGoroutines() (gs []string) {
|
|
buf := make([]byte, 2<<20)
|
|
buf = buf[:runtime.Stack(buf, true)]
|
|
ignoreList := []string{
|
|
"created by github.com/pingcap/tidb.init",
|
|
"testing.RunTests",
|
|
"check.(*resultTracker).start",
|
|
"check.(*suiteRunner).runFunc",
|
|
"check.(*suiteRunner).parallelRun",
|
|
"localstore.(*dbStore).scheduler",
|
|
"tikv.(*noGCHandler).Start",
|
|
"ddl.(*ddl).start",
|
|
"ddl.(*delRange).startEmulator",
|
|
"domain.NewDomain",
|
|
"testing.(*T).Run",
|
|
"domain.(*Domain).LoadPrivilegeLoop",
|
|
"domain.(*Domain).UpdateTableStatsLoop",
|
|
"testing.Main(",
|
|
"runtime.goexit",
|
|
"created by runtime.gc",
|
|
"interestingGoroutines",
|
|
"runtime.MHeap_Scavenger",
|
|
"created by os/signal.init",
|
|
"gopkg.in/natefinch/lumberjack%2ev2.(*Logger).millRun",
|
|
// these go routines are async terminated, so they may still alive after test end, thus cause
|
|
// false positive leak failures
|
|
"google.golang.org/grpc.(*addrConn).resetTransport",
|
|
"google.golang.org/grpc.(*ccBalancerWrapper).watcher",
|
|
"github.com/pingcap/goleveldb/leveldb/util.(*BufferPool).drain",
|
|
"github.com/pingcap/goleveldb/leveldb.(*DB).compactionError",
|
|
"github.com/pingcap/goleveldb/leveldb.(*DB).mpoolDrain",
|
|
"go.etcd.io/etcd/pkg/logutil.(*MergeLogger).outputLoop",
|
|
"go.etcd.io/etcd/v3/pkg/logutil.(*MergeLogger).outputLoop",
|
|
"oracles.(*pdOracle).updateTS",
|
|
"tikv.(*tikvStore).runSafePointChecker",
|
|
"tikv.(*RegionCache).asyncCheckAndResolveLoop",
|
|
"github.com/pingcap/badger",
|
|
"github.com/ngaut/unistore/tikv.(*MVCCStore).runUpdateSafePointLoop",
|
|
}
|
|
shouldIgnore := func(stack string) bool {
|
|
if stack == "" {
|
|
return true
|
|
}
|
|
for _, ident := range ignoreList {
|
|
if strings.Contains(stack, ident) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
for _, g := range strings.Split(string(buf), "\n\n") {
|
|
sl := strings.SplitN(g, "\n", 2)
|
|
if len(sl) != 2 {
|
|
continue
|
|
}
|
|
stack := strings.TrimSpace(sl[1])
|
|
if shouldIgnore(stack) {
|
|
continue
|
|
}
|
|
gs = append(gs, stack)
|
|
}
|
|
sort.Strings(gs)
|
|
return
|
|
}
|
|
|
|
var beforeTestGoroutines = map[string]bool{}
|
|
var testGoroutinesInited bool
|
|
|
|
// BeforeTest gets the current goroutines.
|
|
// It's used for check.Suite.SetUpSuite() function.
|
|
// Now it's only used in the tidb_test.go.
|
|
// Note: it's not accurate, consider the following function:
|
|
//
|
|
// func loop() {
|
|
// for {
|
|
// select {
|
|
// case <-ticker.C:
|
|
// DoSomething()
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// If this loop step into DoSomething() during BeforeTest(), the stack for this goroutine will contain DoSomething().
|
|
// Then if this loop jumps out of DoSomething during AfterTest(), the stack for this goroutine will not contain DoSomething().
|
|
// Resulting in false-positive leak reports.
|
|
func BeforeTest() {
|
|
for _, g := range interestingGoroutines() {
|
|
beforeTestGoroutines[g] = true
|
|
}
|
|
testGoroutinesInited = true
|
|
}
|
|
|
|
const defaultCheckCnt = 50
|
|
|
|
func checkLeakAfterTest(errorFunc func(cnt int, g string)) func() {
|
|
// After `BeforeTest`, `beforeTestGoroutines` may still be empty, in this case,
|
|
// we shouldn't init it again.
|
|
if !testGoroutinesInited && len(beforeTestGoroutines) == 0 {
|
|
for _, g := range interestingGoroutines() {
|
|
beforeTestGoroutines[g] = true
|
|
}
|
|
}
|
|
|
|
cnt := defaultCheckCnt
|
|
return func() {
|
|
defer func() {
|
|
beforeTestGoroutines = map[string]bool{}
|
|
testGoroutinesInited = false
|
|
}()
|
|
|
|
var leaked []string
|
|
for i := 0; i < cnt; i++ {
|
|
leaked = leaked[:0]
|
|
for _, g := range interestingGoroutines() {
|
|
if !beforeTestGoroutines[g] {
|
|
leaked = append(leaked, g)
|
|
}
|
|
}
|
|
// Bad stuff found, but goroutines might just still be
|
|
// shutting down, so give it some time.
|
|
if len(leaked) != 0 {
|
|
time.Sleep(50 * time.Millisecond)
|
|
continue
|
|
}
|
|
|
|
return
|
|
}
|
|
for _, g := range leaked {
|
|
errorFunc(cnt, g)
|
|
}
|
|
}
|
|
}
|
|
|
|
// AfterTest gets the current goroutines and runs the returned function to
|
|
// get the goroutines at that time to contrast whether any goroutines leaked.
|
|
// Usage: defer testleak.AfterTest(c)()
|
|
// It can call with BeforeTest() at the beginning of check.Suite.TearDownSuite() or
|
|
// call alone at the beginning of each test.
|
|
func AfterTest(c *check.C) func() {
|
|
errorFunc := func(cnt int, g string) {
|
|
c.Errorf("Test %s check-count %d appears to have leaked: %v", c.TestName(), cnt, g)
|
|
}
|
|
return checkLeakAfterTest(errorFunc)
|
|
}
|
|
|
|
// AfterTestT is used after all the test cases is finished.
|
|
func AfterTestT(t *testing.T) func() {
|
|
errorFunc := func(cnt int, g string) {
|
|
t.Errorf("Test %s check-count %d appears to have leaked: %v", t.Name(), cnt, g)
|
|
}
|
|
return checkLeakAfterTest(errorFunc)
|
|
}
|
|
|