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