retry-go 实现非常优美的 retry 库
2022/06/15 仿写实现同样的功能 https://github.com/nickChenyx/retry-go-dummy
功能测试
- 定义了两种错误
SomeErr& AnotherErr
,用来测试retry-go
库的不同函数 - 测试一个常见的 HTPP GET 场景
retry.Do(func() error, ...opt)
使用Do
函数立马开始进行 retry 操作,简单的使用一个func() error
包括将要被 retry 的代码- 多种 opt 之
retry.DelayType(func(n uint, err error, config *retry.Config) time.Duration)
主要能力是提供每次重试延迟的时间 - 多种 opt 之
retry.OnRetry(func(n uint, err error)
主要能力是再触发 retry 操作的时候,前置执行该函数,可用于日志记录等 - 多种 opt 之
retry.RetryIf(func(err error) bool
主要能力是判断是否要触发 retry,可以根据不同的错误类型选择是否要进行 retry 操作 - 多种 opt 之
retry.Attempts(uint)
主要是设置重试次数,限制重试的时间 - 额外功能之
retry.BackOffDelay(n, err, config)
使用在retry.DelayType(...)
中,可以设置指数级增长的 delay 时间
type SomeErr struct {
err string
retryAfter time.Duration
}
func (err SomeErr) Error() string {
return err.err
}
type AnotherErr struct {
err string
}
func (err AnotherErr) Error() string {
return err.err
}
func TestHttpGet(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello")
}))
defer ts.Close()
var body []byte
var retrySum uint
err := retry.Do(
func() error {
resp, err := http.Get(ts.URL)
ri := rand.Intn(10)
if ri < 3 {
err = SomeErr{
err: "some err",
retryAfter: time.Second,
}
} else if ri < 6 {
err = AnotherErr{
err: "another err",
}
}
if err == nil {
defer func() {
if err := resp.Body.Close(); err != nil {
panic(err)
}
}()
body, err = ioutil.ReadAll(resp.Body)
}
return err
},
retry.DelayType(func(n uint, err error, config *retry.Config) time.Duration {
switch e := err.(type) {
case SomeErr:
return e.retryAfter
case AnotherErr:
return retry.BackOffDelay(n, err, config)
default:
return time.Second
}
}),
retry.OnRetry(func(n uint, err error) { retrySum += 1 }),
retry.RetryIf(func(err error) bool {
switch err.(type) {
case SomeErr, AnotherErr:
return true
default:
return false
}
}),
retry.Attempts(3),
)
assert.NoError(t, err)
assert.NotEmpty(t, body)
}
看看实现
声明 retry 行为
type RetryableFunc func() error
func Do(retryableFunc RetryableFunc, opts ...Option) error
这里声明了三部分内容:
- Do 函数的执行核心 retryableFunc,一个会返回 error 的简单函数
这里可以看出,待执行的任务会被 func() 包裹,没有额外的入参,但是可以抛出一个 error 作为任务异常的标志。后续重试行为依赖这个 error 信息
Do 函数提供了扩展能力,此处用的是 Options 模式(可以看另一篇 Options-Pattern 文章了解更多)
Do 函数返回了 error,此处描述的是整个 retry 结束过后,任务尚未成功,需要有一个结果
默认 retry 配置
从默认配置中探索
retry-go
库的设计思路
func newDefaultRetryConfig() *Config {
return &Config{
attempts: uint(10),
delay: 100 * time.Millisecond,
maxJitter: 100 * time.Millisecond,
onRetry: func(n uint, err error) {},
retryIf: IsRecoverable,
delayType: CombineDelay(BackOffDelay, RandomDelay),
lastErrorOnly: false,
context: context.Background(),
}
}
当 Do 函数的 options 为空时,该配置就是实际执行 Do 函数的运行时配置了。罗列一下配置项:
- attempts -> 重试次数,默认 10 次,使用 uint 限制重试次数大于 0
- delay -> 重试的间隔时间
- maxJitter -> RandomDelay 函数的 delay 最大值设置,随机范围在
[0, maxJitter)
之间 - onRetry -> 这是一个空函数,默认在每次重试前无动作
- lastErrorOnly -> 表示是否只收集最后一个 error,反之则收集全部任务产生的 error 信息
- context -> 设置一个无用的 context,但是可以传递一个具有超时配置的 context 进来,这样可以设置整个 retry 的全局超时时间
- retryIf -> 这是判断是否要进行重试的函数,
IsRecoverable
作用如下:
func IsRecoverable(err error) bool {
_, isUnrecoverable := err.(unrecoverableError)
return !isUnrecoverable
}
可以看到这里当错误 err 是 unrecoverableError
时,就不会重试。也就是 retry-go 自定义了一个不可恢复的异常,同时提供了 Unrecoverable
函数封装一个 unrecoverableError
。如果用户知道了这个特性,就可以利用起来,从而中断重试。下面是 unrecoverableError
的定义:
type unrecoverableError struct {
error
}
func Unrecoverable(err error) error {
return unrecoverableError{err}
}
- delayType -> 设置延时时间的函数,组合了 BackOffDelay 指数级增长的延时和 RandomDelay 随机延时,从而达到总体上指数级增长但是具体数值又有波动的延时效果
// CombineDelay(BackOffDelay, RandomDelay),
func CombineDelay(delays ...DelayTypeFunc) DelayTypeFunc {
const maxInt64 = uint64(math.MaxInt64)
return func(n uint, err error, config *Config) time.Duration {
var total uint64
for _, delay := range delays {
total += uint64(delay(n, err, config))
if total > maxInt64 {
total = maxInt64
}
}
return time.Duration(total)
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 [email protected]