retry-go 源码阅读
主要用于对部分逻辑封装重试,如 http/rpc 请求,业务逻辑等
主要封装包括:
- 重试次数
- 重试间隔
- 重试条件
- 退避(根据重试次数调整重试间隔)
README 提供的使用例子
// HTTP GET with retry:
url := "http://example.com"
var body []byte
err := retry.Do(
func() error {
// TODO:
return nil
},
)
if err != nil {
// handle error
}
fmt.Println(string(body))
// HTTP GET with retry with data:
url := "http://example.com"
body, err := retry.DoWithData(
func() ([]byte, error) {
// TODO:
var body []byte
return body, nil
},
)
if err != nil {
// handle error
}
fmt.Println(string(body))
可以看出有两种使用可供选择:
- 只返回执行是否发生错误,
- 带了数据返回和错误信息。
从 Do 函数开始分析,
func Do(retryableFunc RetryableFunc, opts ...Option) error
首先分析参数,
第一个参数是自定义的类型 RetryableFunc
第二个参数是选项模式,类型为 Option
// Function signature of retryable function
// 可重试函数的函数签名
type RetryableFunc func() error
// Option represents an option for retry.
// 选项表示重试的选项。
type Option func(*Config)
分析函数的内部逻辑
- 首先根据第一个参数,包装了一个函数,这个函数的返回值,第一个返回值返回 nil
- 调用 DowithData 函数,从这里可以看出,Do 函数最终也是调用的 DoWithData 函数
retryableFuncWithData := func() (any, error) {
return nil, retryableFunc()
}
_, err := DoWithData(retryableFuncWithData, opts...)
return err
那直接跳到 DowithData 函数
func DoWithData[T any](retryableFunc RetryableFuncWithData[T], opts ...Option) (T, error) {
var n uint
var emptyT T
// default
// 初始化默认配置
config := newDefaultRetryConfig()
// apply opts
// 如果你传入了自定义的配置,从这里加载
for _, opt := range opts {
opt(config)
}
// 判断 context 上下文是否被取消或者因为超时被关闭,是的话,立即退出
if err := config.context.Err(); err != nil {
return emptyT, err
}
// Setting attempts to 0 means we'll retry until we succeed
var lastErr error
// !!!如果重试次数设置为0,将会一直重试,直到成功
if config.attempts == 0 {
for {
// 执行重试函数
t, err := retryableFunc()
if err == nil {
return t, nil
}
// 判断是否是可恢复的错误,如果是不可能恢复的,直接退出
if !IsRecoverable(err) {
return emptyT, err
}
// 判断该错误是否需要重试
if !config.retryIf(err) {
return emptyT, err
}
// 记录最后出现的错误
lastErr = err
// 调用配置中的 OnRetry 函数,并传递当前重试次数 n 和发生的错误 err
// 目的是在每次重试尝试后,执行用户定义的回调函数
config.onRetry(n, err)
// 重试次数 +1
n++
// 监听两个事件
// - 一个是每次重试间隔的时长:走到这个这个监听事件时,没做啥处理,继续重试操作
// - 另一个是上下文是否被取消或者到达截止时间,返回响应的错误
select {
case <-config.timer.After(delay(config, n, err)):
case <-config.context.Done():
// 判断是否配置了要包装最后一次 context 错误
if config.wrapContextErrorWithLastError {
return emptyT, Error{config.context.Err(), lastErr}
}
return emptyT, config.context.Err()
}
}
}
// 错误日志切片
errorLog := Error{}
// 复制一份,在配置中设置在给定的“err”中执行导致的重试计数 给定“err”的重试也计入总重试次数。
// 如果给定的重试次数用尽,重试将停止
// 这里只所以需要复制一份是因为下面的代码会对其进行修改
attemptsForError := make(map[error]uint, len(config.attemptsForError))
for err, attempts := range config.attemptsForError {
attemptsForError[err] = attempts
}
shouldRetry := true
for shouldRetry {
t, err := retryableFunc()
if err == nil {
return t, nil
}
// 将错误添加到错误日志切片中
errorLog = append(errorLog, unpackUnrecoverable(err))
if !config.retryIf(err) {
break
}
config.onRetry(n, err)
for errToCheck, attempts := range attemptsForError {
// 如果是在检查的 err
if errors.Is(err, errToCheck) {
// 减少给定的 err 中的重试次数
attempts--
// 记录下来,因为是 for 循环,所以即使无序,也能检查到 attempts 最小的 err
// 不过感觉是不是有点多余,可以新增两个字段,分别记录给定 err 和 给定 err 的最大重试次数
// 这样子就不用每次都需要 for 循环一遍了
attemptsForError[errToCheck] = attempts
// 判断 shouldRetry 是否为 true 并且给定 err 的最大重试次数是否大于 0
shouldRetry = shouldRetry && attempts > 0
}
}
// if this is last attempt - don't wait
// 如果是最后一次尝试,直接退出当前 for 循环
if n == config.attempts-1 {
break
}
select {
// 每次重试事件的延迟时间
case <-config.timer.After(delay(config, n, err)):
// 是否主动取消了(即主动调用 cancle)函数,或者超过截至时间
case <-config.context.Done():
// 如果配置了只需要返回最后一次日志
if config.lastErrorOnly {
return emptyT, config.context.Err()
}
// 否则,将错误日志加入到 errLog 中
return emptyT, append(errorLog, config.context.Err())
}
// 重试次数 +1
n++
// 判断是否需要继续重试
shouldRetry = shouldRetry && n < config.attempts
}
if config.lastErrorOnly {
return emptyT, errorLog.Unwrap()
}
return emptyT, errorLog
}
这个项目的配置很多都是一个函数,并且参数使用了选项模式,带来很多高度自定义和可扩展性,很值得学习。