Skip to content

Commit

Permalink
ethclient/simulated: add test to check if simulated backend leaks gor…
Browse files Browse the repository at this point in the history
…outines
  • Loading branch information
jwasinger committed Jan 20, 2025
1 parent 17199da commit c50e6ba
Showing 1 changed file with 84 additions and 0 deletions.
84 changes: 84 additions & 0 deletions ethclient/simulated/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ import (
"context"
"crypto/ecdsa"
"crypto/sha256"
"fmt"
"math/big"
"math/rand"
"regexp"
"runtime"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -350,3 +354,83 @@ func TestAdjustTimeAfterFork(t *testing.T) {
t.Errorf("failed to build block on fork")
}
}

func createAndCloseSimBackend() {
genesisData := types.GenesisAlloc{}
simulatedBackend := NewBackend(genesisData)
defer simulatedBackend.Close()
}

// sanitizeStackTrace removes any content inside parentheses (including the parentheses) from the input. it also
// removes occurances of the specific go-routine id.
func sanitizeStackTrace(input string) string {
re := regexp.MustCompile(`\(.*\)`)
sanitized := re.ReplaceAllString(input, "")
re2 := regexp.MustCompile(`goroutine [0-9*]*`)
sanitized = re2.ReplaceAllString(sanitized, "goroutine xxx")
return sanitized
}

// collectGoroutineStacks collects the stack traces of all currently-running go-routines, stripping information specific
// to the current invocation (the parameter values in each function call, the specific go-routine id), and returning
// the stack traces as a map of stack trace text to the number of occurances of that stack-trace.
func collectGoroutineStacks() map[string]int {
buf := make([]byte, 1<<20)
stackLen := runtime.Stack(buf, true)

// split all stack-traces by go-routine
stacks := strings.Split(string(buf[:stackLen]), "\n\n")

res := make(map[string]int)

for i := range stacks {
lines := strings.Split(stacks[i], "\n")
combinedLines := strings.Join(lines[1:], "\n")

// filter out the callstack for this goroutine
if strings.Contains(combinedLines, "collectGoroutineStacks") {
continue
}

// remove func call offset and variable values.
combinedLines = sanitizeStackTrace(combinedLines)

res[combinedLines] = res[combinedLines] + 1
}

return res
}

// leaks takes two sets of stack traces (returned as output from collectGoroutineStacks). If any stack traces exist
// after the second invocation which are not present in the first, they are considered "leaked" and returned in the
// output.
func leaks(firstGRs map[string]int, secondGRs map[string]int) []string {
var res []string
for key, _ := range secondGRs {
if _, ok := firstGRs[key]; !ok {
res = append(res, key)
} else if secondGRs[key] > firstGRs[key] {
res = append(res, key)
}
}
return res
}

// TestCheckSimBackendGoroutineLeak checks whether creation of a simulated backend leaks go-routines. Any long-lived go-routines
// spawned by global variables are not considered leaked.
func TestCheckSimBackendGoroutineLeak(t *testing.T) {
createAndCloseSimBackend()
stacks1 := collectGoroutineStacks()
createAndCloseSimBackend()
stacks2 := collectGoroutineStacks()

l := leaks(stacks1, stacks2)
if len(l) > 0 {
var leakedGRs string
for _, leak := range l {
leakedGRs = leakedGRs + fmt.Sprintf("%s\n\n", leak)
}

t.Fatalf("leaked goroutines:\n%s", leakedGRs)
}
}

0 comments on commit c50e6ba

Please sign in to comment.