retry-go 源码阅读

99 阅读4分钟

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
}

这个项目的配置很多都是一个函数,并且参数使用了选项模式,带来很多高度自定义和可扩展性,很值得学习。