-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ 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>
- Loading branch information
Showing
4 changed files
with
360 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
}) | ||
} | ||
} |