From 1e7cc54b1ec546a50f6a687dc60f0932c95546d8 Mon Sep 17 00:00:00 2001 From: Ayaka Neko Date: Fri, 15 Jul 2022 23:07:13 +0800 Subject: [PATCH 1/2] feat: adding HaltingAttempt and HaltingAttemptWithDelay --- README.md | 1 - retry.go | 51 ++++++++++++++ retry_test.go | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 234 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index df1d2863..99428830 100644 --- a/README.md +++ b/README.md @@ -2315,7 +2315,6 @@ For more advanced retry strategies (delay, exponential backoff...), please take ### AttemptWithDelay Invokes a function N times until it returns valid output, with a pause between each call. Returning either the caught error or nil. - When first argument is less than `1`, the function runs until a successful response is returned. ```go diff --git a/retry.go b/retry.go index 0303f84a..e4a75920 100644 --- a/retry.go +++ b/retry.go @@ -101,4 +101,55 @@ func AttemptWithDelay(maxIteration int, delay time.Duration, f func(index int, d return maxIteration, time.Since(start), err } +// HaltingAttempt invokes a function N times until it returns valid output. +// Returning either the caught error or nil. It will terminate the invoke +// immediately if second error is returned with non-nil value. When first +// argument is less than `1`, the function runs until a successful response +// is returned. +func HaltingAttempt(maxIteration int, f func(int) (error, error)) (int, error, error) { + var err error + var haltingErr error + + for i := 0; maxIteration <= 0 || i < maxIteration; i++ { + // for retries >= 0 { + err, haltingErr = f(i) + if haltingErr != nil { + return i + 1, nil, haltingErr + } + if err == nil { + return i + 1, nil, nil + } + } + + return maxIteration, err, haltingErr +} + +// AttemptWithDelay invokes a function N times until it returns valid output, +// with a pause between each call. Returning either the caught error or nil. +// It will terminate the invoke immediately if second error is returned with +// non-nil value. When first argument is less than `1`, the function runs +// until a successful response is returned. +func HaltingAttemptWithDelay(maxIteration int, delay time.Duration, f func(int, time.Duration) (error, error)) (int, time.Duration, error, error) { + var err error + var haltingErr error + + start := time.Now() + + for i := 0; maxIteration <= 0 || i < maxIteration; i++ { + err, haltingErr = f(i, time.Since(start)) + if haltingErr != nil { + return i + 1, time.Since(start), nil, haltingErr + } + if err == nil { + return i + 1, time.Since(start), nil, nil + } + + if maxIteration <= 0 || i+1 < maxIteration { + time.Sleep(delay) + } + } + + return maxIteration, time.Since(start), err, haltingErr +} + // throttle ? diff --git a/retry_test.go b/retry_test.go index b4d5d8dc..d5acae28 100644 --- a/retry_test.go +++ b/retry_test.go @@ -98,6 +98,189 @@ func TestAttemptWithDelay(t *testing.T) { is.Equal(err4, nil) } +func TestHaltingAttempt(t *testing.T) { + is := assert.New(t) + + err := fmt.Errorf("failed") + + iter1, err1, haltingErr1 := HaltingAttempt(42, func(i int) (error, error) { + return nil, nil + }) + + is.Equal(iter1, 1) + is.Nil(err1) + is.Nil(haltingErr1) + + iter2, err2, haltingErr2 := HaltingAttempt(42, func(i int) (error, error) { + if i == 5 { + return nil, nil + } + + return err, nil + }) + + is.Equal(iter2, 6) + is.Nil(err2) + is.Nil(haltingErr2) + + iter3, err3, haltingErr3 := HaltingAttempt(2, func(i int) (error, error) { + if i == 5 { + return nil, nil + } + + return err, nil + }) + + is.Equal(iter3, 2) + is.Equal(err3, err) + is.Nil(haltingErr3) + + iter4, err4, haltingErr4 := HaltingAttempt(0, func(i int) (error, error) { + if i < 42 { + return err, nil + } + + return nil, nil + }) + + is.Equal(iter4, 43) + is.Nil(err4) + is.Nil(haltingErr4) + + iter5, err5, haltingErr5 := HaltingAttempt(0, func(i int) (error, error) { + if i == 5 { + return nil, err + } + + return err, nil + }) + + is.Equal(iter5, 6) + is.Nil(err5) + is.Equal(err, haltingErr5) + + iter6, err6, haltingErr6 := HaltingAttempt(0, func(i int) (error, error) { + return nil, err + }) + + is.Equal(iter6, 1) + is.Nil(err6) + is.Equal(err, haltingErr6) + + iter7, err7, haltingErr7 := HaltingAttempt(42, func(i int) (error, error) { + if i == 42 { + return nil, err + } + if i < 41 { + return err, nil + } + + return nil, nil + }) + + is.Equal(iter7, 42) + is.Nil(err7) + is.Nil(haltingErr7) +} + +func TestHaltingAttemptWithDelay(t *testing.T) { + is := assert.New(t) + + err := fmt.Errorf("failed") + + iter1, dur1, err1, haltingErr1 := HaltingAttemptWithDelay(42, 10*time.Millisecond, func(i int, d time.Duration) (error, error) { + return nil, nil + }) + + is.Equal(iter1, 1) + is.Greater(dur1, 0*time.Millisecond) + is.Less(dur1, 1*time.Millisecond) + is.Nil(err1) + is.Nil(haltingErr1) + + iter2, dur2, err2, haltingErr2 := HaltingAttemptWithDelay(42, 10*time.Millisecond, func(i int, d time.Duration) (error, error) { + if i == 5 { + return nil, nil + } + + return err, nil + }) + + is.Equal(iter2, 6) + is.Greater(dur2, 50*time.Millisecond) + is.Less(dur2, 60*time.Millisecond) + is.Nil(err2) + is.Nil(haltingErr2) + + iter3, dur3, err3, haltingErr3 := HaltingAttemptWithDelay(2, 10*time.Millisecond, func(i int, d time.Duration) (error, error) { + if i == 5 { + return nil, nil + } + + return err, nil + }) + + is.Equal(iter3, 2) + is.Greater(dur3, 10*time.Millisecond) + is.Less(dur3, 20*time.Millisecond) + is.Equal(err3, err) + is.Nil(haltingErr3) + + iter4, dur4, err4, haltingErr4 := HaltingAttemptWithDelay(0, 10*time.Millisecond, func(i int, d time.Duration) (error, error) { + if i < 10 { + return err, nil + } + + return nil, nil + }) + + is.Equal(iter4, 11) + is.Greater(dur4, 100*time.Millisecond) + is.Less(dur4, 115*time.Millisecond) + is.Nil(err4) + is.Nil(haltingErr4) + + iter5, dur5, err5, haltingErr5 := HaltingAttemptWithDelay(0, 10*time.Millisecond, func(i int, d time.Duration) (error, error) { + if i == 5 { + return nil, err + } + + return err, nil + }) + + is.Equal(iter5, 6) + is.Greater(dur5, 10*time.Millisecond) + is.Less(dur5, 115*time.Millisecond) + is.Nil(err5) + is.Equal(err, haltingErr5) + + iter6, dur6, err6, haltingErr6 := HaltingAttemptWithDelay(0, 10*time.Millisecond, func(i int, d time.Duration) (error, error) { + return nil, err + }) + + is.Equal(iter6, 1) + is.Less(dur6, 10*time.Millisecond) + is.Less(dur6, 115*time.Millisecond) + is.Nil(err6) + is.Equal(err, haltingErr6) + + iter7, dur7, err7, haltingErr7 := HaltingAttemptWithDelay(42, 10*time.Millisecond, func(i int, d time.Duration) (error, error) { + if i == 42 { + return nil, err + } + if i < 41 { + return err, nil + } + + return nil, nil + }) + + is.Equal(iter7, 42) + is.Less(dur7, 500*time.Millisecond) + is.Nil(err7) + is.Nil(haltingErr7) +} + func TestDebounce(t *testing.T) { t.Parallel() From 6ebc218fe4b4eb3e2e807001843cc034ef61e5a6 Mon Sep 17 00:00:00 2001 From: Ayaka Neko Date: Mon, 28 Nov 2022 14:16:08 +0800 Subject: [PATCH 2/2] chore: re-implement it as AttemptWhile and AttemptWhileWithDelay --- README.md | 51 +++++++++++++++++++++++++++ retry.go | 50 +++++++++++++------------- retry_test.go | 98 ++++++++++++++++++++++----------------------------- 3 files changed, 119 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 99428830..21288132 100644 --- a/README.md +++ b/README.md @@ -2315,6 +2315,7 @@ For more advanced retry strategies (delay, exponential backoff...), please take ### AttemptWithDelay Invokes a function N times until it returns valid output, with a pause between each call. Returning either the caught error or nil. + When first argument is less than `1`, the function runs until a successful response is returned. ```go @@ -2334,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 e4a75920..60d1647c 100644 --- a/retry.go +++ b/retry.go @@ -101,47 +101,49 @@ func AttemptWithDelay(maxIteration int, delay time.Duration, f func(index int, d return maxIteration, time.Since(start), err } -// HaltingAttempt invokes a function N times until it returns valid output. -// Returning either the caught error or nil. It will terminate the invoke -// immediately if second error is returned with non-nil value. When first -// argument is less than `1`, the function runs until a successful response -// is returned. -func HaltingAttempt(maxIteration int, f func(int) (error, error)) (int, error, error) { +// 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 haltingErr error + var shouldContinueInvoke bool for i := 0; maxIteration <= 0 || i < maxIteration; i++ { // for retries >= 0 { - err, haltingErr = f(i) - if haltingErr != nil { - return i + 1, nil, haltingErr + err, shouldContinueInvoke = f(i) + if !shouldContinueInvoke { // if shouldContinueInvoke is false, then return immediately + return i + 1, err } if err == nil { - return i + 1, nil, nil + return i + 1, nil } } - return maxIteration, err, haltingErr + return maxIteration, err } -// AttemptWithDelay invokes a function N times until it returns valid output, -// with a pause between each call. Returning either the caught error or nil. -// It will terminate the invoke immediately if second error is returned with -// non-nil value. When first argument is less than `1`, the function runs -// until a successful response is returned. -func HaltingAttemptWithDelay(maxIteration int, delay time.Duration, f func(int, time.Duration) (error, error)) (int, time.Duration, error, error) { +// 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 haltingErr error + var shouldContinueInvoke bool start := time.Now() for i := 0; maxIteration <= 0 || i < maxIteration; i++ { - err, haltingErr = f(i, time.Since(start)) - if haltingErr != nil { - return i + 1, time.Since(start), nil, haltingErr + 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, nil + return i + 1, time.Since(start), nil } if maxIteration <= 0 || i+1 < maxIteration { @@ -149,7 +151,7 @@ func HaltingAttemptWithDelay(maxIteration int, delay time.Duration, f func(int, } } - return maxIteration, time.Since(start), err, haltingErr + return maxIteration, time.Since(start), err } // throttle ? diff --git a/retry_test.go b/retry_test.go index d5acae28..015ec791 100644 --- a/retry_test.go +++ b/retry_test.go @@ -98,187 +98,173 @@ func TestAttemptWithDelay(t *testing.T) { is.Equal(err4, nil) } -func TestHaltingAttempt(t *testing.T) { +func TestAttemptWhile(t *testing.T) { is := assert.New(t) err := fmt.Errorf("failed") - iter1, err1, haltingErr1 := HaltingAttempt(42, func(i int) (error, error) { - return nil, nil + iter1, err1 := AttemptWhile(42, func(i int) (error, bool) { + return nil, true }) is.Equal(iter1, 1) is.Nil(err1) - is.Nil(haltingErr1) - iter2, err2, haltingErr2 := HaltingAttempt(42, func(i int) (error, error) { + iter2, err2 := AttemptWhile(42, func(i int) (error, bool) { if i == 5 { - return nil, nil + return nil, true } - return err, nil + return err, true }) is.Equal(iter2, 6) is.Nil(err2) - is.Nil(haltingErr2) - iter3, err3, haltingErr3 := HaltingAttempt(2, func(i int) (error, error) { + iter3, err3 := AttemptWhile(2, func(i int) (error, bool) { if i == 5 { - return nil, nil + return nil, true } - return err, nil + return err, true }) is.Equal(iter3, 2) is.Equal(err3, err) - is.Nil(haltingErr3) - iter4, err4, haltingErr4 := HaltingAttempt(0, func(i int) (error, error) { + iter4, err4 := AttemptWhile(0, func(i int) (error, bool) { if i < 42 { - return err, nil + return err, true } - return nil, nil + return nil, true }) is.Equal(iter4, 43) is.Nil(err4) - is.Nil(haltingErr4) - iter5, err5, haltingErr5 := HaltingAttempt(0, func(i int) (error, error) { + iter5, err5 := AttemptWhile(0, func(i int) (error, bool) { if i == 5 { - return nil, err + return nil, false } - return err, nil + return err, true }) is.Equal(iter5, 6) is.Nil(err5) - is.Equal(err, haltingErr5) - iter6, err6, haltingErr6 := HaltingAttempt(0, func(i int) (error, error) { - return nil, err + iter6, err6 := AttemptWhile(0, func(i int) (error, bool) { + return nil, false }) is.Equal(iter6, 1) is.Nil(err6) - is.Equal(err, haltingErr6) - iter7, err7, haltingErr7 := HaltingAttempt(42, func(i int) (error, error) { + iter7, err7 := AttemptWhile(42, func(i int) (error, bool) { if i == 42 { - return nil, err + return nil, false } if i < 41 { - return err, nil + return err, true } - return nil, nil + return nil, true }) is.Equal(iter7, 42) is.Nil(err7) - is.Nil(haltingErr7) } -func TestHaltingAttemptWithDelay(t *testing.T) { +func TestAttemptWhileWithDelay(t *testing.T) { is := assert.New(t) err := fmt.Errorf("failed") - iter1, dur1, err1, haltingErr1 := HaltingAttemptWithDelay(42, 10*time.Millisecond, func(i int, d time.Duration) (error, error) { - return nil, nil + 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) - is.Nil(haltingErr1) - iter2, dur2, err2, haltingErr2 := HaltingAttemptWithDelay(42, 10*time.Millisecond, func(i int, d time.Duration) (error, error) { + iter2, dur2, err2 := AttemptWhileWithDelay(42, 10*time.Millisecond, func(i int, d time.Duration) (error, bool) { if i == 5 { - return nil, nil + return nil, true } - return err, nil + return err, true }) is.Equal(iter2, 6) is.Greater(dur2, 50*time.Millisecond) is.Less(dur2, 60*time.Millisecond) is.Nil(err2) - is.Nil(haltingErr2) - iter3, dur3, err3, haltingErr3 := HaltingAttemptWithDelay(2, 10*time.Millisecond, func(i int, d time.Duration) (error, error) { + iter3, dur3, err3 := AttemptWhileWithDelay(2, 10*time.Millisecond, func(i int, d time.Duration) (error, bool) { if i == 5 { - return nil, nil + return nil, true } - return err, nil + return err, true }) is.Equal(iter3, 2) is.Greater(dur3, 10*time.Millisecond) is.Less(dur3, 20*time.Millisecond) is.Equal(err3, err) - is.Nil(haltingErr3) - iter4, dur4, err4, haltingErr4 := HaltingAttemptWithDelay(0, 10*time.Millisecond, func(i int, d time.Duration) (error, error) { + iter4, dur4, err4 := AttemptWhileWithDelay(0, 10*time.Millisecond, func(i int, d time.Duration) (error, bool) { if i < 10 { - return err, nil + return err, true } - return nil, nil + return nil, true }) is.Equal(iter4, 11) is.Greater(dur4, 100*time.Millisecond) is.Less(dur4, 115*time.Millisecond) is.Nil(err4) - is.Nil(haltingErr4) - iter5, dur5, err5, haltingErr5 := HaltingAttemptWithDelay(0, 10*time.Millisecond, func(i int, d time.Duration) (error, error) { + iter5, dur5, err5 := AttemptWhileWithDelay(0, 10*time.Millisecond, func(i int, d time.Duration) (error, bool) { if i == 5 { - return nil, err + return nil, false } - return err, nil + return err, true }) is.Equal(iter5, 6) is.Greater(dur5, 10*time.Millisecond) is.Less(dur5, 115*time.Millisecond) is.Nil(err5) - is.Equal(err, haltingErr5) - iter6, dur6, err6, haltingErr6 := HaltingAttemptWithDelay(0, 10*time.Millisecond, func(i int, d time.Duration) (error, error) { - return nil, err + 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) - is.Equal(err, haltingErr6) - iter7, dur7, err7, haltingErr7 := HaltingAttemptWithDelay(42, 10*time.Millisecond, func(i int, d time.Duration) (error, error) { + iter7, dur7, err7 := AttemptWhileWithDelay(42, 10*time.Millisecond, func(i int, d time.Duration) (error, bool) { if i == 42 { - return nil, err + return nil, false } if i < 41 { - return err, nil + return err, true } - return nil, nil + return nil, true }) is.Equal(iter7, 42) is.Less(dur7, 500*time.Millisecond) is.Nil(err7) - is.Nil(haltingErr7) } func TestDebounce(t *testing.T) {