mirror of https://github.com/gofiber/fiber.git
✨ v3 (feature): add retry mechanism (#1972)
* v3-retry-mechanism: Add retry mechanism * General logic is implemented. * Unit tests are added. Signed-off-by: Gökhan Özeloğlu <gokhan.ozeloglu@deliveryhero.com> * Refactor test assertion * Replaced testify/assert with fiber's assert. Signed-off-by: Gökhan Özeloğlu <gokhan.ozeloglu@deliveryhero.com> * Add test for next method * currentInterval bug is fixed in Retry. * If condition is fixed in next. * struct definition refactored and if condtion is removed in TestExponentialBackoff_Retry. Signed-off-by: Gökhan Özeloğlu <gokhan.ozeloglu@deliveryhero.com> * Add config for retry. * Constant variables are removed. * Helper function is added for default. * Helper function is used in New function. Signed-off-by: Gökhan Özeloğlu <gokhan.ozeloglu@deliveryhero.com> * Replace math/rand with crypto/rand * Random number generation package has been replaced with more secure one, crypto/rand. Signed-off-by: Gökhan Özeloğlu <gokhan.ozeloglu@deliveryhero.com> * Add a README for retry middleware * Explanation and examples are added. Signed-off-by: Gökhan Özeloğlu <gokhan.ozeloglu@deliveryhero.com> * Add comment line for documentation * Comment lines are added for ExponentialBackoff variables. Signed-off-by: Gökhan Özeloğlu <gokhan.ozeloglu@deliveryhero.com> * Run go mod tidy * Unused package(s) removed. Signed-off-by: Gökhan Özeloğlu <gokhan.ozeloglu@deliveryhero.com> * move middleware -> addon Signed-off-by: Gökhan Özeloğlu <gokhan.ozeloglu@deliveryhero.com> Co-authored-by: Muhammed Efe Çetin <efectn@protonmail.com>pull/1986/head
parent
cadd6014bb
commit
4adda508b0
|
@ -0,0 +1,97 @@
|
||||||
|
# Retry Addon
|
||||||
|
|
||||||
|
Retry addon for [Fiber](https://github.com/gofiber/fiber) designed to apply retry mechanism for unsuccessful network
|
||||||
|
operations. This addon uses exponential backoff algorithm with jitter. It calls the function multiple times and tries
|
||||||
|
to make it successful. If all calls are failed, then, it returns error. It adds a jitter at each retry step because adding
|
||||||
|
a jitter is a way to break synchronization across the client and avoid collision.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Retry Addon](#retry-addon)
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
|
- [Signatures](#signatures)
|
||||||
|
- [Examples](#examples)
|
||||||
|
- [Default Config](#default-config)
|
||||||
|
- [Custom Config](#custom-config)
|
||||||
|
- [Config](#config)
|
||||||
|
- [Default Config Example](#default-config-example)
|
||||||
|
|
||||||
|
## Signatures
|
||||||
|
|
||||||
|
```go
|
||||||
|
func NewExponentialBackoff(config ...Config) *ExponentialBackoff
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Firstly, import the addon from Fiber,
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v3/addon/retry"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Default Config
|
||||||
|
|
||||||
|
```go
|
||||||
|
retry.NewExponentialBackoff()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Config
|
||||||
|
|
||||||
|
```go
|
||||||
|
retry.NewExponentialBackoff(retry.Config{
|
||||||
|
InitialInterval: 2 * time.Second,
|
||||||
|
MaxBackoffTime: 64 * time.Second,
|
||||||
|
Multiplier: 2.0,
|
||||||
|
MaxRetryCount: 15,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Config defines the config for addon.
|
||||||
|
type Config struct {
|
||||||
|
// InitialInterval defines the initial time interval for backoff algorithm.
|
||||||
|
//
|
||||||
|
// Optional. Default: 1 * time.Second
|
||||||
|
InitialInterval time.Duration
|
||||||
|
|
||||||
|
// MaxBackoffTime defines maximum time duration for backoff algorithm. When
|
||||||
|
// the algorithm is reached this time, rest of the retries will be maximum
|
||||||
|
// 32 seconds.
|
||||||
|
//
|
||||||
|
// Optional. Default: 32 * time.Second
|
||||||
|
MaxBackoffTime time.Duration
|
||||||
|
|
||||||
|
// Multiplier defines multiplier number of the backoff algorithm.
|
||||||
|
//
|
||||||
|
// Optional. Default: 2.0
|
||||||
|
Multiplier float64
|
||||||
|
|
||||||
|
// MaxRetryCount defines maximum retry count for the backoff algorithm.
|
||||||
|
//
|
||||||
|
// Optional. Default: 10
|
||||||
|
MaxRetryCount int
|
||||||
|
|
||||||
|
// currentInterval tracks the current waiting time.
|
||||||
|
//
|
||||||
|
// Optional. Default: 1 * time.Second
|
||||||
|
currentInterval time.Duration
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Default Config Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
// DefaultConfig is the default config for retry.
|
||||||
|
var DefaultConfig = Config{
|
||||||
|
InitialInterval: 1 * time.Second,
|
||||||
|
MaxBackoffTime: 32 * time.Second,
|
||||||
|
Multiplier: 2.0,
|
||||||
|
MaxRetryCount: 10,
|
||||||
|
currentInterval: 1 * time.Second,
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,66 @@
|
||||||
|
package retry
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Config defines the config for addon.
|
||||||
|
type Config struct {
|
||||||
|
// InitialInterval defines the initial time interval for backoff algorithm.
|
||||||
|
//
|
||||||
|
// Optional. Default: 1 * time.Second
|
||||||
|
InitialInterval time.Duration
|
||||||
|
|
||||||
|
// MaxBackoffTime defines maximum time duration for backoff algorithm. When
|
||||||
|
// the algorithm is reached this time, rest of the retries will be maximum
|
||||||
|
// 32 seconds.
|
||||||
|
//
|
||||||
|
// Optional. Default: 32 * time.Second
|
||||||
|
MaxBackoffTime time.Duration
|
||||||
|
|
||||||
|
// Multiplier defines multiplier number of the backoff algorithm.
|
||||||
|
//
|
||||||
|
// Optional. Default: 2.0
|
||||||
|
Multiplier float64
|
||||||
|
|
||||||
|
// MaxRetryCount defines maximum retry count for the backoff algorithm.
|
||||||
|
//
|
||||||
|
// Optional. Default: 10
|
||||||
|
MaxRetryCount int
|
||||||
|
|
||||||
|
// currentInterval tracks the current waiting time.
|
||||||
|
//
|
||||||
|
// Optional. Default: 1 * time.Second
|
||||||
|
currentInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfig is the default config for retry.
|
||||||
|
var DefaultConfig = Config{
|
||||||
|
InitialInterval: 1 * time.Second,
|
||||||
|
MaxBackoffTime: 32 * time.Second,
|
||||||
|
Multiplier: 2.0,
|
||||||
|
MaxRetryCount: 10,
|
||||||
|
currentInterval: 1 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// configDefault sets the config values if they are not set.
|
||||||
|
func configDefault(config ...Config) Config {
|
||||||
|
if len(config) == 0 {
|
||||||
|
return DefaultConfig
|
||||||
|
}
|
||||||
|
cfg := config[0]
|
||||||
|
if cfg.InitialInterval == 0 {
|
||||||
|
cfg.InitialInterval = DefaultConfig.InitialInterval
|
||||||
|
}
|
||||||
|
if cfg.MaxBackoffTime == 0 {
|
||||||
|
cfg.MaxBackoffTime = DefaultConfig.MaxBackoffTime
|
||||||
|
}
|
||||||
|
if cfg.Multiplier <= 0 {
|
||||||
|
cfg.Multiplier = DefaultConfig.Multiplier
|
||||||
|
}
|
||||||
|
if cfg.MaxRetryCount <= 0 {
|
||||||
|
cfg.MaxRetryCount = DefaultConfig.MaxRetryCount
|
||||||
|
}
|
||||||
|
if cfg.currentInterval != cfg.InitialInterval {
|
||||||
|
cfg.currentInterval = DefaultConfig.currentInterval
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package retry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"math/big"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExponentialBackoff is a retry mechanism for retrying some calls.
|
||||||
|
type ExponentialBackoff struct {
|
||||||
|
// InitialInterval is the initial time interval for backoff algorithm.
|
||||||
|
InitialInterval time.Duration
|
||||||
|
|
||||||
|
// MaxBackoffTime is the maximum time duration for backoff algorithm. It limits
|
||||||
|
// the maximum sleep time.
|
||||||
|
MaxBackoffTime time.Duration
|
||||||
|
|
||||||
|
// Multiplier is a multiplier number of the backoff algorithm.
|
||||||
|
Multiplier float64
|
||||||
|
|
||||||
|
// MaxRetryCount is the maximum number of retry count.
|
||||||
|
MaxRetryCount int
|
||||||
|
|
||||||
|
// currentInterval tracks the current sleep time.
|
||||||
|
currentInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExponentialBackoff creates a ExponentialBackoff with default values.
|
||||||
|
func NewExponentialBackoff(config ...Config) *ExponentialBackoff {
|
||||||
|
cfg := configDefault(config...)
|
||||||
|
return &ExponentialBackoff{
|
||||||
|
InitialInterval: cfg.InitialInterval,
|
||||||
|
MaxBackoffTime: cfg.MaxBackoffTime,
|
||||||
|
Multiplier: cfg.Multiplier,
|
||||||
|
MaxRetryCount: cfg.MaxRetryCount,
|
||||||
|
currentInterval: cfg.currentInterval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
// generate a random value between [0, 1000)
|
||||||
|
n, err := rand.Int(rand.Reader, big.NewInt(1000))
|
||||||
|
if err != nil {
|
||||||
|
return e.MaxBackoffTime
|
||||||
|
}
|
||||||
|
t := e.currentInterval + (time.Duration(n.Int64()) * time.Millisecond)
|
||||||
|
e.currentInterval = time.Duration(float64(e.currentInterval) * e.Multiplier)
|
||||||
|
if t >= e.MaxBackoffTime {
|
||||||
|
e.currentInterval = e.MaxBackoffTime
|
||||||
|
return e.MaxBackoffTime
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue