Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
490 lines
13 KiB
Go
490 lines
13 KiB
Go
/*
|
|
*
|
|
* Copyright 2017 gRPC authors.
|
|
*
|
|
* 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 and buffers.
|
|
//
|
|
// Call the following at the beginning of test:
|
|
//
|
|
// defer leakcheck.NewLeakChecker(t).Check()
|
|
package leakcheck
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"google.golang.org/grpc/internal"
|
|
"google.golang.org/grpc/mem"
|
|
)
|
|
|
|
// failTestsOnLeakedBuffers is a special flag that will cause tests to fail if
|
|
// leaked buffers are detected, instead of simply logging them as an
|
|
// informational failure. This can be enabled with the "checkbuffers" compile
|
|
// flag, e.g.:
|
|
//
|
|
// go test -tags=checkbuffers
|
|
var failTestsOnLeakedBuffers = false
|
|
|
|
func init() {
|
|
defaultPool := mem.DefaultBufferPool()
|
|
globalPool.Store(&defaultPool)
|
|
internal.SetDefaultBufferPool.(func(mem.BufferPool))(&globalPool)
|
|
}
|
|
|
|
var globalPool swappableBufferPool
|
|
var globalTimerTracker *timerFactory
|
|
|
|
type swappableBufferPool struct {
|
|
atomic.Pointer[mem.BufferPool]
|
|
}
|
|
|
|
func (b *swappableBufferPool) Get(length int) *[]byte {
|
|
return (*b.Load()).Get(length)
|
|
}
|
|
|
|
func (b *swappableBufferPool) Put(buf *[]byte) {
|
|
(*b.Load()).Put(buf)
|
|
}
|
|
|
|
// SetTrackingBufferPool replaces the default buffer pool in the mem package to
|
|
// one that tracks where buffers are allocated. CheckTrackingBufferPool should
|
|
// then be invoked at the end of the test to validate that all buffers pulled
|
|
// from the pool were returned.
|
|
func SetTrackingBufferPool(logger Logger) {
|
|
newPool := mem.BufferPool(&trackingBufferPool{
|
|
pool: *globalPool.Load(),
|
|
logger: logger,
|
|
allocatedBuffers: make(map[*[]byte][]uintptr),
|
|
})
|
|
globalPool.Store(&newPool)
|
|
}
|
|
|
|
// CheckTrackingBufferPool undoes the effects of SetTrackingBufferPool, and fails
|
|
// unit tests if not all buffers were returned. It is invalid to invoke this
|
|
// function without previously having invoked SetTrackingBufferPool.
|
|
func CheckTrackingBufferPool() {
|
|
p := (*globalPool.Load()).(*trackingBufferPool)
|
|
p.lock.Lock()
|
|
defer p.lock.Unlock()
|
|
|
|
globalPool.Store(&p.pool)
|
|
|
|
type uniqueTrace struct {
|
|
stack []uintptr
|
|
count int
|
|
}
|
|
|
|
var totalLeakedBuffers int
|
|
var uniqueTraces []uniqueTrace
|
|
for _, stack := range p.allocatedBuffers {
|
|
idx, ok := slices.BinarySearchFunc(uniqueTraces, stack, func(trace uniqueTrace, stack []uintptr) int {
|
|
return slices.Compare(trace.stack, stack)
|
|
})
|
|
if !ok {
|
|
uniqueTraces = slices.Insert(uniqueTraces, idx, uniqueTrace{stack: stack})
|
|
}
|
|
uniqueTraces[idx].count++
|
|
totalLeakedBuffers++
|
|
}
|
|
|
|
for _, ut := range uniqueTraces {
|
|
frames := runtime.CallersFrames(ut.stack)
|
|
var trace strings.Builder
|
|
for {
|
|
f, ok := frames.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
trace.WriteString(f.Function)
|
|
trace.WriteString("\n\t")
|
|
trace.WriteString(f.File)
|
|
trace.WriteString(":")
|
|
trace.WriteString(strconv.Itoa(f.Line))
|
|
trace.WriteString("\n")
|
|
}
|
|
format := "%d allocated buffers never freed:\n%s"
|
|
args := []any{ut.count, trace.String()}
|
|
if failTestsOnLeakedBuffers {
|
|
p.logger.Errorf(format, args...)
|
|
} else {
|
|
p.logger.Logf("WARNING "+format, args...)
|
|
}
|
|
}
|
|
|
|
if totalLeakedBuffers > 0 {
|
|
p.logger.Logf("%g%% of buffers never freed", float64(totalLeakedBuffers)/float64(p.bufferCount))
|
|
}
|
|
}
|
|
|
|
type trackingBufferPool struct {
|
|
pool mem.BufferPool
|
|
logger Logger
|
|
|
|
lock sync.Mutex
|
|
bufferCount int
|
|
allocatedBuffers map[*[]byte][]uintptr
|
|
}
|
|
|
|
func (p *trackingBufferPool) Get(length int) *[]byte {
|
|
p.lock.Lock()
|
|
defer p.lock.Unlock()
|
|
p.bufferCount++
|
|
buf := p.pool.Get(length)
|
|
p.allocatedBuffers[buf] = currentStack(2)
|
|
return buf
|
|
}
|
|
|
|
func (p *trackingBufferPool) Put(buf *[]byte) {
|
|
p.lock.Lock()
|
|
defer p.lock.Unlock()
|
|
|
|
if _, ok := p.allocatedBuffers[buf]; !ok {
|
|
p.logger.Errorf("Unknown buffer freed:\n%s", string(debug.Stack()))
|
|
} else {
|
|
delete(p.allocatedBuffers, buf)
|
|
}
|
|
p.pool.Put(buf)
|
|
}
|
|
|
|
var goroutinesToIgnore = []string{
|
|
"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",
|
|
// Ignore the http read/write goroutines. gce metadata.OnGCE() was leaking
|
|
// these, root cause unknown.
|
|
//
|
|
// https://github.com/grpc/grpc-go/issues/5171
|
|
// https://github.com/grpc/grpc-go/issues/5173
|
|
"created by net/http.(*Transport).dialConn",
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Logger is the interface that wraps the Logf and Errorf method. It's a subset
|
|
// of testing.TB to make it easy to use this package.
|
|
type Logger interface {
|
|
Logf(format string, args ...any)
|
|
Errorf(format string, args ...any)
|
|
}
|
|
|
|
// CheckGoroutines looks at the currently-running goroutines and checks if there
|
|
// are any interesting (created by gRPC) goroutines leaked. It waits up to 10
|
|
// seconds in the error cases.
|
|
func CheckGoroutines(ctx context.Context, logger Logger) {
|
|
// Loop, waiting for goroutines to shut down.
|
|
// Wait up to timeout, but finish as quickly as possible.
|
|
var leaked []string
|
|
for ctx.Err() == nil {
|
|
if leaked = interestingGoroutines(); len(leaked) == 0 {
|
|
return
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
for _, g := range leaked {
|
|
logger.Errorf("Leaked goroutine: %v", g)
|
|
}
|
|
}
|
|
|
|
// LeakChecker captures a Logger and is returned by NewLeakChecker as a
|
|
// convenient method to set up leak check tests in a unit test.
|
|
type LeakChecker struct {
|
|
logger Logger
|
|
}
|
|
|
|
// NewLeakChecker offers a convenient way to set up the leak checks for a
|
|
// specific unit test. It can be used as follows, at the beginning of tests:
|
|
//
|
|
// defer leakcheck.NewLeakChecker(t).Check()
|
|
//
|
|
// It initially invokes SetTrackingBufferPool to set up buffer tracking, then the
|
|
// deferred LeakChecker.Check call will invoke CheckTrackingBufferPool and
|
|
// CheckGoroutines with a default timeout of 10 seconds.
|
|
func NewLeakChecker(logger Logger) *LeakChecker {
|
|
SetTrackingBufferPool(logger)
|
|
return &LeakChecker{logger: logger}
|
|
}
|
|
|
|
type timerFactory struct {
|
|
mu sync.Mutex
|
|
allocatedTimers map[internal.Timer][]uintptr
|
|
}
|
|
|
|
func (tf *timerFactory) timeAfterFunc(d time.Duration, f func()) internal.Timer {
|
|
tf.mu.Lock()
|
|
defer tf.mu.Unlock()
|
|
ch := make(chan internal.Timer, 1)
|
|
timer := time.AfterFunc(d, func() {
|
|
f()
|
|
tf.remove(<-ch)
|
|
})
|
|
ch <- timer
|
|
tf.allocatedTimers[timer] = currentStack(2)
|
|
return &trackingTimer{
|
|
Timer: timer,
|
|
parent: tf,
|
|
}
|
|
}
|
|
|
|
func (tf *timerFactory) remove(timer internal.Timer) {
|
|
tf.mu.Lock()
|
|
defer tf.mu.Unlock()
|
|
delete(tf.allocatedTimers, timer)
|
|
}
|
|
|
|
func (tf *timerFactory) pendingTimers() []string {
|
|
tf.mu.Lock()
|
|
defer tf.mu.Unlock()
|
|
leaked := []string{}
|
|
for _, stack := range tf.allocatedTimers {
|
|
leaked = append(leaked, fmt.Sprintf("Allocated timer never cancelled:\n%s", traceToString(stack)))
|
|
}
|
|
return leaked
|
|
}
|
|
|
|
type trackingTimer struct {
|
|
internal.Timer
|
|
parent *timerFactory
|
|
}
|
|
|
|
func (t *trackingTimer) Stop() bool {
|
|
t.parent.remove(t.Timer)
|
|
return t.Timer.Stop()
|
|
}
|
|
|
|
// TrackTimers replaces internal.TimerAfterFunc with one that tracks timer
|
|
// creations, stoppages and expirations. CheckTimers should then be invoked at
|
|
// the end of the test to validate that all timers created have either executed
|
|
// or are cancelled.
|
|
func TrackTimers() {
|
|
globalTimerTracker = &timerFactory{
|
|
allocatedTimers: make(map[internal.Timer][]uintptr),
|
|
}
|
|
internal.TimeAfterFunc = globalTimerTracker.timeAfterFunc
|
|
}
|
|
|
|
// CheckTimers undoes the effects of TrackTimers, and fails unit tests if not
|
|
// all timers were cancelled or executed. It is invalid to invoke this function
|
|
// without previously having invoked TrackTimers.
|
|
func CheckTimers(ctx context.Context, logger Logger) {
|
|
tt := globalTimerTracker
|
|
|
|
// Loop, waiting for timers to be cancelled.
|
|
// Wait up to timeout, but finish as quickly as possible.
|
|
var leaked []string
|
|
for ctx.Err() == nil {
|
|
if leaked = tt.pendingTimers(); len(leaked) == 0 {
|
|
return
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
for _, g := range leaked {
|
|
logger.Errorf("Leaked timers: %v", g)
|
|
}
|
|
|
|
// Reset the internal function.
|
|
internal.TimeAfterFunc = func(d time.Duration, f func()) internal.Timer {
|
|
return time.AfterFunc(d, f)
|
|
}
|
|
}
|
|
|
|
func currentStack(skip int) []uintptr {
|
|
var stackBuf [16]uintptr
|
|
var stack []uintptr
|
|
skip++
|
|
for {
|
|
n := runtime.Callers(skip, stackBuf[:])
|
|
stack = append(stack, stackBuf[:n]...)
|
|
if n < len(stackBuf) {
|
|
break
|
|
}
|
|
skip += len(stackBuf)
|
|
}
|
|
return stack
|
|
}
|
|
|
|
func traceToString(stack []uintptr) string {
|
|
frames := runtime.CallersFrames(stack)
|
|
var trace strings.Builder
|
|
for {
|
|
f, ok := frames.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
trace.WriteString(f.Function)
|
|
trace.WriteString("\n\t")
|
|
trace.WriteString(f.File)
|
|
trace.WriteString(":")
|
|
trace.WriteString(strconv.Itoa(f.Line))
|
|
trace.WriteString("\n")
|
|
}
|
|
return trace.String()
|
|
}
|
|
|
|
// Async Reporter Leak Checking
|
|
|
|
var asyncReporterTracker *reporterTracker
|
|
|
|
type reporterTracker struct {
|
|
mu sync.Mutex
|
|
allocations map[*int][]uintptr
|
|
}
|
|
|
|
func newReporterTracker() *reporterTracker {
|
|
return &reporterTracker{
|
|
allocations: make(map[*int][]uintptr),
|
|
}
|
|
}
|
|
|
|
// register records the stack trace.
|
|
func (rt *reporterTracker) register() *int {
|
|
rt.mu.Lock()
|
|
defer rt.mu.Unlock()
|
|
|
|
id := new(int)
|
|
// Skip 4 frames: register -> internal.Delegate -> stats.RegisterAsyncReporter -> Caller
|
|
rt.allocations[id] = currentStack(4)
|
|
return id
|
|
}
|
|
|
|
// unregister removes the ID.
|
|
func (rt *reporterTracker) unregister(id *int) {
|
|
rt.mu.Lock()
|
|
defer rt.mu.Unlock()
|
|
delete(rt.allocations, id)
|
|
}
|
|
|
|
// leakedStackTraces returns formatted stack traces for all currently registered
|
|
// reporters.
|
|
func (rt *reporterTracker) leakedStackTraces() []string {
|
|
rt.mu.Lock()
|
|
defer rt.mu.Unlock()
|
|
|
|
var traces []string
|
|
for _, pcs := range rt.allocations {
|
|
msg := "\n--- Leaked Async Reporter Registration ---\n" + traceToString(pcs)
|
|
traces = append(traces, msg)
|
|
}
|
|
return traces
|
|
}
|
|
|
|
// TrackAsyncReporters installs the tracking delegate.
|
|
func TrackAsyncReporters() {
|
|
asyncReporterTracker = newReporterTracker()
|
|
|
|
// Swap the delegate: Replace the default pass-through with tracking logic.
|
|
internal.AsyncReporterCleanupDelegate = func(originalCleanup func()) func() {
|
|
// 1. Capture Stack Trace (happens during Registration)
|
|
token := asyncReporterTracker.register()
|
|
|
|
// 2. Return Wrapped Cleanup
|
|
return func() {
|
|
// Defer unregister to ensure we stop tracking even if the original cleanup panics.
|
|
defer asyncReporterTracker.unregister(token)
|
|
|
|
if originalCleanup != nil {
|
|
originalCleanup()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// CheckAsyncReporters verifies that no leaks exist and restores the default delegate.
|
|
func CheckAsyncReporters(logger Logger) {
|
|
// Restore the delegate: Reset to the default pass-through behavior.
|
|
internal.AsyncReporterCleanupDelegate = func(cleanup func()) func() {
|
|
return cleanup
|
|
}
|
|
|
|
if asyncReporterTracker == nil {
|
|
return
|
|
}
|
|
|
|
leaks := asyncReporterTracker.leakedStackTraces()
|
|
if len(leaks) > 0 {
|
|
// Join all stack traces into one message
|
|
allTraces := ""
|
|
for _, trace := range leaks {
|
|
allTraces += trace
|
|
}
|
|
logger.Errorf("Found %d leaked async reporters:%s", len(leaks), allTraces)
|
|
}
|
|
|
|
// Clean up global state
|
|
asyncReporterTracker = nil
|
|
}
|