github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/leak-detect_test.go (about)

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