阅读视图

发现新文章,点击刷新页面。
🔲 ☆

Golang 开源之 retry-go 使用指南

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() errorfunc 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)    }}
🔲 ⭐

Golang 开源之 tparse 使用指南

tparse 分析和归纳 go test 输出的命令行工具

安装

go install github.com/mfridman/tparse@latest

tparse 能做什么

仅使用 go test -v 来显示测试结果,只是罗列了所有测试集合,没有归纳总结,不便于一眼明了。

➜  go test -race ./testtparse -v=== RUN   Test_sum=== RUN   Test_sum/return_2--- PASS: Test_sum (0.00s)    --- PASS: Test_sum/return_2 (0.00s)=== RUN   Test_sub=== RUN   Test_sub/return_0--- PASS: Test_sub (0.00s)    --- PASS: Test_sub/return_0 (0.00s)PASSok      code.byted.org/demo/testtparse  0.026s

使用 tparse 分析归纳 go test 的报告结论

➜  go test -race ./testtparse -json -cover | tparse -all+--------+---------+-------------------+------------+| STATUS | ELAPSED |       TEST        |  PACKAGE   |+--------+---------+-------------------+------------+| PASS   |    0.00 | Test_sum          | testtparse || PASS   |    0.00 | Test_sum/return_2 | testtparse || PASS   |    0.00 | Test_sub          | testtparse || PASS   |    0.00 | Test_sub/return_0 | testtparse |+--------+---------+-------------------+------------++--------+---------+--------------------------------+--------+------+------+------+| STATUS | ELAPSED |            PACKAGE             | COVER  | PASS | FAIL | SKIP |+--------+---------+--------------------------------+--------+------+------+------+| PASS   | 0.04s   | code.byted.org/demo/testtparse | 100.0% |    4 |    0 |    0 |+--------+---------+--------------------------------+--------+------+------+------+
🔲 ⭐

Golang 开源之 semgroup 使用指南

semgroup 用于并发执行一组任务,提供限制协程数量、同步等待任务执行完成及错误信息传递的能力。不同于 errgroup ,semgroup 会执行所有任务,且收集任务产生的 error 信息。在全部任务执行完成后,将收集的 error 信息返回给开发者。

使用方法

快速使用请看官网的使用方法,此处不赘述

示例 TestDemo 中用到了如下功能,可以按需使用

  1. context 定义了超时时间,可以限制整个任务的执行时间

  2. 任务并行执行,且向外丢出了自定义 error

  3. 使用 errors.Iserrors.As 处理 error

  4. 使用了拓展的 ExportMultiError 函数将所有内部错误全部输出(附带错误中包含了关键信息如 taskId,可以在后续的程序中使用)

    func TestDemo(t *testing.T) { // 可控制任务整体的执行时间 timedCtx, cancel := context.WithTimeout(context.Background(), time.Hour) defer cancel() g := NewGroup(timedCtx, 1) fe := fooErr{taskId: 1, msg: "__foo__"} g.Go(func() error { return fe }) g.Go(func() error { return os.ErrClosed }) g.Go(func() error { return nil }) err := g.Wait() if err == nil {     t.Fatalf("g.Wait() should return an error") } var (     fbe fooErr ) // Is 可用于抛出错误的判断 if !errors.Is(err, fe) {     t.Errorf("error should be equal fooErr") } // Is 可用于抛出错误的判断 if !errors.Is(err, os.ErrClosed) {     t.Errorf("error should be equal os.ErrClosed") } // As 可以将错误检索出来 if !errors.As(err, &fbe) {     t.Error("error should be matched foobarErr") } // 通过上面 As 将错误信息取出,应该可以拿到失败的任务 id if fbe.taskId != 1 {     t.Error("fooErr task id should be 1") } me, isMultiError := ExportMultiError(err) if !isMultiError {     t.Error("err should be a multiError") } for _, e := range me {     t.Logf("range me: %v", e)     var fe fooErr     if errors.As(e, &fe) && fe.taskId != 1 {         t.Error("variable t.taskId should be 1")     } }}

补充上文使用到的 ExportMultiError 函数。

func ExportMultiError(err error) ([]error, bool) {    if err == nil {        return nil, false    }    switch err.(type) {    case multiError:        return err.(multiError), true    default:        return []error{err}, false    }}

思考

并行处理失败的任务有无必要返回具体的信息?原库中仅透出了 errors.Iserrors.As 两个函数供处理异常,实际上会不会在 error 中透出具体的任务信息,供错误失败时使用呢?
基于这种思考,先提供了一个 ExportmultiError 的函数解决这个问题。有无更好的方式,或者业界更通用的处理方案?

❌