diff --git a/README.md b/README.md index df1d2863..21288132 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/retry.go b/retry.go index 0303f84a..60d1647c 100644 --- a/retry.go +++ b/retry.go @@ -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 ? diff --git a/retry_test.go b/retry_test.go index b4d5d8dc..015ec791 100644 --- a/retry_test.go +++ b/retry_test.go @@ -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()