Skip to content

Commit

Permalink
feat: adding AttemptWhile and AttemptWhileWithDelay (#183)
Browse files Browse the repository at this point in the history
* feat: adding HaltingAttempt and HaltingAttemptWithDelay

* chore: re-implement it as AttemptWhile and AttemptWhileWithDelay
  • Loading branch information
nekomeowww committed Nov 28, 2022
1 parent 56a3ac6 commit 23ae73b
Show file tree
Hide file tree
Showing 3 changed files with 272 additions and 0 deletions.
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

0 comments on commit 23ae73b

Please sign in to comment.