golang极简retry重试框架实现

设计目标:

  • 支持无状态函数重试
  • 支持带返回数据的函数重试
  • 支持指数退避 + 抖动
  • 支持收集每次重试的error, 并返回

用户自定义配置Config, 默认重试次数3次, 间隔1s, 最大间隔10s,

package examples

import "time"

type Config struct {
    attempts int
    delay    time.Duration
    maxDelay time.Duration
}

func newDefaultConfig() *Config {
    return &Config{
        attempts: 3,
        delay:    time.Second,
        maxDelay: time.Second * 10,
    }
}

type Option func(*Config)

// 尝试次数
func Attempts(attempts int) Option {
    return func(c *Config) {
        c.attempts = attempts
    }
}

// Dealy 间隔
func Delay(delay time.Duration) Option {
    return func(c *Config) {
        c.delay = delay
    }
}

func MaxDelay(delay time.Duration) Option {
    return func(c *Config) {
        c.maxDelay = delay
    }
}

自定义重试Error : 收集每次重试的error, 并统一返回

type Error []error

func (e Error) Error() string {
    logWithNumber := make([]string, len(e))
    for i, l := range e {
        if l != nil {
            logWithNumber[i] = fmt.Sprintf("#%d: %s", i+1, l.Error())
        }
    }

    return fmt.Sprintf("All attempts fail:\n%s", strings.Join(logWithNumber, "\n"))
}

重试执行函数实现(默认开启抖动和指数退避, 不支持带返回数据的函数执行):


func Do(ctx context.Context, fn func() error, opts ...Option) error {
    var errs Error
    // default
    c := newDefaultConfig()
    // apply opts
    for _, opt := range opts {
        opt(c)
    }

    backoff := c.delay

    for i := 0; i < c.attempts; i++ {
        err := fn()
        if err == nil {
            return nil
        }
        errs = append(errs, err)

        if i == c.attempts-1 {
            return errs
        }
        // 指数退避 + 抖动
        jitter := time.Duration(rand.Int63n(int64(backoff)))
        sleep := backoff + jitter
        timer := time.NewTimer(sleep)
        select {
        case <-ctx.Done():
            timer.Stop()
            return ctx.Err()
        case <-timer.C:
        }
        backoff *= 2
        if backoff > c.maxDelay {
            backoff = c.maxDelay
        }
    }
    return errs
}

测试:

package examples

import (
    "context"
    "errors"
    "fmt"
    "testing"
    "time"
)

func TestDo(t *testing.T) {
    ctx := context.Background()
    err := Do(ctx,
        func() error {
            return callRemote() // 可能失败
        },
        Attempts(5),
        Delay(time.Second*1),
        MaxDelay(time.Second*10),
    )
    if err != nil {
        fmt.Println(err.Error())
    }
}

func callRemote() error {
    fmt.Println("do error")
    return errors.New("call Remote error")
    //return nil

}


测试执行结果输出:


image.png

上述实现存在以下问题:

  • 不支持带返回值的函数重试
  • 指数退避的策略不支持用户自选

新增DoWithData函数, 并修改Do方法实现:

type RetryableFuncWithData[T any] func() (T, error)

func DoWithData[T any](ctx context.Context, retryableFunc RetryableFuncWithData[T], opts ...Option) (T, error) {
    var emptyT T
    var errs Error
    c := newDefaultConfig()
    for _, opt := range opts {
        opt(c)
    }
    backoff := c.delay

    for i := 0; i < c.attempts; i++ {
        t, err := retryableFunc()
        if err == nil {
            return t, nil
        }
        errs = append(errs, err)

        if i == c.attempts-1 {
            return emptyT, errs
        }
        sleep := c.delay
        //抖动
        jitter := time.Duration(rand.Int63n(int64(backoff)))
        sleep = backoff + jitter

        timer := time.NewTimer(sleep)
        select {
        case <-ctx.Done():
            timer.Stop()
            return emptyT, ctx.Err()
        case <-timer.C:
        }
        // 指数退避
        if c.backoff {
            backoff *= 2
            if backoff > c.maxDelay {
                backoff = c.maxDelay
            }
        }

    }
    return emptyT, errs
}

func Do(ctx context.Context, fn func() error, opts ...Option) error {
    c := newDefaultConfig()
    for _, opt := range opts {
        opt(c)
    }
    retryableFuncWithData := func() (any, error) {
        return nil, fn()
    }
    _, err := DoWithData(ctx, retryableFuncWithData, opts...)
    return err
}
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • """1.个性化消息: 将用户的姓名存到一个变量中,并向该用户显示一条消息。显示的消息应非常简单,如“Hello ...
    她即我命阅读 4,057评论 0 6
  • 为了让我有一个更快速、更精彩、更辉煌的成长,我将开始这段刻骨铭心的自我蜕变之旅!从今天开始,我将每天坚持阅...
    李薇帆阅读 2,190评论 1 4
  • 似乎最近一直都在路上,每次出来走的时候感受都会很不一样。 1、感恩一直遇到好心人,很幸运。在路上总是...
    时间里的花Lily阅读 1,617评论 1 3
  • 1、expected an indented block 冒号后面是要写上一定的内容的(新手容易遗忘这一点); 缩...
    庵下桃花仙阅读 725评论 0 2
  • 一、工具箱(多种工具共用一个快捷键的可同时按【Shift】加此快捷键选取)矩形、椭圆选框工具 【M】移动工具 【V...
    墨雅丫阅读 1,121评论 0 0

友情链接更多精彩内容