Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ v3 (feature): add retry mechanism #1972

Merged
merged 9 commits into from
Aug 19, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/mattn/go-isatty v0.0.14
github.com/savsgio/dictpool v0.0.0-20220406081701-03de5edb2e6d
github.com/shirou/gopsutil/v3 v3.22.5
github.com/stretchr/testify v1.7.1
github.com/tinylib/msgp v1.1.6
github.com/valyala/bytebufferpool v1.0.0
github.com/valyala/fasthttp v1.37.0
Expand All @@ -16,15 +17,18 @@ require (

require (
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/klauspost/compress v1.15.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/philhofer/fwd v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/savsgio/gotils v0.0.0-20220401102855-e56b59f40436 // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/tklauser/numcpus v0.4.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
68 changes: 68 additions & 0 deletions middleware/retry/exponential_backoff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package retry

import (
"math/rand"
"time"
)

// ExponentialBackoff is a retry mechanism for retrying some calls.
type ExponentialBackoff struct {
InitialInterval time.Duration
MaxBackoffTime time.Duration
Multiplier float64
MaxRetryCount int
currentInterval time.Duration
}

const (
DefaultInitialInterval = 1 * time.Second
DefaultMaxBackoffTime = 32 * time.Second
DefaultMultiplier = 2.0
DefaultMaxRetryCount = 10
)

func init() {
rand.Seed(time.Now().UnixNano())
}

// NewExponentialBackoff creates a ExponentialBackoff with default values.
func NewExponentialBackoff() *ExponentialBackoff {
return &ExponentialBackoff{
InitialInterval: DefaultInitialInterval,
MaxBackoffTime: DefaultMaxBackoffTime,
Multiplier: DefaultMultiplier,
MaxRetryCount: DefaultMaxRetryCount,
currentInterval: DefaultInitialInterval,
}
}

// Retry is the core logic of the retry mechanism. If the calling function returns
// nil as an error, then the Retry method is terminated with returning nil. Otherwise,
// if all function calls are returned error, then the method returns this error.
func (e *ExponentialBackoff) Retry(f func() error) error {
if e.currentInterval <= 0 {
e.currentInterval = e.InitialInterval
}
var err error
for i := 0; i < e.MaxRetryCount; i++ {
err = f()
if err == nil {
return nil
}
next := e.next()
time.Sleep(next)
}
return err
}

// next calculates the next sleeping time interval.
func (e *ExponentialBackoff) next() time.Duration {
// add random value between [0, 1000)
t := e.currentInterval + (time.Duration(rand.Int63n(1000)) * time.Millisecond)
e.currentInterval = time.Duration(float64(e.currentInterval) * e.Multiplier)
if t >= e.MaxBackoffTime {
e.currentInterval = e.MaxBackoffTime
return e.MaxBackoffTime
}
return t
}
124 changes: 124 additions & 0 deletions middleware/retry/exponential_backoff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package retry

import (
"fmt"
"github.com/gofiber/fiber/v3/utils"
"testing"
"time"
)

func TestExponentialBackoff_Retry(t *testing.T) {
tests := []struct {
name string
expBackoff *ExponentialBackoff
f func() error
expErr error
}{
{
name: "With default values - successful",
expBackoff: NewExponentialBackoff(),
f: func() error {
return nil
},
},
{
name: "With default values - unsuccessful",
expBackoff: NewExponentialBackoff(),
f: func() error {
return fmt.Errorf("failed function")
},
expErr: fmt.Errorf("failed function"),
},
{
name: "Successful function",
expBackoff: &ExponentialBackoff{
InitialInterval: 1 * time.Millisecond,
MaxBackoffTime: 100 * time.Millisecond,
Multiplier: 2.0,
MaxRetryCount: 5,
},
f: func() error {
return nil
},
},
{
name: "Unsuccessful function",
expBackoff: &ExponentialBackoff{
InitialInterval: 2 * time.Millisecond,
MaxBackoffTime: 100 * time.Millisecond,
Multiplier: 2.0,
MaxRetryCount: 5,
},
f: func() error {
return fmt.Errorf("failed function")
},
expErr: fmt.Errorf("failed function"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.expBackoff.Retry(tt.f)
utils.AssertEqual(t, tt.expErr, err)
})
}
}

func TestExponentialBackoff_Next(t *testing.T) {
tests := []struct {
name string
expBackoff *ExponentialBackoff
expNextTimeIntervals []time.Duration
}{
{
name: "With default values",
expBackoff: NewExponentialBackoff(),
expNextTimeIntervals: []time.Duration{
1 * time.Second,
2 * time.Second,
4 * time.Second,
8 * time.Second,
16 * time.Second,
32 * time.Second,
32 * time.Second,
32 * time.Second,
32 * time.Second,
32 * time.Second,
},
},
{
name: "Custom values",
expBackoff: &ExponentialBackoff{
InitialInterval: 2.0 * time.Second,
MaxBackoffTime: 64 * time.Second,
Multiplier: 3.0,
MaxRetryCount: 8,
currentInterval: 2.0 * time.Second,
},
expNextTimeIntervals: []time.Duration{
2 * time.Second,
6 * time.Second,
18 * time.Second,
54 * time.Second,
64 * time.Second,
64 * time.Second,
64 * time.Second,
64 * time.Second,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for i := 0; i < tt.expBackoff.MaxRetryCount; i++ {
next := tt.expBackoff.next()
if next < tt.expNextTimeIntervals[i] || next > tt.expNextTimeIntervals[i]+1*time.Second {
t.Errorf("wrong next time:\n"+
"actual:%v\n"+
"expected range:%v-%v\n",
next, tt.expNextTimeIntervals[i], tt.expNextTimeIntervals[i]+1*time.Second)
}
}
})
}
}