Golang 开源之 retry-go 使用指南

  1. 功能测试
  2. 看看实现
    1. 声明 retry 行为
    2. 默认 retry 配置

retry-go 实现非常优美的 retry 库
2022/06/15 仿写实现同样的功能 https://github.com/nickChenyx/retry-go-dummy

功能测试

  1. 定义了两种错误 SomeErr& AnotherErr ,用来测试 retry-go 库的不同函数
  2. 测试一个常见的 HTPP GET 场景
  3. retry.Do(func() error, ...opt) 使用 Do 函数立马开始进行 retry 操作,简单的使用一个 func() error 包括将要被 retry 的代码
  4. 多种 opt 之 retry.DelayType(func(n uint, err error, config *retry.Config) time.Duration) 主要能力是提供每次重试延迟的时间
  5. 多种 opt 之 retry.OnRetry(func(n uint, err error) 主要能力是再触发 retry 操作的时候,前置执行该函数,可用于日志记录等
  6. 多种 opt 之 retry.RetryIf(func(err error) bool 主要能力是判断是否要触发 retry,可以根据不同的错误类型选择是否要进行 retry 操作
  7. 多种 opt 之 retry.Attempts(uint) 主要是设置重试次数,限制重试的时间
  8. 额外功能之 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

这里声明了三部分内容:

  1. Do 函数的执行核心 retryableFunc,一个会返回 error 的简单函数

这里可以看出,待执行的任务会被 func() 包裹,没有额外的入参,但是可以抛出一个 error 作为任务异常的标志。后续重试行为依赖这个 error 信息

  1. Do 函数提供了扩展能力,此处用的是 Options 模式(可以看另一篇 Options-Pattern 文章了解更多)

  2. 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)
    }
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 nickchenyx@gmail.com

Title:Golang 开源之 retry-go 使用指南

Count:1.2k

Author:nickChen

Created At:2022-06-12, 18:59:22

Updated At:2023-05-08, 23:27:10

Url:http://nickchenyx.github.io/2022/06/12/golang-retry-usage/

Copyright: 'Attribution-non-commercial-shared in the same way 4.0' Reprint please keep the original link and author.