buildkite/roko

GitHub: buildkite/roko

Roko 是一个轻量级、可配置的 Go 语言重试库,帮助开发者优雅地处理可能失败的操作并灵活控制重试行为。

Stars: 9 | Forks: 2

# Roko [![Go Reference](https://pkg.go.dev/badge/github.com/buildkite/roko.svg)](https://pkg.go.dev/github.com/buildkite/roko) [![构建状态](https://badge.buildkite.com/f546789abb4ecc61654da783bdcc05965789a4834958b178ec.svg)](https://buildkite.com/buildkite/roko) 一个为 Go 打造的轻量级、可配置且易于使用的重试库 ## 安装 要安装,请运行 ``` go get -u github.com/buildkite/roko ``` 这会将 Roko 添加到你的 go.mod 文件中,并使其可以在你的项目中使用。 ## 用法 Roko 允许你配置应用程序应如何响应可能会失败的操作。它的核心接口是 **Retrier**,它允许你告诉应用程序在何种情况下以及如何重试操作。 假设我们要执行一些操作: ``` func canFail() error { // ... } ``` 并且如果它失败,我们希望它每 5 秒重试一次,并在 3 次尝试后放弃。为此,我们可以配置一个 retrier,然后使用 `roko.Retrier.Do()` 函数执行我们的操作: ``` r := roko.NewRetrier( roko.WithMaxAttempts(3), // Only try 3 times, then give up roko.WithStrategy(roko.Constant(5 * time.Second)), // Wait 5 seconds between attempts ) err := r.Do(func(r *roko.Retrier) error { return canFail() }) ``` 在这种情况下,我们将尝试运行 `canFail` 函数,如果它返回错误,我们将等待 5 秒,然后再试一次。如果 `canFail` 在达到最大尝试次数后返回错误,`r.Do` 将返回该错误。如果 `canFail` 成功(即它没有返回错误),`r.Do` 将返回 nil。 ### 提前放弃 有时,你的操作返回的错误可能是不可恢复的,因此我们不想重试它。在这种情况下,我们可以使用 `roko.Retrier.Break` 函数。`Break()` 指示 retrier 在本次运行后停止——请注意,它**并不会立即停止操作**。 ``` r := roko.NewRetrier( roko.WithMaxAttempts(3), // Only try 3 times, then give up roko.WithStrategy(roko.Constant(5 * time.Second)), // Wait 5 seconds between attempts ) err := r.Do(func(r *roko.Retrier) error { err := canFail() if err.Is(errorUnrecoverable) { r.Break() // Give up, we can't recover from this error return err // We still need to return from this function, Break() doesn't halt this callback // return nil would be appropriate too, if we don't want to handle this error further } }) ``` 在这个示例中,如果 `canFail()` 返回不可恢复的错误,则 `r.Do()` 调用返回的结果就是该不可恢复的错误。 ### 永不放弃! 或者(也可以同时!),你可能希望你的 retrier 永不放弃,并不断尝试直到最终成功。Roko 可以通过 `TryForever()` 选项来实现这一点。 ``` r := roko.NewRetrier( roko.TryForever(), roko.WithStrategy(roko.Constant(5 * time.Second)), // Wait 5 seconds between attempts ) err := r.Do(func(r *roko.Retrier) error { return canFail() }) ``` 这将尝试执行 `canFail()`,直到最终成功。 请注意,上面提到的 `Break()` 方法在启用 `TryForever()` 时仍然有效——这允许你在遇到不可恢复的错误时仍然可以退出。 ### Jitter 为了避免惊群效应,可以配置 roko 在其重试时间间隔计算中添加 jitter。当使用 jitter 时,间隔计算器会在每次计算时增加一段最多一秒的随机时间。 ``` r := roko.NewRetrier( roko.WithMaxAttempts(3), // Only try 3 times, then give up roko.WithJitter() // Add up to a second of jitter roko.WithStrategy(roko.Constant(5 * time.Second)), // Wait 5ish seconds between attempts ) err := r.Do(func(r *roko.Retrier) error { return canFail() }) ``` 在这个示例中,一切都与第一个示例相同,但 retrier 不再总是等待 5 秒,而是等待 5 到 6 秒之间的随机时间间隔。这有助于减少资源争用。 ### 指数退避 如果你不喜欢恒定的重试策略,可以将 roko 配置为根据目前为止发生的尝试次数改用指数退避策略: ``` r := roko.NewRetrier( roko.WithMaxAttempts(5), // Only try 5 times, then give up roko.WithStrategy(roko.Exponential(2, 0)), // Wait (2 ^ attemptCount) + 0 seconds between attempts ) err := r.Do(func(r *roko.Retrier) error { return canFail() }) ``` 在这种情况下,retrier 在两次尝试之间等待的时间取决于已经过去的尝试次数——第一次等待时间为 2^0 == 1 秒,然后是 2^1 == 2 秒,接着是 2^2 == 4 秒,依此类推。 `roko.Exponential()` 方法的第二个参数是常数调整值——roko 会将此数字添加到计算出的指数中。 ### 使用自定义策略 如果 roko 内置的两种重试策略(`Constant` 和 `Exponential`)不能满足你的需求,你可以定义自己的策略——`roko.WithStrategy` 方法将接受任何返回 `(roko.Strategy, string)` 元组的对象。例如,我们可以实现一个自定义的 `Linear` 策略,将尝试次数乘以一个固定的数字: ``` func Linear(gradient float64, yIntercept float64) (roko.Strategy, string) { return func(r *roko.Retrier) time.Duration { return time.Duration(((gradient * float64(r.AttemptCount())) + yIntercept)) * time.Second }, "linear" // The second element of the return tuple is the name of the strategy } err := roko.NewRetrier( roko.WithMaxAttempts(3), // Only try 3 times, then give up roko.WithStrategy(Linear(0.5, 5.0)), // Wait 5 seconds + half of the attempt count seconds ).Do(func(r *roko.Retrier) error { return canFail() }) ``` ### 手动设置下一个时间间隔 有时你只有在每次尝试之后才知道所需的时间间隔,例如受速率限制的 API 可能包含 `Retry-After` header。对于这些情况,可以使用 `SetNextInterval(time.Duration)` 方法。它将仅应用于下一个时间间隔,然后会恢复为配置的策略,除非在下一次尝试时再次调用它。 ``` // manually specify interval during each try, defaulting to 10 seconds roko.NewRetrier( roko.WithStrategy(Constant(10 * time.Second)), roko.WithMaxAttempts(10), ).Do(func(r *roko.Retrier) error { response := apiCall() // may be rate limited if err := response.HTTPError(); err != nil { if response.Status == HttpTooManyRequests { if retryAfter, err := strconv.Atoi(response.Header("Retry-After")); err != nil { r.SetNextInterval(retryAfter * time.Second) // respect the API } } return err } return nil }) ``` ### 重试与测试 为了加快测试速度,可以为 roko 配置自定义的 sleep 函数: ``` err := roko.NewRetrier( roko.WithStrategy(roko.Constant(50000 * time.Hour)) // Wait a very long time between attempts... roko.WithSleepFunc(func(time.Duration) {}) // ...but don't actually sleep roko.WithMaxAttempts(3), ).Do(func(r *roko.Retrier) error { return canFail() }) ``` 传递给 `WithSleepFunc()` 的实际函数是任意的,但使用空操作(noop)可能是最有用的。 为了生成确定性的 jitter,Retrier 还接受一个 `*rand.Rand`: ``` err := roko.NewRetrier( roko.WithStrategy(roko.Constant(5 * time.Second)) roko.WithRand(rand.New(rand.NewSource(12345))), // Generate the same jitters every time, using a seeded random number generator roko.WithMaxAttempts(3), roko.WithJitter(), ).Do(func(r *roko.Retrier) error { return canFail() }) ``` 随机数生成器仅用于 jitter,因此只有在你使用 jitter 时传入它才有意义。 ## 名字有什么含义? Roko 得名于 [Josevata Rokocoko](https://en.wikipedia.org/wiki/Joe_Rokocoko),他是一名斐济-新西兰橄榄球运动员,也是史上最优秀的球员之一。他达阵得分(scored tries)无数,因此,他是一个不断重试的人(re-trier)。 ## 在寻找优秀的 CI 提供商吗?别再找了。 [Buildkite](https://buildkite.com) 是一个可以在你自己的基础设施上运行快速、安全且可扩展的 CI pipeline 的平台。
标签:EVTX分析, Go, Ruby工具, 容错处理, 开发库, 日志审计, 重试机制