Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding AttemptWhile and AttemptWhileWithDelay #183

Merged
merged 2 commits into from Nov 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 50 additions & 0 deletions README.md
Expand Up @@ -2335,6 +2335,56 @@ For more advanced retry strategies (delay, exponential backoff...), please take

[[play](https://go.dev/play/p/tVs6CygC7m1)]

### AttemptWhile

Invokes a function N times until it returns valid output. Returning either the caught error or nil, and along with a bool value to identifying whether it needs invoke function continuously. It will terminate the invoke immediately if second bool value is returned with falsy value.

When first argument is less than `1`, the function runs until a successful response is returned.

```go
count1, err1 := AttemptWhile(5, func(i int) (error, bool) {
err := doMockedHTTPRequest(i)
if err != nil {
if errors.Is(err, ErrBadRequest) { // lets assume ErrBadRequest is a critical error that needs to terminate the invoke
return err, false // flag the second return value as false to terminate the invoke
}

return err, true
}

return nil, false
})
```

For more advanced retry strategies (delay, exponential backoff...), please take a look on [cenkalti/backoff](https://github.com/cenkalti/backoff).

[play](https://go.dev/play/p/M2wVq24PaZM)

### AttemptWhileWithDelay

Invokes a function N times until it returns valid output, with a pause between each call. Returning either the caught error or nil, and along with a bool value to identifying whether it needs to invoke function continuously. It will terminate the invoke immediately if second bool value is returned with falsy value.

When first argument is less than `1`, the function runs until a successful response is returned.

```go
count1, time1, err1 := AttemptWhileWithDelay(5, time.Millisecond, func(i int, d time.Duration) (error, bool) {
err := doMockedHTTPRequest(i)
if err != nil {
if errors.Is(err, ErrBadRequest) { // lets assume ErrBadRequest is a critical error that needs to terminate the invoke
return err, false // flag the second return value as false to terminate the invoke
}

return err, true
}

return nil, false
})
```

For more advanced retry strategies (delay, exponential backoff...), please take a look on [cenkalti/backoff](https://github.com/cenkalti/backoff).

[play](https://go.dev/play/p/LPsWgf1ilBO)

### Debounce

`NewDebounce` creates a debounced instance that delays invoking functions given until after wait milliseconds have elapsed, until `cancel` is called.
Expand Down
53 changes: 53 additions & 0 deletions retry.go
Expand Up @@ -101,4 +101,57 @@ func AttemptWithDelay(maxIteration int, delay time.Duration, f func(index int, d
return maxIteration, time.Since(start), err
}

// AttemptWhile invokes a function N times until it returns valid output.
// Returning either the caught error or nil, and along with a bool value to identify
// whether it needs invoke function continuously. It will terminate the invoke
// immediately if second bool value is returned with falsy value. When first
// argument is less than `1`, the function runs until a successful response is
// returned.
func AttemptWhile(maxIteration int, f func(int) (error, bool)) (int, error) {
var err error
var shouldContinueInvoke bool

for i := 0; maxIteration <= 0 || i < maxIteration; i++ {
// for retries >= 0 {
err, shouldContinueInvoke = f(i)
if !shouldContinueInvoke { // if shouldContinueInvoke is false, then return immediately
return i + 1, err
}
if err == nil {
return i + 1, nil
}
}

return maxIteration, err
}

// AttemptWhileWithDelay invokes a function N times until it returns valid output,
// with a pause between each call. Returning either the caught error or nil, and along
// with a bool value to identify whether it needs to invoke function continuously.
// It will terminate the invoke immediately if second bool value is returned with falsy
// value. When first argument is less than `1`, the function runs until a successful
// response is returned.
func AttemptWhileWithDelay(maxIteration int, delay time.Duration, f func(int, time.Duration) (error, bool)) (int, time.Duration, error) {
var err error
var shouldContinueInvoke bool

start := time.Now()

for i := 0; maxIteration <= 0 || i < maxIteration; i++ {
err, shouldContinueInvoke = f(i, time.Since(start))
if !shouldContinueInvoke { // if shouldContinueInvoke is false, then return immediately
return i + 1, time.Since(start), err
}
if err == nil {
return i + 1, time.Since(start), nil
}

if maxIteration <= 0 || i+1 < maxIteration {
time.Sleep(delay)
}
}

return maxIteration, time.Since(start), err
}

// throttle ?
169 changes: 169 additions & 0 deletions retry_test.go
Expand Up @@ -98,6 +98,175 @@ func TestAttemptWithDelay(t *testing.T) {
is.Equal(err4, nil)
}

func TestAttemptWhile(t *testing.T) {
is := assert.New(t)

err := fmt.Errorf("failed")

iter1, err1 := AttemptWhile(42, func(i int) (error, bool) {
return nil, true
})

is.Equal(iter1, 1)
is.Nil(err1)

iter2, err2 := AttemptWhile(42, func(i int) (error, bool) {
if i == 5 {
return nil, true
}

return err, true
})

is.Equal(iter2, 6)
is.Nil(err2)

iter3, err3 := AttemptWhile(2, func(i int) (error, bool) {
if i == 5 {
return nil, true
}

return err, true
})

is.Equal(iter3, 2)
is.Equal(err3, err)

iter4, err4 := AttemptWhile(0, func(i int) (error, bool) {
if i < 42 {
return err, true
}

return nil, true
})

is.Equal(iter4, 43)
is.Nil(err4)

iter5, err5 := AttemptWhile(0, func(i int) (error, bool) {
if i == 5 {
return nil, false
}

return err, true
})

is.Equal(iter5, 6)
is.Nil(err5)

iter6, err6 := AttemptWhile(0, func(i int) (error, bool) {
return nil, false
})

is.Equal(iter6, 1)
is.Nil(err6)

iter7, err7 := AttemptWhile(42, func(i int) (error, bool) {
if i == 42 {
return nil, false
}
if i < 41 {
return err, true
}

return nil, true
})

is.Equal(iter7, 42)
is.Nil(err7)
}

func TestAttemptWhileWithDelay(t *testing.T) {
is := assert.New(t)

err := fmt.Errorf("failed")

iter1, dur1, err1 := AttemptWhileWithDelay(42, 10*time.Millisecond, func(i int, d time.Duration) (error, bool) {
return nil, true
})

is.Equal(iter1, 1)
is.Greater(dur1, 0*time.Millisecond)
is.Less(dur1, 1*time.Millisecond)
is.Nil(err1)

iter2, dur2, err2 := AttemptWhileWithDelay(42, 10*time.Millisecond, func(i int, d time.Duration) (error, bool) {
if i == 5 {
return nil, true
}

return err, true
})

is.Equal(iter2, 6)
is.Greater(dur2, 50*time.Millisecond)
is.Less(dur2, 60*time.Millisecond)
is.Nil(err2)

iter3, dur3, err3 := AttemptWhileWithDelay(2, 10*time.Millisecond, func(i int, d time.Duration) (error, bool) {
if i == 5 {
return nil, true
}

return err, true
})

is.Equal(iter3, 2)
is.Greater(dur3, 10*time.Millisecond)
is.Less(dur3, 20*time.Millisecond)
is.Equal(err3, err)

iter4, dur4, err4 := AttemptWhileWithDelay(0, 10*time.Millisecond, func(i int, d time.Duration) (error, bool) {
if i < 10 {
return err, true
}

return nil, true
})

is.Equal(iter4, 11)
is.Greater(dur4, 100*time.Millisecond)
is.Less(dur4, 115*time.Millisecond)
is.Nil(err4)

iter5, dur5, err5 := AttemptWhileWithDelay(0, 10*time.Millisecond, func(i int, d time.Duration) (error, bool) {
if i == 5 {
return nil, false
}

return err, true
})

is.Equal(iter5, 6)
is.Greater(dur5, 10*time.Millisecond)
is.Less(dur5, 115*time.Millisecond)
is.Nil(err5)

iter6, dur6, err6 := AttemptWhileWithDelay(0, 10*time.Millisecond, func(i int, d time.Duration) (error, bool) {
return nil, false
})

is.Equal(iter6, 1)
is.Less(dur6, 10*time.Millisecond)
is.Less(dur6, 115*time.Millisecond)
is.Nil(err6)

iter7, dur7, err7 := AttemptWhileWithDelay(42, 10*time.Millisecond, func(i int, d time.Duration) (error, bool) {
if i == 42 {
return nil, false
}
if i < 41 {
return err, true
}

return nil, true
})

is.Equal(iter7, 42)
is.Less(dur7, 500*time.Millisecond)
is.Nil(err7)
}

func TestDebounce(t *testing.T) {
t.Parallel()

Expand Down