storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/leak-detect_test.go (about)

     1  // Copyright 2013 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package cmd
     6  
     7  import (
     8  	"runtime/debug"
     9  	"sort"
    10  	"strings"
    11  	"time"
    12  )
    13  
    14  const (
    15  	// deadline (in seconds) up to which the go routine leak detection has to be retried.
    16  	leakDetectDeadline = 5
    17  	// pause time (in milliseconds) between each snapshot at the end of the go routine leak detection.
    18  	leakDetectPauseTimeMs = 50
    19  )
    20  
    21  // LeakDetect - type with  methods for go routine leak detection.
    22  type LeakDetect struct {
    23  	relevantRoutines map[string]bool
    24  }
    25  
    26  // NewLeakDetect - Initialize a LeakDetector with the snapshot of relevant Go routines.
    27  func NewLeakDetect() LeakDetect {
    28  	snapshot := LeakDetect{
    29  		relevantRoutines: make(map[string]bool),
    30  	}
    31  	for _, g := range pickRelevantGoroutines() {
    32  		snapshot.relevantRoutines[g] = true
    33  	}
    34  	return snapshot
    35  }
    36  
    37  // CompareCurrentSnapshot - Compares the initial relevant stack trace with the current one (during the time of invocation).
    38  func (initialSnapShot LeakDetect) CompareCurrentSnapshot() []string {
    39  	var stackDiff []string
    40  	for _, g := range pickRelevantGoroutines() {
    41  		// Identify the Go routines those were not present in the initial snapshot.
    42  		// In other words a stack diff.
    43  		if !initialSnapShot.relevantRoutines[g] {
    44  			stackDiff = append(stackDiff, g)
    45  		}
    46  	}
    47  	return stackDiff
    48  }
    49  
    50  // DetectLeak - Creates a snapshot of runtime stack and compares it with the initial stack snapshot.
    51  func (initialSnapShot LeakDetect) DetectLeak(t TestErrHandler) {
    52  	if t.Failed() {
    53  		return
    54  	}
    55  	// Loop, waiting for goroutines to shut down.
    56  	// Wait up to 5 seconds, but finish as quickly as possible.
    57  	deadline := UTCNow().Add(leakDetectDeadline * time.Second)
    58  	for {
    59  		// get sack snapshot of relevant go routines.
    60  		leaked := initialSnapShot.CompareCurrentSnapshot()
    61  		// current stack snapshot matches the initial one, no leaks, return.
    62  		if len(leaked) == 0 {
    63  			return
    64  		}
    65  		// wait a test again will deadline.
    66  		if UTCNow().Before(deadline) {
    67  			time.Sleep(leakDetectPauseTimeMs * time.Millisecond)
    68  			continue
    69  		}
    70  		// after the deadline time report all the difference in the latest snapshot compared with the initial one.
    71  		for _, g := range leaked {
    72  			t.Errorf("Leaked goroutine: %v", g)
    73  		}
    74  		return
    75  	}
    76  }
    77  
    78  // DetectTestLeak -  snapshots the currently running goroutines and returns a
    79  // function to be run at the end of tests to see whether any
    80  // goroutines leaked.
    81  // Usage: `defer DetectTestLeak(t)()` in beginning line of benchmarks or unit tests.
    82  func DetectTestLeak(t TestErrHandler) func() {
    83  	initialStackSnapShot := NewLeakDetect()
    84  	return func() {
    85  		initialStackSnapShot.DetectLeak(t)
    86  	}
    87  }
    88  
    89  // list of functions to be ignored from the stack trace.
    90  // Leak detection is done when tests are run, should ignore the tests related functions,
    91  // and other runtime functions while identifying leaks.
    92  var ignoredStackFns = []string{
    93  	"",
    94  	// Below are the stacks ignored by the upstream leaktest code.
    95  	"testing.Main(",
    96  	"testing.tRunner(",
    97  	"testing.tRunner(",
    98  	"runtime.goexit",
    99  	"created by runtime.gc",
   100  	// ignore the snapshot function.
   101  	// since the snapshot is taken here the entry will have the current function too.
   102  	"pickRelevantGoroutines",
   103  	"runtime.MHeap_Scavenger",
   104  	"signal.signal_recv",
   105  	"sigterm.handler",
   106  	"runtime_mcall",
   107  	"goroutine in C code",
   108  }
   109  
   110  // Identify whether the stack trace entry is part of ignoredStackFn .
   111  func isIgnoredStackFn(stack string) (ok bool) {
   112  	ok = true
   113  	for _, stackFn := range ignoredStackFns {
   114  		if !strings.Contains(stack, stackFn) {
   115  			ok = false
   116  			continue
   117  		}
   118  		break
   119  	}
   120  	return ok
   121  }
   122  
   123  // pickRelevantGoroutines returns all goroutines we care about for the purpose
   124  // of leak checking. It excludes testing or runtime ones.
   125  func pickRelevantGoroutines() (gs []string) {
   126  	// get runtime stack buffer.
   127  	buf := debug.Stack()
   128  	// runtime stack of go routines will be listed with 2 blank spaces between each of them, so split on "\n\n" .
   129  	for _, g := range strings.Split(string(buf), "\n\n") {
   130  		// Again split on a new line, the first line of the second half contaisn the info about the go routine.
   131  		sl := strings.SplitN(g, "\n", 2)
   132  		if len(sl) != 2 {
   133  			continue
   134  		}
   135  		stack := strings.TrimSpace(sl[1])
   136  		// ignore the testing go routine.
   137  		// since the tests will be invoking the leaktest it would contain the test go routine.
   138  		if strings.HasPrefix(stack, "testing.RunTests") {
   139  			continue
   140  		}
   141  		// Ignore the following go routines.
   142  		// testing and run time go routines should be ignored, only the application generated go routines should be taken into account.
   143  		if isIgnoredStackFn(stack) {
   144  			continue
   145  		}
   146  		gs = append(gs, g)
   147  	}
   148  	sort.Strings(gs)
   149  	return
   150  }