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 }