Skip to content

Commit

Permalink
Make maxSleep and maxRetires configurable when building options (#94)
Browse files Browse the repository at this point in the history
Make goleak options more flexible to adapt to users' variety scenarios

Signed-off-by: Kante Yin <kerthcet@gmail.com>

fix  #93

---------

Signed-off-by: Kante Yin <kerthcet@gmail.com>
Co-authored-by: Sung Yoon Whang <sungyoonwhang@gmail.com>
Co-authored-by: Abhinav Gupta <mail@abhinavg.net>
  • Loading branch information
3 people committed Feb 13, 2023
1 parent 21e4cb1 commit 751da59
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 11 deletions.
3 changes: 3 additions & 0 deletions leaks.go
Expand Up @@ -56,6 +56,9 @@ func Find(options ...Option) error {
cur := stack.Current().ID()

opts := buildOpts(options...)
if err := opts.validate(); err != nil {
return err
}
if opts.cleanup != nil {
return errors.New("Cleanup can only be passed to VerifyNone or VerifyTestMain")
}
Expand Down
12 changes: 11 additions & 1 deletion leaks_test.go
Expand Up @@ -36,7 +36,7 @@ var _ = TestingT(testing.TB(nil))
// testOptions passes a shorter max sleep time, used so tests don't wait
// ~1 second in cases where we expect Find to error out.
func testOptions() Option {
return maxSleep(time.Millisecond)
return MaxSleepInterval(time.Millisecond)
}

func TestFind(t *testing.T) {
Expand All @@ -60,6 +60,16 @@ func TestFind(t *testing.T) {
err := Find(Cleanup(func(int) { assert.Fail(t, "this should not be called") }))
require.Error(t, err, "Should exit with invalid option")
})

t.Run("Find should return error when maxRetries is less than 0", func(t *testing.T) {
err := Find(MaxRetryAttempts(-1))
require.Error(t, err, "maxRetries should be greater than 0")
})

t.Run("Find should return error when maxSleep is less than 0s", func(t *testing.T) {
err := Find(MaxSleepInterval(time.Duration(-1)))
require.Error(t, err, "maxSleep should be greater than 0s")
})
}

func TestFindRetry(t *testing.T) {
Expand Down
44 changes: 37 additions & 7 deletions options.go
Expand Up @@ -21,6 +21,7 @@
package goleak

import (
"errors"
"strings"
"time"

Expand All @@ -32,10 +33,14 @@ type Option interface {
apply(*opts)
}

// We retry up to 20 times if we can't find the goroutine that
// we are looking for. In between each attempt, we will sleep for
// a short while to let any running goroutines complete.
const _defaultRetries = 20
const (
// We retry up to default 20 times if we can't find the goroutine that
// we are looking for.
_defaultRetryAttempts = 20
// In between each retry attempt, sleep for up to default 100 microseconds
// to let any running goroutine completes.
_defaultSleepInterval = 100 * time.Microsecond
)

type opts struct {
filters []func(stack.Stack) bool
Expand All @@ -53,6 +58,17 @@ func (o *opts) apply(opts *opts) {
opts.cleanup = o.cleanup
}

// validate the options.
func (o *opts) validate() error {
if o.maxRetries < 0 {
return errors.New("maxRetryAttempts should be greater than 0")
}
if o.maxSleep <= 0 {
return errors.New("maxSleepInterval should be greater than 0s")
}
return nil
}

// optionFunc lets us easily write options without a custom type.
type optionFunc func(*opts)

Expand Down Expand Up @@ -91,12 +107,25 @@ func IgnoreCurrent() Option {
})
}

func maxSleep(d time.Duration) Option {
// MaxSleepInterval sets the maximum sleep time in-between each retry attempt.
// The sleep duration grows in an exponential backoff, to a maximum of the value specified here.
// If not configured, default to 100 microseconds.
func MaxSleepInterval(d time.Duration) Option {
return optionFunc(func(opts *opts) {
opts.maxSleep = d
})
}

// MaxRetryAttempts sets the retry upper limit.
// When finding extra goroutines, we'll retry until all goroutines complete
// or end up with the maximum retry attempts.
// If not configured, default to 20 times.
func MaxRetryAttempts(num int) Option {
return optionFunc(func(opts *opts) {
opts.maxRetries = num
})
}

func addFilter(f func(stack.Stack) bool) Option {
return optionFunc(func(opts *opts) {
opts.filters = append(opts.filters, f)
Expand All @@ -105,8 +134,8 @@ func addFilter(f func(stack.Stack) bool) Option {

func buildOpts(options ...Option) *opts {
opts := &opts{
maxRetries: _defaultRetries,
maxSleep: 100 * time.Millisecond,
maxRetries: _defaultRetryAttempts,
maxSleep: _defaultSleepInterval,
}
opts.filters = append(opts.filters,
isTestStack,
Expand All @@ -117,6 +146,7 @@ func buildOpts(options ...Option) *opts {
for _, option := range options {
option.apply(opts)
}

return opts
}

Expand Down
16 changes: 13 additions & 3 deletions options_test.go
Expand Up @@ -65,10 +65,20 @@ func TestOptionsFilters(t *testing.T) {
require.Zero(t, countUnfiltered(), "blockedG should be filtered out. running: %v", stack.All())
}

func TestOptionsRetry(t *testing.T) {
func TestBuildOptions(t *testing.T) {
// With default options.
opts := buildOpts()
opts.maxRetries = 50 // initial attempt + 50 retries = 11
opts.maxSleep = time.Millisecond
assert.Equal(t, _defaultSleepInterval, opts.maxSleep, "value of maxSleep not right")
assert.Equal(t, _defaultRetryAttempts, opts.maxRetries, "value of maxRetries not right")

// With customized options.
opts = buildOpts(MaxRetryAttempts(50), MaxSleepInterval(time.Microsecond))
assert.Equal(t, time.Microsecond, opts.maxSleep, "value of maxSleep not right")
assert.Equal(t, 50, opts.maxRetries, "value of maxRetries not right")
}

func TestOptionsRetry(t *testing.T) {
opts := buildOpts(MaxSleepInterval(time.Millisecond), MaxRetryAttempts(50)) // initial attempt + 50 retries = 51

for i := 0; i < 50; i++ {
assert.True(t, opts.retry(i), "Attempt %v/51 should allow retrying", i)
Expand Down

0 comments on commit 751da59

Please sign in to comment.