buildkite/roko
GitHub: buildkite/roko
Roko 是一个轻量级、可配置的 Go 语言重试库,帮助开发者优雅地处理可能失败的操作并灵活控制重试行为。
Stars: 9 | Forks: 2
# Roko
[](https://pkg.go.dev/github.com/buildkite/roko)
[](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工具, 容错处理, 开发库, 日志审计, 重试机制