| // Copyright 2018 Google LLC |
| // |
| // 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, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // Package leakcheck contains functions to check leaked goroutines. |
| // |
| // Call "defer leakcheck.Check(t)" at the beginning of tests. |
| // |
| // This is very closely based on grpc-go's leakcheck. |
| package leakcheck |
| |
| import ( |
| "runtime" |
| "sort" |
| "strings" |
| "time" |
| ) |
| |
| var goroutinesToIgnore = []string{ |
| "net/http/transport.go", |
| "net/http/h2_bundle.go", |
| "src/go.opencensus.io/stats/view/worker.go", |
| "testing.Main(", |
| "testing.tRunner(", |
| "testing.(*M).", |
| "runtime.goexit", |
| "created by runtime.gc", |
| "created by runtime/trace.Start", |
| "interestingGoroutines", |
| "runtime.MHeap_Scavenger", |
| "signal.signal_recv", |
| "sigterm.handler", |
| "runtime_mcall", |
| "(*loggingT).flushDaemon", |
| "goroutine in C code", |
| } |
| |
| // RegisterIgnoreGoroutine appends s into the ignore goroutine list. The |
| // goroutines whose stack trace contains s will not be identified as leaked |
| // goroutines. Not thread-safe, only call this function in init(). |
| func RegisterIgnoreGoroutine(s string) { |
| goroutinesToIgnore = append(goroutinesToIgnore, s) |
| } |
| |
| func ignore(g string) bool { |
| sl := strings.SplitN(g, "\n", 2) |
| if len(sl) != 2 { |
| return true |
| } |
| stack := strings.TrimSpace(sl[1]) |
| if strings.HasPrefix(stack, "testing.RunTests") { |
| return true |
| } |
| |
| if stack == "" { |
| return true |
| } |
| |
| for _, s := range goroutinesToIgnore { |
| if strings.Contains(stack, s) { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| // interestingGoroutines returns all goroutines we care about for the purpose of |
| // leak checking. It excludes testing or runtime ones. |
| func interestingGoroutines() (gs []string) { |
| buf := make([]byte, 2<<20) |
| buf = buf[:runtime.Stack(buf, true)] |
| for _, g := range strings.Split(string(buf), "\n\n") { |
| if !ignore(g) { |
| gs = append(gs, g) |
| } |
| } |
| sort.Strings(gs) |
| return |
| } |
| |
| // Errorfer is the interface that wraps the Errorf method. It's a subset of |
| // testing.TB to make it easy to use Check. |
| type Errorfer interface { |
| Errorf(format string, args ...interface{}) |
| } |
| |
| func check(efer Errorfer, timeout time.Duration) { |
| // Loop, waiting for goroutines to shut down. |
| // Wait up to timeout, but finish as quickly as possible. |
| deadline := time.Now().Add(timeout) |
| var leaked []string |
| for time.Now().Before(deadline) { |
| if leaked = interestingGoroutines(); len(leaked) == 0 { |
| return |
| } |
| time.Sleep(50 * time.Millisecond) |
| } |
| for _, g := range leaked { |
| efer.Errorf("Leaked goroutine: %v", g) |
| } |
| } |
| |
| // Check looks at the currently-running goroutines and checks if there are any |
| // interestring (created by gRPC) goroutines leaked. It waits up to 10 seconds |
| // in the error cases. |
| func Check(efer Errorfer) { |
| check(efer, 10*time.Second) |
| } |