Compare commits

..
2 Commits
Author SHA1 Message Date
dudaodong 2e1c2276a5 test: add test coverageg 2024-02-22 14:39:53 +08:00
donutloopandGitHub d367397dab 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
2024-02-22 10:39:45 +08:00
5 changed files with 311 additions and 3 deletions
+68
View File
@@ -1,6 +1,7 @@
package compare
import (
"encoding/json"
"testing"
"time"
@@ -11,6 +12,7 @@ func TestEqual(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestEqual")
// basic type
assert.Equal(true, Equal(1, 1))
assert.Equal(true, Equal(int64(1), int64(1)))
assert.Equal(true, Equal("a", "a"))
@@ -25,6 +27,7 @@ func TestEqual(t *testing.T) {
assert.Equal(false, Equal([]int{1, 2}, []int{1, 2, 3}))
assert.Equal(false, Equal(map[int]string{1: "a", 2: "b"}, map[int]string{1: "a"}))
// time
time1 := time.Now()
time2 := time1.Add(time.Second)
time3 := time1.Add(time.Second)
@@ -32,6 +35,7 @@ func TestEqual(t *testing.T) {
assert.Equal(false, Equal(time1, time2))
assert.Equal(true, Equal(time2, time3))
// struct
st1 := struct {
A string
B string
@@ -58,6 +62,19 @@ func TestEqual(t *testing.T) {
assert.Equal(true, Equal(st1, st2))
assert.Equal(false, Equal(st1, st3))
//byte slice
bs1 := []byte("hello")
bs2 := []byte("hello")
assert.Equal(true, Equal(bs1, bs2))
// json.Number
var jsonNumber1, jsonNumber2 json.Number
json.Unmarshal([]byte(`123`), &jsonNumber1)
json.Unmarshal([]byte(`123`), &jsonNumber2)
assert.Equal(true, Equal(jsonNumber1, jsonNumber2))
}
func TestEqualValue(t *testing.T) {
@@ -69,6 +86,13 @@ func TestEqualValue(t *testing.T) {
assert.Equal(true, EqualValue(1, "1"))
assert.Equal(false, EqualValue(1, "2"))
// json.Number
var jsonNumber1, jsonNumber2 json.Number
json.Unmarshal([]byte(`123`), &jsonNumber1)
json.Unmarshal([]byte(`123`), &jsonNumber2)
assert.Equal(true, EqualValue(jsonNumber1, 123))
}
func TestLessThan(t *testing.T) {
@@ -85,6 +109,17 @@ func TestLessThan(t *testing.T) {
assert.Equal(false, LessThan(1, 1))
assert.Equal(false, LessThan(1, int64(1)))
bs1 := []byte("hello1")
bs2 := []byte("hello2")
assert.Equal(true, LessThan(bs1, bs2))
// json.Number
var jsonNumber1, jsonNumber2 json.Number
json.Unmarshal([]byte(`123`), &jsonNumber1)
json.Unmarshal([]byte(`124`), &jsonNumber2)
assert.Equal(true, LessThan(jsonNumber1, jsonNumber2))
}
func TestGreaterThan(t *testing.T) {
@@ -102,6 +137,17 @@ func TestGreaterThan(t *testing.T) {
assert.Equal(false, GreaterThan(1, 2))
assert.Equal(false, GreaterThan(int64(2), 1))
assert.Equal(false, GreaterThan("b", "c"))
bs1 := []byte("hello1")
bs2 := []byte("hello2")
assert.Equal(true, GreaterThan(bs2, bs1))
// json.Number
var jsonNumber1, jsonNumber2 json.Number
json.Unmarshal([]byte(`123`), &jsonNumber1)
json.Unmarshal([]byte(`124`), &jsonNumber2)
assert.Equal(true, GreaterThan(jsonNumber2, jsonNumber1))
}
func TestLessOrEqual(t *testing.T) {
@@ -119,6 +165,17 @@ func TestLessOrEqual(t *testing.T) {
assert.Equal(false, LessOrEqual(2, 1))
assert.Equal(false, LessOrEqual(1, int64(2)))
bs1 := []byte("hello1")
bs2 := []byte("hello2")
assert.Equal(true, LessOrEqual(bs1, bs2))
// json.Number
var jsonNumber1, jsonNumber2 json.Number
json.Unmarshal([]byte(`123`), &jsonNumber1)
json.Unmarshal([]byte(`124`), &jsonNumber2)
assert.Equal(true, LessOrEqual(jsonNumber1, jsonNumber2))
}
func TestGreaterOrEqual(t *testing.T) {
@@ -137,6 +194,17 @@ func TestGreaterOrEqual(t *testing.T) {
assert.Equal(false, GreaterOrEqual(1, 2))
assert.Equal(false, GreaterOrEqual(int64(2), 1))
assert.Equal(false, GreaterOrEqual("b", "c"))
bs1 := []byte("hello1")
bs2 := []byte("hello2")
assert.Equal(true, GreaterOrEqual(bs2, bs1))
// json.Number
var jsonNumber1, jsonNumber2 json.Number
json.Unmarshal([]byte(`123`), &jsonNumber1)
json.Unmarshal([]byte(`124`), &jsonNumber2)
assert.Equal(true, GreaterOrEqual(jsonNumber2, jsonNumber1))
}
func TestInDelta(t *testing.T) {
+9 -1
View File
@@ -6,10 +6,11 @@ import (
func TestAssert(t *testing.T) {
assert := NewAssert(t, "TestAssert")
assert.Equal(0, 0)
assert.NotEqual(1, 0)
assert.NotEqual("1", 1)
var uInt1 uint
var uInt2 uint
var uInt8 uint8
@@ -47,4 +48,11 @@ func TestAssert(t *testing.T) {
assert.IsNil(nil)
assert.IsNotNil("abc")
var valA int = 1
var valB int64 = 1
assert.NotEqual(valA, valB)
assert.EqualValues(valA, valB)
assert.ShouldBeFalse(false)
assert.ShouldBeTrue(true)
}
+71 -2
View File
@@ -8,6 +8,8 @@ import (
"context"
"errors"
"fmt"
"math"
"math/rand"
"reflect"
"runtime"
"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.
// Play: https://go.dev/play/p/xnAOOXv9GkS
func Context(ctx context.Context) Option {
@@ -117,8 +149,45 @@ type linear struct {
interval time.Duration
}
// CalculateInterval is the method implementation for the linear struct.
// It returns the fixed interval defined in the linear struct.
// CalculateInterval calculates the next interval returns a constant.
func (l *linear) CalculateInterval() time.Duration {
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)
}
+21
View File
@@ -51,6 +51,27 @@ func ExampleRetryWithLinearBackoff() {
// 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() {
number := 0
+142
View File
@@ -26,6 +26,80 @@ func TestRetryFailed(t *testing.T) {
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) {
t.Parallel()
@@ -46,6 +120,74 @@ func TestRetrySucceeded(t *testing.T) {
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) {
t.Parallel()