Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion leaks_test.go
Original file line number Diff line number Diff line change
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 MaxSleep(time.Millisecond)
}

func TestFind(t *testing.T) {
Expand Down Expand Up @@ -74,6 +74,41 @@ func TestFindRetry(t *testing.T) {
require.NoError(t, Find(), "Find should retry while background goroutine ends")
}

func TestFindMaxRetries(t *testing.T) {
t.Run("zero retries does not retry and fails immediately", func(t *testing.T) {
bg := startBlockedG()
defer bg.unblock()

start := time.Now()
err := Find(MaxRetries(0))
require.Error(t, err, "Should find leaks without retrying")
assert.Less(t, time.Since(start), 50*time.Millisecond, "Should return immediately with 0 retries")
})

t.Run("custom retries allows delayed goroutine exit", func(t *testing.T) {
bg := startBlockedG()
go func() {
time.Sleep(5 * time.Millisecond)
bg.unblock()
}()

err := Find(MaxRetries(20), MaxSleep(5*time.Millisecond))
require.NoError(t, err, "Find should retry and succeed when goroutine finishes")
})
}

func TestFindMaxSleep(t *testing.T) {
t.Run("shorter max sleep fails faster", func(t *testing.T) {
bg := startBlockedG()
defer bg.unblock()

start := time.Now()
err := Find(MaxRetries(5), MaxSleep(time.Millisecond))
require.Error(t, err)
assert.Less(t, time.Since(start), 100*time.Millisecond, "Find should return quickly with short max sleep")
})
}

type fakeT struct {
errors []string
}
Expand Down Expand Up @@ -103,6 +138,15 @@ func TestVerifyNone(t *testing.T) {
}))
require.True(t, cleanupCalled, "expect cleanup registered callback to be called")
})

t.Run("VerifyNone with MaxRetries and MaxSleep", func(t *testing.T) {
ft := &fakeT{}
bg := startBlockedG()
defer bg.unblock()

VerifyNone(ft, MaxRetries(1), MaxSleep(time.Millisecond))
require.NotEmpty(t, ft.errors, "Expect errors from VerifyNone with custom MaxRetries and MaxSleep")
})
}

func TestIgnoreCurrent(t *testing.T) {
Expand Down
28 changes: 24 additions & 4 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,29 @@ func RunOnFailure() Option {
})
}

func maxSleep(d time.Duration) Option {
// MaxRetries sets the maximum retry attempts when looking for extra goroutines.
// When looking for leaking goroutines, goleak will retry until all goroutines
// terminate or until the maximum retry attempts are exhausted.
// If not configured, defaults to 20.
func MaxRetries(n int) Option {
return optionFunc(func(opts *opts) {
opts.maxRetries = n
})
}

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

func maxSleep(d time.Duration) Option {
return MaxSleep(d)
}

func addFilter(f func(stack.Stack) bool) Option {
return optionFunc(func(opts *opts) {
opts.filters = append(opts.filters, f)
Expand Down Expand Up @@ -170,9 +187,12 @@ func (o *opts) retry(i int) bool {
return false
}

d := time.Duration(int(time.Microsecond) << uint(i))
if d > o.maxSleep {
d = o.maxSleep
d := o.maxSleep
if i < 40 {
d = time.Duration(int(time.Microsecond) << uint(i))
if d > o.maxSleep {
d = o.maxSleep
}
}
time.Sleep(d)
return true
Expand Down
27 changes: 21 additions & 6 deletions options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,29 @@ func TestOptionsIgnoreAnyFunction(t *testing.T) {
}
}

func TestOptionsRetry(t *testing.T) {
func TestMaxRetriesAndMaxSleep(t *testing.T) {
opts := buildOpts()
opts.maxRetries = 50 // initial attempt + 50 retries = 11
opts.maxSleep = time.Millisecond
assert.Equal(t, _defaultRetries, opts.maxRetries)
assert.Equal(t, 100*time.Millisecond, opts.maxSleep)

opts = buildOpts(MaxRetries(50), MaxSleep(time.Millisecond))
assert.Equal(t, 50, opts.maxRetries)
assert.Equal(t, time.Millisecond, opts.maxSleep)
}

func TestOptionsRetry(t *testing.T) {
opts := buildOpts(MaxRetries(50), MaxSleep(time.Millisecond))

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

func TestOptionsRetryLargeAttempt(t *testing.T) {
opts := buildOpts(MaxRetries(100), MaxSleep(time.Millisecond))
assert.True(t, opts.retry(65), "Attempt 65 should not overflow")
assert.False(t, opts.retry(100), "Attempt 100 should not allow retrying")
}