Golang 开源之 retry-go 使用指南
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() errorfunc 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) }}