gleak

package
v1.28.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 23, 2023 License: MIT Imports: 12 Imported by: 1

Documentation

Overview

package gleak complements the Gingko/Gomega testing and matchers framework with matchers for Goroutine leakage detection.

Basics of nleak

To start with,

Goroutines()

returns information about all (non-dead) goroutines at a particular moment. This is useful to capture a known correct snapshot and then later taking a new snapshot and comparing these two snapshots for leaked goroutines.

Next, the matcher

HaveLeaked(...)

filters out well-known and expected "non-leaky" goroutines from an actual list of goroutines (passed from Eventually or Expect), hopefully ending up with an empty list of leaked goroutines. If there are still goroutines left after filtering, then HaveLeaked() will succeed ... which usually is actually considered to be failure. So, this can be rather declared to be "suckcess" because no one wants leaked goroutines.

A typical pattern to detect goroutines leaked in individual tests is as follows:

var ignoreGood []Goroutine

BeforeEach(func() {
    ignoreGood = Goroutines()
})

AfterEach(func() {
    // Note: it's "Goroutines", but not "Goroutines()", when using with Eventually!
    Eventually(Goroutines).ShouldNot(HaveLeaked(ignoreGood))
})

Using Eventually instead of Expect ensures that there is some time given for temporary goroutines to finally wind down. Gomega's default values apply: the 1s timeout and 10ms polling interval.

Please note that the form

HaveLeaked(ignoreGood)

is the same as the slightly longer, but also more expressive variant:

HaveLeaked(IgnoringGoroutines(ignoreGood))

Depending on your tests and the dependencies used, you might need to identify additional goroutines as not being leaks. The gleak packages comes with the following predefined goroutine "filter" matchers that can be specified as arguments to HaveLeaked(...):

IgnoringTopFunction("foo.bar")                // exactly "foo.bar"
IgnoringTopFunction("foo.bar...")             // top function name with prefix "foo.bar." (note the trailing dot!)
IgnoringTopFunction("foo.bar [chan receive]") // exactly "foo.bar" with state starting with "chan receive"
IgnoringGoroutines(expectedGoroutines)        // ignore specified goroutines with these IDs
IgnoringInBacktrace("foo.bar.baz")            // "foo.bar.baz" within the backtrace
IgnoringCreator("foo.bar")                    // exact creator function name "foo.bar"
IgnoringCreator("foo.bar...")                 // creator function name with prefix "foo.bar."

In addition, you can use any other GomegaMatcher, as long as it can work on a (single) Goroutine. For instance, Gomega's HaveField and WithTransform matchers are good foundations for writing project-specific gleak matchers.

Leaked Goroutine Dump

By default, when gleak's HaveLeaked matcher finds one or more leaked goroutines, it dumps the goroutine backtraces in a condensed format that uses only a single line per call instead of two lines. Moreover, the backtraces abbreviate the source file location in the form of package/source.go:lineno:

goroutine 42 [flabbergasted]
    main.foo.func1() at foo/test.go:6
    created by main.foo at foo/test.go:5

By setting gleak.ReportFilenameWithPath=true the leaky goroutine backtraces will show full path names for each source file:

goroutine 42 [flabbergasted]
    main.foo.func1() at /home/go/foo/test.go:6
    created by main.foo at home/go/foo/test.go:5

Acknowledgement

gleak has been heavily inspired by the Goroutine leak detector github.com/uber-go/goleak. That's definitely a fine piece of work!

But then why another goroutine leak package? After a deep analysis of Uber's goleak we decided against crunching goleak somehow half-assed into the Gomega TDD matcher ecosystem. In particular, reusing and wrapping of the existing Uber implementation would have become very awkward: goleak.Find combines all the different elements of getting actual goroutines information, filtering them, arriving at a leak conclusion, and even retrying multiple times all in just one single exported function. Unfortunately, goleak makes gathering information about all goroutines an internal matter, so we cannot reuse such functionality elsewhere.

Users of the Gomega ecosystem are already experienced in arriving at conclusions and retrying temporarily failing expectations: Gomega does it in form of Eventually().ShouldNot(), and (without the trying aspect) with Expect().NotTo(). So what is missing is only a goroutine leak detector in form of the HaveLeaked matcher, as well as the ability to specify goroutine filters in order to sort out the non-leaking (and therefore expected) goroutines, using a few filter criteria. That is, a few new goroutine-related matchers. In this architecture, even existing Gomega matchers can optionally be (re)used as the need arises.

References

https://github.com/onsi/gomega and https://github.com/onsi/ginkgo.

Index

Constants

This section is empty.

Variables

View Source
var ReportFilenameWithPath = false

ReportFilenameWithPath controls whether to show call locations in leak reports by default in abbreviated form with only source code filename with package name and line number, or alternatively with source code filename with path and line number.

That is, with ReportFilenameWithPath==false:

foo/bar.go:123

Or with ReportFilenameWithPath==true:

/home/goworld/coolprojects/mymodule/foo/bar.go:123

Functions

func HaveLeaked

func HaveLeaked(ignoring ...interface{}) types.GomegaMatcher

HaveLeaked succeeds (or rather, "suckceeds" considering it appears in failing tests) if after filtering out ("ignoring") the expected goroutines from the list of actual goroutines the remaining list of goroutines is non-empty. These goroutines not filtered out are considered to have been leaked.

For convenience, HaveLeaked automatically filters out well-known runtime and testing goroutines using a built-in standard filter matchers list. In addition to the built-in filters, HaveLeaked accepts an optional list of non-leaky goroutine filter matchers. These filtering matchers can be specified in different formats, as described below.

Since there might be "pending" goroutines at the end of tests that eventually will properly wind down so they aren't leaking, HaveLeaked is best paired with Eventually instead of Expect. In its shortest form this will use Eventually's default timeout and polling interval settings, but these can be overridden as usual:

// Remember to use "Goroutines" and not "Goroutines()" with Eventually()!
Eventually(Goroutines).ShouldNot(HaveLeaked())
Eventually(Goroutines).WithTimeout(5 * time.Second).ShouldNot(HaveLeaked())

In its simplest form, an expected non-leaky goroutine can be identified by passing the (fully qualified) name (in form of a string) of the topmost function in the backtrace. For instance:

Eventually(Goroutines).ShouldNot(HaveLeaked("foo.bar"))

This is the shorthand equivalent to this explicit form:

Eventually(Goroutines).ShouldNot(HaveLeaked(IgnoringTopFunction("foo.bar")))

HaveLeak also accepts passing a slice of Goroutine objects to be considered non-leaky goroutines.

snapshot := Goroutines()
DoSomething()
Eventually(Goroutines).ShouldNot(HaveLeaked(snapshot))

Again, this is shorthand for the following explicit form:

snapshot := Goroutines()
DoSomething()
Eventually(Goroutines).ShouldNot(HaveLeaked(IgnoringGoroutines(snapshot)))

Finally, HaveLeaked accepts any GomegaMatcher and will repeatedly pass it a Goroutine object: if the matcher succeeds, the Goroutine object in question is considered to be non-leaked and thus filtered out. While the following built-in Goroutine filter matchers should hopefully cover most situations, any suitable GomegaMatcher can be used for tricky leaky Goroutine filtering.

IgnoringTopFunction("foo.bar")
IgnoringTopFunction("foo.bar...")
IgnoringTopFunction("foo.bar [chan receive]")
IgnoringGoroutines(expectedGoroutines)
IgnoringInBacktrace("foo.bar.baz")

func IgnoreGinkgoParallelClient added in v1.20.1

func IgnoreGinkgoParallelClient()

IgnoreGinkgoParallelClient must be called in a BeforeSuite whenever a test suite is run in parallel with other test suites using "ginkgo -p". Calling IgnoreGinkgoParallelClient checks for a Ginkgo-related background go routine and then updates gleak's internal ignore list to specifically ignore this background go routine by its ("random") ID.

func IgnoringCreator

func IgnoringCreator(creatorfname string) types.GomegaMatcher

IgnoringCreator succeeds if the goroutine was created by a function matching the specified name. The expected creator function name is either in the form of "creatorfunction-name" or "creatorfunction-name...".

An ellipsis "..." after a creatorfunction-name matches any creator function name if creatorfunction-name is a prefix and the goroutine's creator function name is at least one level deeper. For instance, "foo.bar..." matches "foo.bar.baz", but doesn't match "foo.bar".

func IgnoringGoroutines

func IgnoringGoroutines(goroutines []Goroutine) types.GomegaMatcher

IgnoringGoroutines succeeds if an actual goroutine, identified by its ID, is in a slice of expected goroutines. A typical use of the IgnoringGoroutines matcher is to take a snapshot of the current goroutines just right before a test and then at the end of a test filtering out these "good" and known goroutines.

func IgnoringInBacktrace

func IgnoringInBacktrace(fname string) types.GomegaMatcher

IgnoringInBacktrace succeeds if a function name is contained in the backtrace of the actual goroutine description.

func IgnoringTopFunction

func IgnoringTopFunction(topfname string) types.GomegaMatcher

IgnoringTopFunction succeeds if the topmost function in the backtrace of an actual goroutine has the specified function name, and optionally the actual goroutine has the specified goroutine state.

The expected top function name topfn is either in the form of "topfunction-name", "topfunction-name...", or "topfunction-name [state]".

An ellipsis "..." after a topfunction-name matches any goroutine's top function name if topfunction-name is a prefix and the goroutine's top function name is at least one level deeper. For instance, "foo.bar..." matches "foo.bar.baz", but doesn't match "foo.bar".

If the optional expected state is specified, then a goroutine's state needs to start with this expected state text. For instance, "foo.bar [running]" matches a goroutine where the name of the top function is "foo.bar" and the goroutine's state starts with "running".

Types

type Goroutine

type Goroutine = goroutine.Goroutine

Goroutine represents information about a single goroutine and is a convenience type alias.

func G

func G(actual interface{}, matchername string) (Goroutine, error)

G takes an actual "any" untyped value and returns it as a typed Goroutine, if possible. It returns an error if actual isn't of either type Goroutine or a pointer to it. G is intended to be mainly used by goroutine-related Gomega matchers, such as IgnoringTopFunction, et cetera.

func Goroutines

func Goroutines() []Goroutine

Goroutines returns information about all goroutines: their goroutine IDs, the names of the topmost functions in the backtraces, and finally the goroutine backtraces.

type HaveLeakedMatcher

type HaveLeakedMatcher struct {
	// contains filtered or unexported fields
}

HaveLeakedMatcher implements the HaveLeaked Gomega Matcher that succeeds if the actual list of goroutines is non-empty after filtering out the expected goroutines.

func (*HaveLeakedMatcher) FailureMessage

func (matcher *HaveLeakedMatcher) FailureMessage(actual interface{}) (message string)

FailureMessage returns a failure message if there are leaked goroutines.

func (*HaveLeakedMatcher) Match

func (matcher *HaveLeakedMatcher) Match(actual interface{}) (success bool, err error)

Match succeeds if actual is an array or slice of Goroutine information and still contains goroutines after filtering out all expected goroutines that were specified when creating the matcher.

func (*HaveLeakedMatcher) NegatedFailureMessage

func (matcher *HaveLeakedMatcher) NegatedFailureMessage(actual interface{}) (message string)

NegatedFailureMessage returns a negated failure message if there aren't any leaked goroutines.

type Uint64Slice

type Uint64Slice []uint64

Uint64Slice implements the sort.Interface for a []uint64 to sort in increasing order.

func (Uint64Slice) Len

func (s Uint64Slice) Len() int

func (Uint64Slice) Less

func (s Uint64Slice) Less(a, b int) bool

func (Uint64Slice) Swap

func (s Uint64Slice) Swap(a, b int)

Directories

Path Synopsis
Package goroutine discovers and returns information about either all goroutines or only the caller's goroutine.
Package goroutine discovers and returns information about either all goroutines or only the caller's goroutine.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL