mirror of
https://github.com/duke-git/lancet.git
synced 2026-02-11 00:02:28 +08:00
Add exponential With jitter backoff (#174)
* Add exponential With jitter backoff Adds exponential + jitter retry policy. To enable drastic slow down of sending out requests to any external system. Jitter in computational contexts refers to the addition of a small random variation to a value to break the symmetric patterns * Retry with exp: Allow shift for all multiple of 2
This commit is contained in:
@@ -8,6 +8,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -56,6 +58,36 @@ func RetryWithLinearBackoff(interval time.Duration) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RetryWithExponentialWithJitterBackoff set exponential strategy backoff
|
||||||
|
// todo: Add playground link
|
||||||
|
func RetryWithExponentialWithJitterBackoff(interval time.Duration, base uint64, maxJitter time.Duration) Option {
|
||||||
|
if interval <= 0 {
|
||||||
|
panic("programming error: retry interval should not be lower or equal to 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxJitter < 0 {
|
||||||
|
panic("programming error: retry maxJitter should not be lower to 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if base%2 == 0 {
|
||||||
|
return func(rc *RetryConfig) {
|
||||||
|
rc.backoffStrategy = &shiftExponentialWithJitter{
|
||||||
|
interval: interval,
|
||||||
|
maxJitter: maxJitter,
|
||||||
|
shifter: uint64(math.Log2(float64(base))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(rc *RetryConfig) {
|
||||||
|
rc.backoffStrategy = &exponentialWithJitter{
|
||||||
|
interval: interval,
|
||||||
|
base: time.Duration(base),
|
||||||
|
maxJitter: maxJitter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Context set retry context config.
|
// Context set retry context config.
|
||||||
// Play: https://go.dev/play/p/xnAOOXv9GkS
|
// Play: https://go.dev/play/p/xnAOOXv9GkS
|
||||||
func Context(ctx context.Context) Option {
|
func Context(ctx context.Context) Option {
|
||||||
@@ -117,8 +149,45 @@ type linear struct {
|
|||||||
interval time.Duration
|
interval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// CalculateInterval is the method implementation for the linear struct.
|
// CalculateInterval calculates the next interval returns a constant.
|
||||||
// It returns the fixed interval defined in the linear struct.
|
|
||||||
func (l *linear) CalculateInterval() time.Duration {
|
func (l *linear) CalculateInterval() time.Duration {
|
||||||
return l.interval
|
return l.interval
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exponentialWithJitter is a struct that implements the BackoffStrategy interface using a exponential backoff strategy.
|
||||||
|
type exponentialWithJitter struct {
|
||||||
|
base time.Duration // base is the multiplier for the exponential backoff.
|
||||||
|
interval time.Duration // interval is the current backoff interval, which will be adjusted over time.
|
||||||
|
maxJitter time.Duration // maxJitter is the maximum amount of jitter to apply to the backoff interval.
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateInterval calculates the next backoff interval with jitter and updates the interval.
|
||||||
|
func (e *exponentialWithJitter) CalculateInterval() time.Duration {
|
||||||
|
current := e.interval
|
||||||
|
e.interval = e.interval * e.base
|
||||||
|
return current + jitter(e.maxJitter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// shiftExponentialWithJitter is a struct that implements the BackoffStrategy interface using a exponential backoff strategy.
|
||||||
|
type shiftExponentialWithJitter struct {
|
||||||
|
interval time.Duration // interval is the current backoff interval, which will be adjusted over time.
|
||||||
|
maxJitter time.Duration // maxJitter is the maximum amount of jitter to apply to the backoff interval.
|
||||||
|
shifter uint64 // shift by n faster than multiplication
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateInterval calculates the next backoff interval with jitter and updates the interval.
|
||||||
|
// Uses shift instead of multiplication
|
||||||
|
func (e *shiftExponentialWithJitter) CalculateInterval() time.Duration {
|
||||||
|
current := e.interval
|
||||||
|
e.interval = e.interval << e.shifter
|
||||||
|
return current + jitter(e.maxJitter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jitter adds a random duration, up to maxJitter,
|
||||||
|
// to the current interval to introduce randomness and avoid synchronized patterns in retry behavior
|
||||||
|
func jitter(maxJitter time.Duration) time.Duration {
|
||||||
|
if maxJitter == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return time.Duration(rand.Int63n(int64(maxJitter)) + 1)
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,27 @@ func ExampleRetryWithLinearBackoff() {
|
|||||||
// 3
|
// 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExampleRetryWithExponentialWithJitterBackoff() {
|
||||||
|
number := 0
|
||||||
|
increaseNumber := func() error {
|
||||||
|
number++
|
||||||
|
if number == 3 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("error occurs")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Retry(increaseNumber, RetryWithExponentialWithJitterBackoff(time.Microsecond*50, 2, time.Microsecond*25))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(number)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// 3
|
||||||
|
}
|
||||||
|
|
||||||
func ExampleRetryTimes() {
|
func ExampleRetryTimes() {
|
||||||
number := 0
|
number := 0
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,80 @@ func TestRetryFailed(t *testing.T) {
|
|||||||
assert.Equal(DefaultRetryTimes, number)
|
assert.Equal(DefaultRetryTimes, number)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRetryShiftExponentialWithJitterFailed(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
assert := internal.NewAssert(t, "TestRetryShiftExponentialWithJitterFailed")
|
||||||
|
|
||||||
|
var number int
|
||||||
|
increaseNumber := func() error {
|
||||||
|
number++
|
||||||
|
return errors.New("error occurs")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Retry(increaseNumber, RetryWithExponentialWithJitterBackoff(time.Microsecond*50, 2, time.Microsecond*25))
|
||||||
|
|
||||||
|
assert.IsNotNil(err)
|
||||||
|
assert.Equal(DefaultRetryTimes, number)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetryExponentialWithJitterFailed(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
assert := internal.NewAssert(t, "TestRetryExponentialWithJitterFailed")
|
||||||
|
|
||||||
|
var number int
|
||||||
|
increaseNumber := func() error {
|
||||||
|
number++
|
||||||
|
return errors.New("error occurs")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Retry(increaseNumber, RetryWithExponentialWithJitterBackoff(time.Microsecond*50, 3, time.Microsecond*25))
|
||||||
|
|
||||||
|
assert.IsNotNil(err)
|
||||||
|
assert.Equal(DefaultRetryTimes, number)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetryWithExponentialSucceeded(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
assert := internal.NewAssert(t, "TestRetryWithExponentialSucceeded")
|
||||||
|
|
||||||
|
var number int
|
||||||
|
increaseNumber := func() error {
|
||||||
|
number++
|
||||||
|
if number == DefaultRetryTimes {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("error occurs")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Retry(increaseNumber, RetryWithExponentialWithJitterBackoff(time.Microsecond*50, 3, time.Microsecond*25))
|
||||||
|
|
||||||
|
assert.IsNil(err)
|
||||||
|
assert.Equal(DefaultRetryTimes, number)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetryWithExponentialShiftSucceeded(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
assert := internal.NewAssert(t, "TestRetryWithExponentialShiftSucceeded")
|
||||||
|
|
||||||
|
var number int
|
||||||
|
increaseNumber := func() error {
|
||||||
|
number++
|
||||||
|
if number == DefaultRetryTimes {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("error occurs")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Retry(increaseNumber, RetryWithExponentialWithJitterBackoff(time.Microsecond*50, 4, time.Microsecond*25))
|
||||||
|
|
||||||
|
assert.IsNil(err)
|
||||||
|
assert.Equal(DefaultRetryTimes, number)
|
||||||
|
}
|
||||||
|
|
||||||
func TestRetrySucceeded(t *testing.T) {
|
func TestRetrySucceeded(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -46,6 +120,74 @@ func TestRetrySucceeded(t *testing.T) {
|
|||||||
assert.Equal(DefaultRetryTimes, number)
|
assert.Equal(DefaultRetryTimes, number)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRetryOneShotSucceeded(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
assert := internal.NewAssert(t, "TestRetryOneShotSucceeded")
|
||||||
|
|
||||||
|
var number int
|
||||||
|
increaseNumber := func() error {
|
||||||
|
number++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Retry(increaseNumber, RetryWithLinearBackoff(time.Microsecond*50))
|
||||||
|
|
||||||
|
assert.IsNil(err)
|
||||||
|
assert.Equal(1, number)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetryWithExponentialWithJitterBackoffShiftOneShotSucceeded(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
assert := internal.NewAssert(t, "TestRetryWithExponentialWithJitterBackoffShiftOneShotSucceeded")
|
||||||
|
|
||||||
|
var number int
|
||||||
|
increaseNumber := func() error {
|
||||||
|
number++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Retry(increaseNumber, RetryWithExponentialWithJitterBackoff(time.Microsecond*50, 2, time.Microsecond*25))
|
||||||
|
|
||||||
|
assert.IsNil(err)
|
||||||
|
assert.Equal(1, number)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetryWithExponentialWithJitterBackoffOneShotSucceeded(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
assert := internal.NewAssert(t, "TestRetryWithExponentialWithJitterBackoffOneShotSucceeded")
|
||||||
|
|
||||||
|
var number int
|
||||||
|
increaseNumber := func() error {
|
||||||
|
number++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Retry(increaseNumber, RetryWithExponentialWithJitterBackoff(time.Microsecond*50, 3, time.Microsecond*25))
|
||||||
|
|
||||||
|
assert.IsNil(err)
|
||||||
|
assert.Equal(1, number)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetryWithExponentialWithJitterBackoffNoJitterOneShotSucceeded(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
assert := internal.NewAssert(t, "TestRetryWithExponentialWithJitterBackoffNoJitterOneShotSucceeded")
|
||||||
|
|
||||||
|
var number int
|
||||||
|
increaseNumber := func() error {
|
||||||
|
number++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Retry(increaseNumber, RetryWithExponentialWithJitterBackoff(time.Microsecond*50, 3, 0))
|
||||||
|
|
||||||
|
assert.IsNil(err)
|
||||||
|
assert.Equal(1, number)
|
||||||
|
}
|
||||||
|
|
||||||
func TestSetRetryTimes(t *testing.T) {
|
func TestSetRetryTimes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user