add retry codes

This commit is contained in:
Edward 2020-05-03 10:52:05 +08:00
parent cf68abae34
commit 76dfaf7a59
7 changed files with 490 additions and 1 deletions

View File

@ -1,6 +1,6 @@
# Go语言设计模式示例集合(Go Patterns Examples)
**包括了[go-patterns](https://github.com/tmrts/go-patterns) 和[golang-design-pattern](https://github.com/senghoo/golang-design-pattern)中的全部模式**
**包括了[go-patterns](https://github.com/tmrts/go-patterns) 和[golang-design-pattern](https://github.com/senghoo/golang-design-pattern)中的全部模式**
目前包括了**39种Go中常用的、面向工程化和最佳实践的模式/套路**自然也包含常见的23种设计模式,重点是这里全部是例子、通俗易懂,甚至每个模式下的例子,改一下名字,稍微再增加几行代码就可以直接用在项目和工程中了。
@ -80,6 +80,8 @@
[design_pattern](http://c.biancheng.net/design_pattern)
[go-resiliency](https://github.com/eapache/go-resiliency)
## 更多

View File

@ -0,0 +1,24 @@
package retrier
import "time"
// ConstantBackoff generates a simple back-off strategy of retrying 'n' times, and waiting 'amount' time after each one.
func ConstantBackoff(n int, amount time.Duration) []time.Duration {
ret := make([]time.Duration, n)
for i := range ret {
ret[i] = amount
}
return ret
}
// ExponentialBackoff generates a simple back-off strategy of retrying 'n' times, and doubling the amount of
// time waited after each one.
func ExponentialBackoff(n int, initialAmount time.Duration) []time.Duration {
ret := make([]time.Duration, n)
next := initialAmount
for i := range ret {
ret[i] = next
next *= 2
}
return ret
}

View File

@ -0,0 +1,55 @@
package retrier
import (
"testing"
"time"
)
func TestConstantBackoff(t *testing.T) {
b := ConstantBackoff(1, 10*time.Millisecond)
if len(b) != 1 {
t.Error("incorrect length")
}
for i := range b {
if b[i] != 10*time.Millisecond {
t.Error("incorrect value at", i)
}
}
b = ConstantBackoff(10, 250*time.Hour)
if len(b) != 10 {
t.Error("incorrect length")
}
for i := range b {
if b[i] != 250*time.Hour {
t.Error("incorrect value at", i)
}
}
}
func TestExponentialBackoff(t *testing.T) {
b := ExponentialBackoff(1, 10*time.Millisecond)
if len(b) != 1 {
t.Error("incorrect length")
}
if b[0] != 10*time.Millisecond {
t.Error("incorrect value")
}
b = ExponentialBackoff(4, 1*time.Minute)
if len(b) != 4 {
t.Error("incorrect length")
}
if b[0] != 1*time.Minute {
t.Error("incorrect value")
}
if b[1] != 2*time.Minute {
t.Error("incorrect value")
}
if b[2] != 4*time.Minute {
t.Error("incorrect value")
}
if b[3] != 8*time.Minute {
t.Error("incorrect value")
}
}

View File

@ -0,0 +1,66 @@
package retrier
// Action is the type returned by a Classifier to indicate how the Retrier should proceed.
type Action int
const (
Succeed Action = iota // Succeed indicates the Retrier should treat this value as a success.
Fail // Fail indicates the Retrier should treat this value as a hard failure and not retry.
Retry // Retry indicates the Retrier should treat this value as a soft failure and retry.
)
// Classifier is the interface implemented by anything that can classify Errors for a Retrier.
type Classifier interface {
Classify(error) Action
}
// DefaultClassifier classifies errors in the simplest way possible. If
// the error is nil, it returns Succeed, otherwise it returns Retry.
type DefaultClassifier struct{}
// Classify implements the Classifier interface.
func (c DefaultClassifier) Classify(err error) Action {
if err == nil {
return Succeed
}
return Retry
}
// WhitelistClassifier classifies errors based on a whitelist. If the error is nil, it
// returns Succeed; if the error is in the whitelist, it returns Retry; otherwise, it returns Fail.
type WhitelistClassifier []error
// Classify implements the Classifier interface.
func (list WhitelistClassifier) Classify(err error) Action {
if err == nil {
return Succeed
}
for _, pass := range list {
if err == pass {
return Retry
}
}
return Fail
}
// BlacklistClassifier classifies errors based on a blacklist. If the error is nil, it
// returns Succeed; if the error is in the blacklist, it returns Fail; otherwise, it returns Retry.
type BlacklistClassifier []error
// Classify implements the Classifier interface.
func (list BlacklistClassifier) Classify(err error) Action {
if err == nil {
return Succeed
}
for _, pass := range list {
if err == pass {
return Fail
}
}
return Retry
}

View File

@ -0,0 +1,66 @@
package retrier
import (
"errors"
"testing"
)
var (
errFoo = errors.New("FOO")
errBar = errors.New("BAR")
errBaz = errors.New("BAZ")
)
func TestDefaultClassifier(t *testing.T) {
c := DefaultClassifier{}
if c.Classify(nil) != Succeed {
t.Error("default misclassified nil")
}
if c.Classify(errFoo) != Retry {
t.Error("default misclassified foo")
}
if c.Classify(errBar) != Retry {
t.Error("default misclassified bar")
}
if c.Classify(errBaz) != Retry {
t.Error("default misclassified baz")
}
}
func TestWhitelistClassifier(t *testing.T) {
c := WhitelistClassifier{errFoo, errBar}
if c.Classify(nil) != Succeed {
t.Error("whitelist misclassified nil")
}
if c.Classify(errFoo) != Retry {
t.Error("whitelist misclassified foo")
}
if c.Classify(errBar) != Retry {
t.Error("whitelist misclassified bar")
}
if c.Classify(errBaz) != Fail {
t.Error("whitelist misclassified baz")
}
}
func TestBlacklistClassifier(t *testing.T) {
c := BlacklistClassifier{errBar}
if c.Classify(nil) != Succeed {
t.Error("blacklist misclassified nil")
}
if c.Classify(errFoo) != Retry {
t.Error("blacklist misclassified foo")
}
if c.Classify(errBar) != Fail {
t.Error("blacklist misclassified bar")
}
if c.Classify(errBaz) != Retry {
t.Error("blacklist misclassified baz")
}
}

97
gomore/retrier/retrier.go Normal file
View File

@ -0,0 +1,97 @@
// Package retrier implements the "retriable" resiliency pattern for Go.
package retrier
import (
"context"
"math/rand"
"sync"
"time"
)
// Retrier implements the "retriable" resiliency pattern, abstracting out the process of retrying a failed action
// a certain number of times with an optional back-off between each retry.
type Retrier struct {
backoff []time.Duration
class Classifier
jitter float64
rand *rand.Rand
randMu sync.Mutex
}
// New constructs a Retrier with the given backoff pattern and classifier. The length of the backoff pattern
// indicates how many times an action will be retried, and the value at each index indicates the amount of time
// waited before each subsequent retry. The classifier is used to determine which errors should be retried and
// which should cause the retrier to fail fast. The DefaultClassifier is used if nil is passed.
func New(backoff []time.Duration, class Classifier) *Retrier {
if class == nil {
class = DefaultClassifier{}
}
return &Retrier{
backoff: backoff,
class: class,
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
}
}
// Run executes the given work function by executing RunCtx without context.Context.
func (r *Retrier) Run(work func() error) error {
return r.RunCtx(context.Background(), func(ctx context.Context) error {
// never use ctx
return work()
})
}
// RunCtx executes the given work function, then classifies its return value based on the classifier used
// to construct the Retrier. If the result is Succeed or Fail, the return value of the work function is
// returned to the caller. If the result is Retry, then Run sleeps according to the its backoff policy
// before retrying. If the total number of retries is exceeded then the return value of the work function
// is returned to the caller regardless.
func (r *Retrier) RunCtx(ctx context.Context, work func(ctx context.Context) error) error {
retries := 0
for {
ret := work(ctx)
switch r.class.Classify(ret) {
case Succeed, Fail:
return ret
case Retry:
if retries >= len(r.backoff) {
return ret
}
timeout := time.After(r.calcSleep(retries))
if err := r.sleep(ctx, timeout); err != nil {
return err
}
retries++
}
}
}
func (r *Retrier) sleep(ctx context.Context, t <-chan time.Time) error {
select {
case <-t:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func (r *Retrier) calcSleep(i int) time.Duration {
// lock unsafe rand prng
r.randMu.Lock()
defer r.randMu.Unlock()
// take a random float in the range (-r.jitter, +r.jitter) and multiply it by the base amount
return r.backoff[i] + time.Duration(((r.rand.Float64()*2)-1)*r.jitter*float64(r.backoff[i]))
}
// SetJitter sets the amount of jitter on each back-off to a factor between 0.0 and 1.0 (values outside this range
// are silently ignored). When a retry occurs, the back-off is adjusted by a random amount up to this value.
func (r *Retrier) SetJitter(jit float64) {
if jit < 0 || jit > 1 {
return
}
r.jitter = jit
}

View File

@ -0,0 +1,179 @@
package retrier
import (
"context"
"errors"
"testing"
"time"
)
var i int
func genWork(returns []error) func() error {
i = 0
return func() error {
i++
if i > len(returns) {
return nil
}
return returns[i-1]
}
}
func genWorkWithCtx() func(ctx context.Context) error {
i = 0
return func(ctx context.Context) error {
select {
case <-ctx.Done():
return errFoo
default:
i++
}
return nil
}
}
func TestRetrier(t *testing.T) {
r := New([]time.Duration{0, 10 * time.Millisecond}, WhitelistClassifier{errFoo})
err := r.Run(genWork([]error{errFoo, errFoo}))
if err != nil {
t.Error(err)
}
if i != 3 {
t.Error("run wrong number of times")
}
err = r.Run(genWork([]error{errFoo, errBar}))
if err != errBar {
t.Error(err)
}
if i != 2 {
t.Error("run wrong number of times")
}
err = r.Run(genWork([]error{errBar, errBaz}))
if err != errBar {
t.Error(err)
}
if i != 1 {
t.Error("run wrong number of times")
}
}
func TestRetrierCtx(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
r := New([]time.Duration{0, 10 * time.Millisecond}, WhitelistClassifier{})
err := r.RunCtx(ctx, genWorkWithCtx())
if err != nil {
t.Error(err)
}
if i != 1 {
t.Error("run wrong number of times")
}
cancel()
err = r.RunCtx(ctx, genWorkWithCtx())
if err != errFoo {
t.Error("context must be cancelled")
}
if i != 0 {
t.Error("run wrong number of times")
}
}
func TestRetrierNone(t *testing.T) {
r := New(nil, nil)
i = 0
err := r.Run(func() error {
i++
return errFoo
})
if err != errFoo {
t.Error(err)
}
if i != 1 {
t.Error("run wrong number of times")
}
i = 0
err = r.Run(func() error {
i++
return nil
})
if err != nil {
t.Error(err)
}
if i != 1 {
t.Error("run wrong number of times")
}
}
func TestRetrierJitter(t *testing.T) {
r := New([]time.Duration{0, 10 * time.Millisecond, 4 * time.Hour}, nil)
if r.calcSleep(0) != 0 {
t.Error("Incorrect sleep calculated")
}
if r.calcSleep(1) != 10*time.Millisecond {
t.Error("Incorrect sleep calculated")
}
if r.calcSleep(2) != 4*time.Hour {
t.Error("Incorrect sleep calculated")
}
r.SetJitter(0.25)
for i := 0; i < 20; i++ {
if r.calcSleep(0) != 0 {
t.Error("Incorrect sleep calculated")
}
slp := r.calcSleep(1)
if slp < 7500*time.Microsecond || slp > 12500*time.Microsecond {
t.Error("Incorrect sleep calculated")
}
slp = r.calcSleep(2)
if slp < 3*time.Hour || slp > 5*time.Hour {
t.Error("Incorrect sleep calculated")
}
}
r.SetJitter(-1)
if r.jitter != 0.25 {
t.Error("Invalid jitter value accepted")
}
r.SetJitter(2)
if r.jitter != 0.25 {
t.Error("Invalid jitter value accepted")
}
}
func TestRetrierThreadSafety(t *testing.T) {
r := New([]time.Duration{0}, nil)
for i := 0; i < 2; i++ {
go func() {
r.Run(func() error {
return errors.New("error")
})
}()
}
}
func ExampleRetrier() {
r := New(ConstantBackoff(3, 100*time.Millisecond), nil)
err := r.Run(func() error {
// do some work
return nil
})
if err != nil {
// handle the case where the work failed three times
}
}