├── .github
└── workflows
│ └── test.yaml
├── .gitignore
├── LICENSE
├── README.md
├── README_CN.md
├── assets
└── logo.png
├── backoff.go
├── backoff_test.go
├── config.go
├── error.go
├── examples
├── callback
│ └── demo.go
├── factory
│ └── demo.go
└── normal
│ └── demo.go
├── go.mod
├── go.sum
├── interface.go
├── retry.go
└── retry_test.go
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches: ["main"]
4 | pull_request:
5 | branches: ["main"]
6 | name: Test
7 | jobs:
8 | test:
9 | strategy:
10 | matrix:
11 | go-version: [1.18.x, 1.19.x, 1.20.x, 1.21.x, 1.22.x]
12 | os: [ubuntu-latest, macos-latest, windows-latest]
13 | runs-on: ${{ matrix.os }}
14 | steps:
15 | - uses: actions/setup-go@v4
16 | with:
17 | go-version: "${{ matrix.go-version }}"
18 | - uses: actions/checkout@v3
19 | - name: Test
20 | run: go test -v ./...
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Test binary, built with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Dependency directories (remove the comment below to include it)
18 | # vendor/
19 |
20 | # Go workspace file
21 | go.work
22 |
23 | .idea
24 | .vscode
25 | .DS_Store
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Kuma (路口IT大叔)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | English | [中文](./README_CN.md)
2 |
3 |
4 |
Retry
5 |
A simple, dependency-free module for effortless function retrying in various scenarios.
6 |

7 |
8 |
9 | [](https://goreportcard.com/report/github.com/shengyanli1982/retry)
10 | [](github.com/shengyanli1982/retry/actions)
11 | [](https://pkg.go.dev/github.com/shengyanli1982/retry)
12 |
13 | # Introduction
14 |
15 | `Retry` is a lightweight module for retrying function calls. It is simple, easy to use, and has no third-party dependencies. It is designed for scenarios where you need to retry a function call.
16 |
17 | `Retry` provides the following features:
18 |
19 | 1. Retry a function call a specified number of times.
20 | 2. Retry a function call a specified number of times for specific errors.
21 | 3. Support action callback functions.
22 | 4. Support jitter factor for delay.
23 | 5. Support exponential backoff delay, random delay, and fixed delay.
24 | 6. Support recording detailed errors for each failed retry.
25 |
26 | # Advantages
27 |
28 | - Simple and user-friendly
29 | - No external dependencies required
30 | - Efficient memory usage
31 | - Supports callback functions
32 |
33 | # Installation
34 |
35 | ```bash
36 | go get github.com/shengyanli1982/retry
37 | ```
38 |
39 | # Quick Start
40 |
41 | Using `Retry` is simple. Just one line of code is needed to retry a function call.
42 |
43 | ## 1. Normal Model
44 |
45 | ### Config
46 |
47 | `Retry` provides a config object to customize the retry behavior. The config object has the following fields:
48 |
49 | - `ctx`: The context.Context object. The default value is `context.Background()`.
50 | - `callback`: The callback function. The default value is `&emptyCallback{}`.
51 | - `attempts`: The number of retry attempts. The default value is `3`.
52 | - `attemptsByErrors`: The number of retry attempts for specific errors. The default value is `map[error]uint64{}`.
53 | - `delay`: The delay time between retries. The default value is `200ms`.
54 | - `factor`: The retry times factor. The default value is `1.0`.
55 | - `retryIf`: The function to determine whether to retry. The default value is `defaultRetryIfFunc`.
56 | - `backoff`: The backoff function. The default value is `defaultBackoffFunc`.
57 | - `detail`: Whether to record detailed errors. The default value is `false`.
58 |
59 | You can use the following methods to set config values:
60 |
61 | - `WithContext`: Set the context.Context object.
62 | - `WithCallback`: Set the callback function.
63 | - `WithAttempts`: Set the number of retry attempts.
64 | - `WithAttemptsByError`: Set the number of retry attempts for specific errors.
65 | - `WithDelay`: Set the delay time for the first retry.
66 | - `WithFactor`: Set the retry times factor.
67 | - `WithRetryIfFunc`: Set the function to determine whether to retry.
68 | - `WithBackOffFunc`: Set the backoff function.
69 | - `WithDetail`: Set whether to record detailed errors.
70 |
71 | > [!NOTE]
72 | > The backoff algorithm determines the delay time between retries. `Retry` supports three backoff algorithms: exponential backoff, random backoff, and fixed backoff. By default, `Retry` uses exponential backoff with random backoff values added to the delay time.
73 | >
74 | > You can use the `WithBackOffFunc` method to set the backoff algorithm.
75 | >
76 | > **eg**: backoff = backoffFunc(factor \* count + jitter \* rand.Float64()) \* 100 \* Millisecond + delay
77 |
78 | ### Methods
79 |
80 | - `Do`: Retry a function call by specifying a config object and a function. It returns a `Result` object.
81 | - `DoWithDefault`: Retry a function call with default config values. It returns a `Result` object.
82 |
83 | > [!TIP]
84 | > The `Result` object contains the result of the function call, the error of the last retry, the errors of all retries, and whether the retry was successful. If the function call fails, the default value will be returned.
85 |
86 | ### Exec Result
87 |
88 | After retrying, `Retry` returns a `Result` object. The `Result` object provides the following methods:
89 |
90 | - `Data`: Get the result of the successfully called function. The type is `interface{}`.
91 | - `TryError`: Get the error of the retry action. If the retry is successful, the value is `nil`.
92 | - `ExecErrors`: Get the errors of all retries.
93 | - `IsSuccess`: Check if the retry action was successful.
94 | - `LastExecError`: Get the last error of the retries.
95 | - `FirstExecError`: Get the first error of the retries.
96 | - `ExecErrorByIndex`: Get the error of a specific retry by index.
97 |
98 | ### Example
99 |
100 | ```go
101 | package main
102 |
103 | import (
104 | "fmt"
105 |
106 | "github.com/shengyanli1982/retry"
107 | )
108 |
109 | // 定义一个可重试的函数
110 | // Define a retryable function
111 | func testFunc() (any, error) {
112 | // 此函数返回一个字符串 "lee" 和一个 nil 错误
113 | // This function returns a string "lee" and a nil error
114 | return "lee", nil
115 | }
116 |
117 | func main() {
118 | // 使用默认的重试策略调用 testFunc 函数
119 | // Call the testFunc function using the default retry strategy
120 | result := retry.DoWithDefault(testFunc)
121 |
122 | // 打印执行结果
123 | // Print the execution result
124 | fmt.Println("result:", result.Data())
125 |
126 | // 打印尝试执行的错误
127 | // Print the error of the attempt to execute
128 | fmt.Println("tryError:", result.TryError())
129 |
130 | // 打印执行过程中的所有错误
131 | // Print all errors during execution
132 | fmt.Println("execErrors:", result.ExecErrors())
133 |
134 | // 打印是否成功执行
135 | // Print whether the execution was successful
136 | fmt.Println("isSuccess:", result.IsSuccess())
137 | }
138 | ```
139 |
140 | **Result**
141 |
142 | ```bash
143 | $ go run demo.go
144 | result: lee
145 | tryError:
146 | execErrors: []
147 | isSuccess: true
148 | ```
149 |
150 | ## 2. Factory Model
151 |
152 | The Factory Model provides all the same retry functions and features as the Normal Model. It uses the same `Config`, `Methods`, `Result`, and `Callback`.
153 |
154 | The only difference is that the `Retry` object is created using the `New` method. Then you can use the `TryOnConflict` method to retry the function call with the same parameters.
155 |
156 | ### Example
157 |
158 | ```go
159 | package main
160 |
161 | import (
162 | "errors"
163 | "fmt"
164 |
165 | "github.com/shengyanli1982/retry"
166 | )
167 |
168 | // 定义一个可重试的函数 testFunc1
169 | // Define a retryable function testFunc1
170 | func testFunc1() (any, error) {
171 | // 此函数返回一个字符串 "testFunc1" 和一个 nil 错误
172 | // This function returns a string "testFunc1" and a nil error
173 | return "testFunc1", nil
174 | }
175 |
176 | // 定义一个可重试的函数 testFunc2
177 | // Define a retryable function testFunc2
178 | func testFunc2() (any, error) {
179 | // 此函数返回一个 nil 和一个新的错误 "testFunc2"
180 | // This function returns a nil and a new error "testFunc2"
181 | return nil, errors.New("testFunc2")
182 | }
183 |
184 | func main() {
185 | // 使用默认的配置创建一个新的重试实例
186 | // Create a new retry instance with the default configuration
187 | r := retry.New(nil)
188 |
189 | // 尝试执行 testFunc1 函数,如果遇到冲突则进行重试
190 | // Try to execute the testFunc1 function, retry if there is a conflict
191 | result := r.TryOnConflict(testFunc1)
192 |
193 | // 打印 testFunc1 执行结果
194 | // Print the testFunc1 execution result
195 | fmt.Println("========= testFunc1 =========")
196 |
197 | // 打印执行结果
198 | // Print the execution result
199 | fmt.Println("result:", result.Data())
200 |
201 | // 打印尝试执行的错误
202 | // Print the error of the attempt to execute
203 | fmt.Println("tryError:", result.TryError())
204 |
205 | // 打印执行过程中的所有错误
206 | // Print all errors during execution
207 | fmt.Println("execErrors:", result.ExecErrors())
208 |
209 | // 打印是否成功执行
210 | // Print whether the execution was successful
211 | fmt.Println("isSuccess:", result.IsSuccess())
212 |
213 | // 尝试执行 testFunc2 函数,如果遇到冲突则进行重试
214 | // Try to execute the testFunc2 function, retry if there is a conflict
215 | result = r.TryOnConflict(testFunc2)
216 |
217 | // 打印 testFunc2 执行结果
218 | // Print the testFunc2 execution result
219 | fmt.Println("========= testFunc2 =========")
220 |
221 | // 打印执行结果
222 | // Print the execution result
223 | fmt.Println("result:", result.Data())
224 |
225 | // 打印尝试执行的错误
226 | // Print the error of the attempt to execute
227 | fmt.Println("tryError:", result.TryError())
228 |
229 | // 打印执行过程中的所有错误
230 | // Print all errors during execution
231 | fmt.Println("execErrors:", result.ExecErrors())
232 |
233 | // 打印是否成功执行
234 | // Print whether the execution was successful
235 | fmt.Println("isSuccess:", result.IsSuccess())
236 | }
237 | ```
238 |
239 | **Result**
240 |
241 | ```bash
242 | $ go run demo.go
243 | ========= testFunc1 =========
244 | result: testFunc1
245 | tryError:
246 | execErrors: []
247 | isSuccess: true
248 | ========= testFunc2 =========
249 | result:
250 | tryError: retry attempts exceeded
251 | execErrors: []
252 | isSuccess: false
253 | ```
254 |
255 | # Features
256 |
257 | `Retry` provides a set of features that are sufficient for most services.
258 |
259 | ## 1. Callback
260 |
261 | `Retry` supports callback functions. You can specify a callback function when creating a retry, and it will be called when the `Retry` performs certain actions.
262 |
263 | > [!TIP]
264 | > Callback functions are optional. If you don't need a callback function, you can pass `nil` when creating a retry, and it won't be called.
265 | >
266 | > You can use the `WithCallback` method to set a callback function.
267 |
268 | The callback function has the following methods:
269 |
270 | - `OnRetry`: called when retrying. The `count` parameter represents the current retry count, the `delay` parameter represents the delay time for the next retry, and the `err` parameter represents the error from the last retry.
271 |
272 | ```go
273 | // Callback 接口用于定义重试回调函数
274 | // The Callback interface is used to define the retry callback function.
275 | type Callback interface {
276 | // OnRetry 方法在每次重试时调用,传入当前的重试次数、延迟时间和错误信息
277 | // The OnRetry method is called on each retry, passing in the current retry count, delay time, and error information
278 | OnRetry(count int64, delay time.Duration, err error)
279 | }
280 | ```
281 |
282 | ### Example
283 |
284 | ```go
285 | package main
286 |
287 | import (
288 | "errors"
289 | "fmt"
290 | "time"
291 |
292 | "github.com/shengyanli1982/retry"
293 | )
294 |
295 | // 定义一个错误变量
296 | // Define an error variable
297 | var err = errors.New("test") // error
298 |
299 | // 定义一个回调结构体
300 | // Define a callback structure
301 | type callback struct{}
302 |
303 | // OnRetry 方法在每次重试时被调用,接收重试次数、延迟时间和错误作为参数
304 | // The OnRetry method is called each time a retry is performed, receiving the number of retries, delay time, and error as parameters
305 | func (cb *callback) OnRetry(count int64, delay time.Duration, err error) {
306 | fmt.Println("OnRetry", count, delay.String(), err)
307 | }
308 |
309 | // 定义一个可重试的函数,返回一个 nil 和一个错误
310 | // Define a retryable function that returns a nil and an error
311 | func testFunc() (any, error) {
312 | return nil, err
313 | }
314 |
315 | func main() {
316 | // 创建一个新的重试配置,并设置回调函数
317 | // Create a new retry configuration and set the callback function
318 | cfg := retry.NewConfig().WithCallback(&callback{})
319 |
320 | // 使用重试配置调用可重试的函数
321 | // Call the retryable function using the retry configuration
322 | result := retry.Do(testFunc, cfg)
323 |
324 | // 打印执行结果
325 | // Print the execution result
326 | fmt.Println("result:", result.Data())
327 |
328 | // 打印尝试执行的错误
329 | // Print the error of the attempt to execute
330 | fmt.Println("tryError:", result.TryError())
331 |
332 | // 打印执行过程中的所有错误
333 | // Print all errors during execution
334 | fmt.Println("execErrors:", result.ExecErrors())
335 |
336 | // 打印是否成功执行
337 | // Print whether the execution was successful
338 | fmt.Println("isSuccess:", result.IsSuccess())
339 | }
340 | ```
341 |
342 | **Result**
343 |
344 | ```bash
345 | $ go run demo.go
346 | OnRetry 1 1s test
347 | OnRetry 2 1.5s test
348 | OnRetry 3 2.4s test
349 | result:
350 | tryError: retry attempts exceeded
351 | execErrors: []
352 | isSuccess: false
353 | ```
354 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 | [English](./README.md) | 中文
2 |
3 |
4 |
Retry
5 |
一个简单、无依赖的 Go 函数执行模块,用于在各种场景下轻松进行函数重试。
6 |

7 |
8 |
9 | [](https://goreportcard.com/report/github.com/shengyanli1982/retry)
10 | [](github.com/shengyanli1982/retry/actions)
11 | [](https://pkg.go.dev/github.com/shengyanli1982/retry)
12 |
13 | # 介绍
14 |
15 | `Retry` 是一个轻量级的函数重试模块。它简单易用,没有第三方依赖。它专为需要重试函数调用的场景而设计。
16 |
17 | `Retry` 提供以下功能:
18 |
19 | 1. 可以指定重试函数调用的次数。
20 | 2. 可以指定特定错误的重试次数。
21 | 3. 支持回调函数。
22 | 4. 支持延迟的抖动因子。
23 | 5. 支持指数退避延迟、随机延迟和固定延迟。
24 | 6. 支持记录每次失败重试的详细错误信息。
25 |
26 | # 优势
27 |
28 | - 简单易用
29 | - 无需外部依赖
30 | - 内存使用高效
31 | - 支持回调函数
32 |
33 | # 安装
34 |
35 | ```bash
36 | go get github.com/shengyanli1982/retry
37 | ```
38 |
39 | # 快速入门
40 |
41 | 使用 `Retry` 很简单。只需要一行代码就可以重试函数调用。
42 |
43 | ## 1. 普通模式
44 |
45 | ### 配置
46 |
47 | `Retry` 提供了一个配置对象来自定义重试行为。配置对象具有以下字段:
48 |
49 | - `ctx`:上下文对象 `context.Context`。默认值为 `context.Background()`。
50 | - `callback`:回调函数。默认值为 `&emptyCallback{}`。
51 | - `attempts`:重试次数。默认值为 `3`。
52 | - `attemptsByErrors`:特定错误的重试次数。默认值为 `map[error]uint64{}`。
53 | - `delay`:重试之间的延迟时间。默认值为 `200ms`。
54 | - `factor`:重试次数的因子。默认值为 `1.0`。
55 | - `retryIf`:确定是否重试的函数。默认值为 `defaultRetryIfFunc`。
56 | - `backoff`:退避函数。默认值为 `defaultBackoffFunc`。
57 | - `detail`:是否记录详细错误信息。默认值为 `false`。
58 |
59 | 您可以使用以下方法来设置配置值:
60 |
61 | - `WithContext`:设置上下文对象 `context.Context`。
62 | - `WithCallback`:设置回调函数。
63 | - `WithAttempts`:设置重试次数。
64 | - `WithAttemptsByError`:设置特定错误的重试次数。
65 | - `WithDelay`:设置第一次重试的延迟时间。
66 | - `WithFactor`:设置重试次数的因子。
67 | - `WithRetryIfFunc`:设置确定是否重试的函数。
68 | - `WithBackOffFunc`:设置退避函数。
69 | - `WithDetail`:设置是否记录详细错误信息。
70 |
71 | > [!NOTE]
72 | > 退避算法决定了重试之间的延迟时间。`Retry` 支持三种退避算法:指数退避、随机退避和固定退避。默认情况下,`Retry` 使用指数退避与随机退避值之和。
73 | >
74 | > 您可以使用 `WithBackOffFunc` 方法来设置退避算法。
75 | >
76 | > **eg**: backoff = backoffFunc(factor \* count + jitter \* rand.Float64()) \* 100 \* Millisecond + delay
77 |
78 | ### 方法
79 |
80 | - `Do`: 通过指定配置对象和函数来重试函数调用。它返回一个 `Result` 对象。
81 | - `DoWithDefault`: 使用默认配置值来重试函数调用。它返回一个 `Result` 对象。
82 |
83 | > [!TIP]
84 | > 在 `Result` 对象内包含函数调用的结果、最后一次重试的错误、所有重试的错误以及重试是否成功。如果函数调用失败,将返回默认值。
85 |
86 | ### 执行结果
87 |
88 | 在重试之后,`Retry` 返回一个 `Result` 对象。`Result` 对象提供以下方法:
89 |
90 | - `Data`: 获取成功调用函数的结果。类型为 `interface{}`。
91 | - `TryError`: 获取重试操作的错误。如果重试成功,则值为 `nil`。
92 | - `ExecErrors`: 获取所有重试的错误。
93 | - `IsSuccess`: 检查重试操作是否成功。
94 | - `LastExecError`: 获取最后一次重试的错误。
95 | - `FirstExecError`: 获取第一次重试的错误。
96 | - `ExecErrorByIndex`: 通过索引获取特定重试的错误。
97 |
98 | ### 示例
99 |
100 | ```go
101 | package main
102 |
103 | import (
104 | "fmt"
105 |
106 | "github.com/shengyanli1982/retry"
107 | )
108 |
109 | // 定义一个可重试的函数
110 | // Define a retryable function
111 | func testFunc() (any, error) {
112 | // 此函数返回一个字符串 "lee" 和一个 nil 错误
113 | // This function returns a string "lee" and a nil error
114 | return "lee", nil
115 | }
116 |
117 | func main() {
118 | // 使用默认的重试策略调用 testFunc 函数
119 | // Call the testFunc function using the default retry strategy
120 | result := retry.DoWithDefault(testFunc)
121 |
122 | // 打印执行结果
123 | // Print the execution result
124 | fmt.Println("result:", result.Data())
125 |
126 | // 打印尝试执行的错误
127 | // Print the error of the attempt to execute
128 | fmt.Println("tryError:", result.TryError())
129 |
130 | // 打印执行过程中的所有错误
131 | // Print all errors during execution
132 | fmt.Println("execErrors:", result.ExecErrors())
133 |
134 | // 打印是否成功执行
135 | // Print whether the execution was successful
136 | fmt.Println("isSuccess:", result.IsSuccess())
137 | }
138 | ```
139 |
140 | **Result**
141 |
142 | ```bash
143 | $ go run demo.go
144 | result: lee
145 | tryError:
146 | execErrors: []
147 | isSuccess: true
148 | ```
149 |
150 | ## 2. 工厂模式
151 |
152 | 工厂模式提供了与普通模式相同的重试函数和功能。它使用相同的 `Config`、`Methods`、`Result` 和 `Callback`。
153 |
154 | 唯一的区别是使用 `New` 方法创建 `Retry` 对象,然后可以使用 `TryOnConflict` 方法以相同的参数重试函数调用。
155 |
156 | ### 示例
157 |
158 | ```go
159 | package main
160 |
161 | import (
162 | "errors"
163 | "fmt"
164 |
165 | "github.com/shengyanli1982/retry"
166 | )
167 |
168 | // 定义一个可重试的函数 testFunc1
169 | // Define a retryable function testFunc1
170 | func testFunc1() (any, error) {
171 | // 此函数返回一个字符串 "testFunc1" 和一个 nil 错误
172 | // This function returns a string "testFunc1" and a nil error
173 | return "testFunc1", nil
174 | }
175 |
176 | // 定义一个可重试的函数 testFunc2
177 | // Define a retryable function testFunc2
178 | func testFunc2() (any, error) {
179 | // 此函数返回一个 nil 和一个新的错误 "testFunc2"
180 | // This function returns a nil and a new error "testFunc2"
181 | return nil, errors.New("testFunc2")
182 | }
183 |
184 | func main() {
185 | // 使用默认的配置创建一个新的重试实例
186 | // Create a new retry instance with the default configuration
187 | r := retry.New(nil)
188 |
189 | // 尝试执行 testFunc1 函数,如果遇到冲突则进行重试
190 | // Try to execute the testFunc1 function, retry if there is a conflict
191 | result := r.TryOnConflict(testFunc1)
192 |
193 | // 打印 testFunc1 执行结果
194 | // Print the testFunc1 execution result
195 | fmt.Println("========= testFunc1 =========")
196 |
197 | // 打印执行结果
198 | // Print the execution result
199 | fmt.Println("result:", result.Data())
200 |
201 | // 打印尝试执行的错误
202 | // Print the error of the attempt to execute
203 | fmt.Println("tryError:", result.TryError())
204 |
205 | // 打印执行过程中的所有错误
206 | // Print all errors during execution
207 | fmt.Println("execErrors:", result.ExecErrors())
208 |
209 | // 打印是否成功执行
210 | // Print whether the execution was successful
211 | fmt.Println("isSuccess:", result.IsSuccess())
212 |
213 | // 尝试执行 testFunc2 函数,如果遇到冲突则进行重试
214 | // Try to execute the testFunc2 function, retry if there is a conflict
215 | result = r.TryOnConflict(testFunc2)
216 |
217 | // 打印 testFunc2 执行结果
218 | // Print the testFunc2 execution result
219 | fmt.Println("========= testFunc2 =========")
220 |
221 | // 打印执行结果
222 | // Print the execution result
223 | fmt.Println("result:", result.Data())
224 |
225 | // 打印尝试执行的错误
226 | // Print the error of the attempt to execute
227 | fmt.Println("tryError:", result.TryError())
228 |
229 | // 打印执行过程中的所有错误
230 | // Print all errors during execution
231 | fmt.Println("execErrors:", result.ExecErrors())
232 |
233 | // 打印是否成功执行
234 | // Print whether the execution was successful
235 | fmt.Println("isSuccess:", result.IsSuccess())
236 | }
237 | ```
238 |
239 | **Result**
240 |
241 | ```bash
242 | $ go run demo.go
243 | ========= testFunc1 =========
244 | result: testFunc1
245 | tryError:
246 | execErrors: []
247 | isSuccess: true
248 | ========= testFunc2 =========
249 | result:
250 | tryError: retry attempts exceeded
251 | execErrors: []
252 | isSuccess: false
253 | ```
254 |
255 | # 特性
256 |
257 | `Retry` 提供了一组足够满足大多数服务需求的特性。
258 |
259 | ## 1. 回调函数
260 |
261 | `Retry` 支持回调函数。在创建重试实例时,您可以指定一个回调函数,当 `Retry` 执行特定操作时,该函数将被调用。
262 |
263 | > [!TIP]
264 | > 回调函数是可选的。如果您不需要回调函数,可以在创建重试实例时传递 `nil`,它将不会被调用。
265 | >
266 | > 您可以使用 `WithCallback` 方法来设置回调函数。
267 |
268 | 回调函数具有以下方法:
269 |
270 | - `OnRetry`:在重试时调用。`count` 参数表示当前重试次数,`delay` 参数表示下一次重试的延迟时间,`err` 参数表示上一次重试的错误信息。
271 |
272 | ```go
273 | // Callback 接口用于定义重试回调函数
274 | // The Callback interface is used to define the retry callback function.
275 | type Callback interface {
276 | // OnRetry 方法在每次重试时调用,传入当前的重试次数、延迟时间和错误信息
277 | // The OnRetry method is called on each retry, passing in the current retry count, delay time, and error information
278 | OnRetry(count int64, delay time.Duration, err error)
279 | }
280 | ```
281 |
282 | ### 示例
283 |
284 | ```go
285 | package main
286 |
287 | import (
288 | "errors"
289 | "fmt"
290 | "time"
291 |
292 | "github.com/shengyanli1982/retry"
293 | )
294 |
295 | // 定义一个错误变量
296 | // Define an error variable
297 | var err = errors.New("test") // error
298 |
299 | // 定义一个回调结构体
300 | // Define a callback structure
301 | type callback struct{}
302 |
303 | // OnRetry 方法在每次重试时被调用,接收重试次数、延迟时间和错误作为参数
304 | // The OnRetry method is called each time a retry is performed, receiving the number of retries, delay time, and error as parameters
305 | func (cb *callback) OnRetry(count int64, delay time.Duration, err error) {
306 | fmt.Println("OnRetry", count, delay.String(), err)
307 | }
308 |
309 | // 定义一个可重试的函数,返回一个 nil 和一个错误
310 | // Define a retryable function that returns a nil and an error
311 | func testFunc() (any, error) {
312 | return nil, err
313 | }
314 |
315 | func main() {
316 | // 创建一个新的重试配置,并设置回调函数
317 | // Create a new retry configuration and set the callback function
318 | cfg := retry.NewConfig().WithCallback(&callback{})
319 |
320 | // 使用重试配置调用可重试的函数
321 | // Call the retryable function using the retry configuration
322 | result := retry.Do(testFunc, cfg)
323 |
324 | // 打印执行结果
325 | // Print the execution result
326 | fmt.Println("result:", result.Data())
327 |
328 | // 打印尝试执行的错误
329 | // Print the error of the attempt to execute
330 | fmt.Println("tryError:", result.TryError())
331 |
332 | // 打印执行过程中的所有错误
333 | // Print all errors during execution
334 | fmt.Println("execErrors:", result.ExecErrors())
335 |
336 | // 打印是否成功执行
337 | // Print whether the execution was successful
338 | fmt.Println("isSuccess:", result.IsSuccess())
339 | }
340 | ```
341 |
342 | **Result**
343 |
344 | ```bash
345 | $ go run demo.go
346 | OnRetry 1 1s test
347 | OnRetry 2 1.5s test
348 | OnRetry 3 2.4s test
349 | result:
350 | tryError: retry attempts exceeded
351 | execErrors: []
352 | isSuccess: false
353 | ```
354 |
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shengyanli1982/retry/a2f12924f920cf13e1671cd45dbe8ac37cdfdad2/assets/logo.png
--------------------------------------------------------------------------------
/backoff.go:
--------------------------------------------------------------------------------
1 | package retry
2 |
3 | import (
4 | "math"
5 | "math/rand"
6 | "sync"
7 | "time"
8 | )
9 |
10 | const (
11 | // 基础时间单位为100毫秒
12 | // Base time unit is 100 milliseconds
13 | baseInterval = 100 * time.Millisecond
14 |
15 | // 防止 time.Duration 溢出的最大指数值
16 | // Maximum exponent to prevent time.Duration overflow
17 | maxExponent = 62
18 | )
19 |
20 | var (
21 | // 使用独立的随机数生成器,避免全局锁竞争
22 | // Use a separate random number generator to avoid global lock contention
23 | randGen = rand.New(rand.NewSource(time.Now().UnixNano()))
24 | randMu sync.Mutex
25 | )
26 |
27 | // BackoffFunc 定义了退避策略函数的类型
28 | // BackoffFunc defines the type for backoff strategy functions
29 | type BackoffFunc = func(int64) time.Duration
30 |
31 | // FixedBackoff 返回固定时间间隔的退避策略
32 | // FixedBackoff returns a fixed-interval backoff strategy
33 | func FixedBackoff(interval int64) time.Duration {
34 | if interval <= 0 {
35 | return defaultDelay
36 | }
37 | return time.Duration(interval) * baseInterval
38 | }
39 |
40 | // RandomBackoff 返回随机时间间隔的退避策略
41 | // RandomBackoff returns a random-interval backoff strategy
42 | func RandomBackoff(maxInterval int64) time.Duration {
43 | if maxInterval <= 0 {
44 | return defaultDelay
45 | }
46 |
47 | randMu.Lock()
48 | interval := randGen.Int63n(maxInterval)
49 | randMu.Unlock()
50 |
51 | return time.Duration(interval) * baseInterval
52 | }
53 |
54 | // ExponentialBackoff 返回指数增长的退避策略
55 | // ExponentialBackoff returns an exponential backoff strategy
56 | func ExponentialBackoff(power int64) time.Duration {
57 | if power <= 0 {
58 | return defaultDelay
59 | }
60 |
61 | // 限制最大指数以防止溢出
62 | // Limit maximum exponent to prevent overflow
63 | if power > maxExponent {
64 | power = maxExponent
65 | }
66 |
67 | return time.Duration(int64(math.Exp2(float64(power)))) * baseInterval
68 | }
69 |
70 | // CombineBackoffs 将多个退避策略组合成一个
71 | // CombineBackoffs combines multiple backoff strategies into one
72 | func CombineBackoffs(backoffs ...BackoffFunc) BackoffFunc {
73 | if len(backoffs) == 0 {
74 | return FixedBackoff
75 | }
76 |
77 | return func(n int64) time.Duration {
78 | var totalDelay time.Duration
79 | for _, backoff := range backoffs {
80 | totalDelay += backoff(n)
81 | }
82 |
83 | if totalDelay <= 0 {
84 | return defaultDelay
85 | }
86 | return totalDelay
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/backoff_test.go:
--------------------------------------------------------------------------------
1 | package retry
2 |
3 | import (
4 | "math"
5 | "sync"
6 | "testing"
7 | "time"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestFixedBackoff(t *testing.T) {
13 | tests := []struct {
14 | name string
15 | input int64
16 | expected time.Duration
17 | }{
18 | {
19 | name: "normal case",
20 | input: 3,
21 | expected: 3 * baseInterval,
22 | },
23 | {
24 | name: "zero input",
25 | input: 0,
26 | expected: defaultDelay,
27 | },
28 | {
29 | name: "negative input",
30 | input: -1,
31 | expected: defaultDelay,
32 | },
33 | }
34 |
35 | for _, tt := range tests {
36 | t.Run(tt.name, func(t *testing.T) {
37 | result := FixedBackoff(tt.input)
38 | assert.Equal(t, tt.expected, result)
39 | })
40 | }
41 | }
42 |
43 | func TestRandomBackoff(t *testing.T) {
44 | tests := []struct {
45 | name string
46 | input int64
47 | }{
48 | {
49 | name: "normal case",
50 | input: 5,
51 | },
52 | {
53 | name: "zero input",
54 | input: 0,
55 | },
56 | {
57 | name: "negative input",
58 | input: -1,
59 | },
60 | }
61 |
62 | for _, tt := range tests {
63 | t.Run(tt.name, func(t *testing.T) {
64 | result := RandomBackoff(tt.input)
65 | if tt.input <= 0 {
66 | assert.Equal(t, defaultDelay, result)
67 | } else {
68 | assert.LessOrEqual(t, result, time.Duration(tt.input)*baseInterval)
69 | assert.GreaterOrEqual(t, result, time.Duration(0))
70 | }
71 | })
72 | }
73 | }
74 |
75 | func TestExponentialBackoff(t *testing.T) {
76 | tests := []struct {
77 | name string
78 | input int64
79 | expected time.Duration
80 | }{
81 | {
82 | name: "normal case",
83 | input: 3,
84 | expected: time.Duration(int64(math.Exp2(3))) * baseInterval,
85 | },
86 | {
87 | name: "zero input",
88 | input: 0,
89 | expected: defaultDelay,
90 | },
91 | {
92 | name: "negative input",
93 | input: -1,
94 | expected: defaultDelay,
95 | },
96 | {
97 | name: "max exponential",
98 | input: maxExponent + 1,
99 | expected: time.Duration(int64(math.Exp2(float64(maxExponent)))) * baseInterval,
100 | },
101 | }
102 |
103 | for _, tt := range tests {
104 | t.Run(tt.name, func(t *testing.T) {
105 | result := ExponentialBackoff(tt.input)
106 | assert.Equal(t, tt.expected, result)
107 | })
108 | }
109 | }
110 |
111 | func TestCombineBackoffs(t *testing.T) {
112 | tests := []struct {
113 | name string
114 | backoffs []BackoffFunc
115 | input int64
116 | expected time.Duration
117 | }{
118 | {
119 | name: "empty backoffs",
120 | backoffs: []BackoffFunc{},
121 | input: 3,
122 | expected: FixedBackoff(3),
123 | },
124 | {
125 | name: "single backoff",
126 | backoffs: []BackoffFunc{FixedBackoff},
127 | input: 3,
128 | expected: 3 * baseInterval,
129 | },
130 | {
131 | name: "multiple backoffs",
132 | backoffs: []BackoffFunc{FixedBackoff, ExponentialBackoff},
133 | input: 3,
134 | expected: time.Duration(3+int64(math.Exp2(3))) * baseInterval,
135 | },
136 | }
137 |
138 | for _, tt := range tests {
139 | t.Run(tt.name, func(t *testing.T) {
140 | combined := CombineBackoffs(tt.backoffs...)
141 | result := combined(tt.input)
142 | assert.Equal(t, tt.expected, result)
143 | })
144 | }
145 | }
146 |
147 | func TestConcurrentBackoffs(t *testing.T) {
148 | const (
149 | goroutines = 100
150 | iterations = 1000
151 | )
152 |
153 | var wg sync.WaitGroup
154 | wg.Add(goroutines)
155 |
156 | for i := 0; i < goroutines; i++ {
157 | go func() {
158 | defer wg.Done()
159 | for j := 0; j < iterations; j++ {
160 | // Test all backoff functions concurrently
161 | _ = RandomBackoff(5)
162 | _ = ExponentialBackoff(3)
163 | combined := CombineBackoffs(FixedBackoff, ExponentialBackoff)
164 | _ = combined(3)
165 | }
166 | }()
167 | }
168 |
169 | wg.Wait()
170 | }
171 |
172 | func BenchmarkFixedBackoff(b *testing.B) {
173 | for i := 0; i < b.N; i++ {
174 | _ = FixedBackoff(3)
175 | }
176 | }
177 |
178 | func BenchmarkRandomBackoff(b *testing.B) {
179 | for i := 0; i < b.N; i++ {
180 | _ = RandomBackoff(3)
181 | }
182 | }
183 |
184 | func BenchmarkExponentialBackoff(b *testing.B) {
185 | for i := 0; i < b.N; i++ {
186 | _ = ExponentialBackoff(3)
187 | }
188 | }
189 |
190 | func BenchmarkCombinedBackoffs(b *testing.B) {
191 | combined := CombineBackoffs(FixedBackoff, ExponentialBackoff)
192 | b.ResetTimer()
193 | for i := 0; i < b.N; i++ {
194 | _ = combined(3)
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package retry
2 |
3 | import (
4 | "context"
5 | "math"
6 | "time"
7 | )
8 |
9 | // 定义默认的重试次数、延迟时间、抖动和因子
10 | // Define the default number of retries, delay time, jitter, and factor
11 | const (
12 | defaultAttempts = 3 // 默认的重试次数为3次
13 | defaultDelayNum = 5 // 默认的延迟时间为5毫秒
14 | defaultDelay = defaultDelayNum * time.Millisecond * 100 // 计算默认的延迟时间
15 | defaultJitter = 3.0 // 默认的抖动为3.0
16 | defaultFactor = 1.0 // 默认的因子为1.0
17 | )
18 |
19 | // 定义默认的重试条件函数和退避函数
20 | // Define the default retry condition function and backoff function
21 | var (
22 | // defaultRetryIfFunc 是默认的重试条件函数,对所有错误都进行重试
23 | // defaultRetryIfFunc is the default retry condition function, which retries for all errors
24 | defaultRetryIfFunc = func(error) bool { return true }
25 |
26 | // defaultBackoffFunc 是默认的退避函数,使用指数退避和随机退避的组合
27 | // defaultBackoffFunc is the default backoff function, which combines exponential backoff and random backoff
28 | defaultBackoffFunc = func(n int64) time.Duration {
29 | return CombineBackoffs(ExponentialBackoff, RandomBackoff)(n)
30 | }
31 | )
32 |
33 | // 定义一个空的回调结构体
34 | // Define an empty callback structure
35 | type emptyCallback struct{}
36 |
37 | // OnRetry 方法在每次重试时调用,但不执行任何操作
38 | // The OnRetry method is called on each retry, but does not perform any operations
39 | func (cb *emptyCallback) OnRetry(count int64, delay time.Duration, err error) {}
40 |
41 | // NewEmptyCallback 函数返回一个新的空回调实例
42 | // The NewEmptyCallback function returns a new empty callback instance
43 | func NewEmptyCallback() Callback {
44 | return &emptyCallback{}
45 | }
46 |
47 | // RetryIfFunc 类型定义了一个接受错误并返回布尔值的函数类型
48 | // The RetryIfFunc type defines a function type that accepts an error and returns a boolean value
49 | type RetryIfFunc = func(error) bool
50 |
51 | // Config 结构体定义了重试的配置
52 | // The Config structure defines the configuration for retries
53 | type Config struct {
54 | ctx context.Context // 上下文,用于控制重试的生命周期
55 | callback Callback // 回调函数,用于在每次重试时执行
56 | attempts uint64 // 重试次数
57 | attemptsByError map[error]uint64 // 按错误类型的重试次数
58 | factor float64 // 退避因子,用于控制退避时间的增长速度
59 | jitter float64 // 抖动,用于在退避时间上添加随机性
60 | delay time.Duration // 延迟时间,用于控制每次重试之间的间隔
61 | retryIfFunc RetryIfFunc // 重试条件函数,用于判断是否应该重试
62 | backoffFunc BackoffFunc // 退避函数,用于计算每次重试的延迟时间
63 | detail bool // 是否显示详细的错误信息
64 | }
65 |
66 | // NewConfig 函数返回一个新的 Config 实例,使用默认的配置
67 | // The NewConfig function returns a new Config instance with the default configuration
68 | func NewConfig() *Config {
69 | return &Config{
70 | ctx: context.Background(),
71 | callback: NewEmptyCallback(),
72 | attempts: defaultAttempts,
73 | attemptsByError: make(map[error]uint64),
74 | factor: defaultFactor,
75 | delay: defaultDelay,
76 | jitter: defaultJitter,
77 | retryIfFunc: defaultRetryIfFunc,
78 | backoffFunc: defaultBackoffFunc,
79 | detail: false,
80 | }
81 | }
82 |
83 | // WithContext 方法设置 Config 的上下文并返回 Config 实例
84 | // The WithContext method sets the context of the Config and returns the Config instance
85 | func (c *Config) WithContext(ctx context.Context) *Config {
86 | c.ctx = ctx
87 | return c
88 | }
89 |
90 | // WithCallback 方法设置 Config 的回调函数并返回 Config 实例
91 | // The WithCallback method sets the callback function of the Config and returns the Config instance
92 | func (c *Config) WithCallback(cb Callback) *Config {
93 | c.callback = cb
94 | return c
95 | }
96 |
97 | // WithAttempts 方法设置 Config 的重试次数并返回 Config 实例
98 | // The WithAttempts method sets the number of retries of the Config and returns the Config instance
99 | func (c *Config) WithAttempts(attempts uint64) *Config {
100 | c.attempts = attempts
101 | return c
102 | }
103 |
104 | // WithAttemptsByError 方法设置 Config 的错误重试次数并返回 Config 实例
105 | // The WithAttemptsByError method sets the number of error retries of the Config and returns the Config instance
106 | func (c *Config) WithAttemptsByError(attemptsByError map[error]uint64) *Config {
107 | c.attemptsByError = attemptsByError
108 | return c
109 | }
110 |
111 | // WithFactor 方法设置 Config 的因子并返回 Config 实例
112 | // The WithFactor method sets the factor of the Config and returns the Config instance
113 | func (c *Config) WithFactor(factor float64) *Config {
114 | c.factor = factor
115 | return c
116 | }
117 |
118 | // WithInitDelay 方法设置 Config 的初始延迟时间并返回 Config 实例
119 | // The WithInitDelay method sets the initial delay time of the Config and returns the Config instance
120 | func (c *Config) WithInitDelay(delay time.Duration) *Config {
121 | c.delay = delay
122 | return c
123 | }
124 |
125 | // WithJitter 方法设置 Config 的抖动并返回 Config 实例
126 | // The WithJitter method sets the jitter of the Config and returns the Config instance
127 | func (c *Config) WithJitter(jitter float64) *Config {
128 | c.jitter = jitter
129 | return c
130 | }
131 |
132 | // WithRetryIfFunc 方法设置 Config 的重试条件函数并返回 Config 实例
133 | // The WithRetryIfFunc method sets the retry condition function of the Config and returns the Config instance
134 | func (c *Config) WithRetryIfFunc(retryIf RetryIfFunc) *Config {
135 | c.retryIfFunc = retryIf
136 | return c
137 | }
138 |
139 | // WithBackOffFunc 方法设置 Config 的退避函数并返回 Config 实例
140 | // The WithBackOffFunc method sets the backoff function of the Config and returns the Config instance
141 | func (c *Config) WithBackOffFunc(backoff BackoffFunc) *Config {
142 | c.backoffFunc = backoff
143 | return c
144 | }
145 |
146 | // WithDetail 方法设置 Config 的详细错误信息显示选项并返回 Config 实例
147 | // The WithDetail method sets the detailed error information display option of the Config and returns the Config instance
148 | func (c *Config) WithDetail(detail bool) *Config {
149 | c.detail = detail
150 | return c
151 | }
152 |
153 | // isConfigValid 函数检查 Config 是否有效,如果无效则使用默认值
154 | // The isConfigValid function checks whether the Config is valid, and uses the default value if it is invalid
155 | func isConfigValid(conf *Config) *Config {
156 | // 如果 conf 为 nil,则创建一个新的 Config 实例
157 | // If conf is nil, create a new Config instance
158 | if conf == nil {
159 | conf = NewConfig()
160 | } else {
161 | // 如果 conf.ctx 为 nil,则设置为默认的上下文
162 | // If conf.ctx is nil, set it to the default context
163 | if conf.ctx == nil {
164 | conf.ctx = context.Background()
165 | }
166 |
167 | // 如果 conf.callback 为 nil,则设置为默认的回调函数
168 | // If conf.callback is nil, set it to the default callback function
169 | if conf.callback == nil {
170 | conf.callback = NewEmptyCallback()
171 | }
172 |
173 | // 如果 conf.attempts 不在有效范围内,则设置为默认的重试次数
174 | // If conf.attempts is not within the valid range, set it to the default number of retries
175 | if conf.attempts <= 0 || conf.attempts >= math.MaxUint16 {
176 | conf.attempts = defaultAttempts
177 | }
178 |
179 | // 如果 conf.attemptsByError 为 nil,则初始化为一个空的映射
180 | // If conf.attemptsByError is nil, initialize it to an empty map
181 | if conf.attemptsByError == nil {
182 | conf.attemptsByError = make(map[error]uint64)
183 | }
184 |
185 | // 如果 conf.factor 小于 0,则设置为默认的退避因子
186 | // If conf.factor is less than 0, set it to the default backoff factor
187 | if conf.factor < 0 {
188 | conf.factor = defaultFactor
189 | }
190 |
191 | // 如果 conf.delay 小于等于 0,则设置为默认的延迟时间
192 | // If conf.delay is less than or equal to 0, set it to the default delay time
193 | if conf.delay <= 0 {
194 | conf.delay = defaultDelay
195 | }
196 |
197 | // 如果 conf.jitter 小于 0,则设置为默认的抖动
198 | // If conf.jitter is less than 0, set it to the default jitter
199 | if conf.jitter < 0 {
200 | conf.jitter = defaultJitter
201 | }
202 |
203 | // 如果 conf.retryIfFunc 为 nil,则设置为默认的重试条件函数
204 | // If conf.retryIfFunc is nil, set it to the default retry condition function
205 | if conf.retryIfFunc == nil {
206 | conf.retryIfFunc = defaultRetryIfFunc
207 | }
208 |
209 | // 如果 conf.backoffFunc 为 nil,则设置为默认的退避函数
210 | // If conf.backoffFunc is nil, set it to the default backoff function
211 | if conf.backoffFunc == nil {
212 | conf.backoffFunc = defaultBackoffFunc
213 | }
214 | }
215 |
216 | // 返回检查并修正后的 Config 实例
217 | // Return the checked and corrected Config instance
218 | return conf
219 | }
220 |
221 | // DefaultConfig 函数返回一个新的默认配置的 Config 实例
222 | // The DefaultConfig function returns a new Config instance with the default configuration
223 | func DefaultConfig() *Config {
224 | return NewConfig()
225 | }
226 |
227 | // FixConfig 函数返回一个新的固定退避时间的 Config 实例
228 | // The FixConfig function returns a new Config instance with a fixed backoff time
229 | func FixConfig() *Config {
230 | return NewConfig().WithBackOffFunc(FixedBackoff).WithFactor(0).WithJitter(0)
231 | }
232 |
--------------------------------------------------------------------------------
/error.go:
--------------------------------------------------------------------------------
1 | package retry
2 |
3 | import "errors"
4 |
5 | var (
6 | // ErrorRetryIf 表示重试检查函数的结果为FALSE的错误
7 | // ErrorRetryIf represents an error when the retry check function result is FALSE
8 | ErrorRetryIf = errors.New("retry check func result is FALSE")
9 |
10 | // ErrorRetryAttemptsExceeded 表示重试次数超过限制的错误
11 | // ErrorRetryAttemptsExceeded represents an error when the retry attempts exceeded the limit
12 | ErrorRetryAttemptsExceeded = errors.New("retry attempts exceeded")
13 |
14 | // ErrorRetryAttemptsByErrorExceeded 表示由于特定错误导致的重试次数超过限制的错误
15 | // ErrorRetryAttemptsByErrorExceeded represents an error when the retry attempts exceeded the limit due to a specific error
16 | ErrorRetryAttemptsByErrorExceeded = errors.New("retry attempts by spec error exceeded")
17 |
18 | // ErrorExecErrByIndexOutOfBound 表示由于索引越界导致的执行错误
19 | // ErrorExecErrByIndexOutOfBound represents an execution error caused by index out of bound
20 | ErrorExecErrByIndexOutOfBound = errors.New("exec error by index out of bound")
21 |
22 | // ErrorExecErrNotFound 表示未找到执行错误
23 | // ErrorExecErrNotFound represents an error when the execution error is not found
24 | ErrorExecErrNotFound = errors.New("exec error not found")
25 | )
26 |
--------------------------------------------------------------------------------
/examples/callback/demo.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "time"
7 |
8 | "github.com/shengyanli1982/retry"
9 | )
10 |
11 | // 定义一个错误变量
12 | // Define an error variable
13 | var err = errors.New("test") // error
14 |
15 | // 定义一个回调结构体
16 | // Define a callback structure
17 | type callback struct{}
18 |
19 | // OnRetry 方法在每次重试时被调用,接收重试次数、延迟时间和错误作为参数
20 | // The OnRetry method is called each time a retry is performed, receiving the number of retries, delay time, and error as parameters
21 | func (cb *callback) OnRetry(count int64, delay time.Duration, err error) {
22 | fmt.Println("OnRetry", count, delay.String(), err)
23 | }
24 |
25 | // 定义一个可重试的函数,返回一个 nil 和一个错误
26 | // Define a retryable function that returns a nil and an error
27 | func testFunc() (any, error) {
28 | return nil, err
29 | }
30 |
31 | func main() {
32 | // 创建一个新的重试配置,并设置回调函数
33 | // Create a new retry configuration and set the callback function
34 | cfg := retry.NewConfig().WithCallback(&callback{})
35 |
36 | // 使用重试配置调用可重试的函数
37 | // Call the retryable function using the retry configuration
38 | result := retry.Do(testFunc, cfg)
39 |
40 | // 打印执行结果
41 | // Print the execution result
42 | fmt.Println("result:", result.Data())
43 |
44 | // 打印尝试执行的错误
45 | // Print the error of the attempt to execute
46 | fmt.Println("tryError:", result.TryError())
47 |
48 | // 打印执行过程中的所有错误
49 | // Print all errors during execution
50 | fmt.Println("execErrors:", result.ExecErrors())
51 |
52 | // 打印是否成功执行
53 | // Print whether the execution was successful
54 | fmt.Println("isSuccess:", result.IsSuccess())
55 | }
56 |
--------------------------------------------------------------------------------
/examples/factory/demo.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/shengyanli1982/retry"
8 | )
9 |
10 | // 定义一个可重试的函数 testFunc1
11 | // Define a retryable function testFunc1
12 | func testFunc1() (any, error) {
13 | // 此函数返回一个字符串 "testFunc1" 和一个 nil 错误
14 | // This function returns a string "testFunc1" and a nil error
15 | return "testFunc1", nil
16 | }
17 |
18 | // 定义一个可重试的函数 testFunc2
19 | // Define a retryable function testFunc2
20 | func testFunc2() (any, error) {
21 | // 此函数返回一个 nil 和一个新的错误 "testFunc2"
22 | // This function returns a nil and a new error "testFunc2"
23 | return nil, errors.New("testFunc2")
24 | }
25 |
26 | func main() {
27 | // 使用默认的配置创建一个新的重试实例
28 | // Create a new retry instance with the default configuration
29 | r := retry.New(nil)
30 |
31 | // 尝试执行 testFunc1 函数,如果遇到冲突则进行重试
32 | // Try to execute the testFunc1 function, retry if there is a conflict
33 | result := r.TryOnConflict(testFunc1)
34 |
35 | // 打印 testFunc1 执行结果
36 | // Print the testFunc1 execution result
37 | fmt.Println("========= testFunc1 =========")
38 |
39 | // 打印执行结果
40 | // Print the execution result
41 | fmt.Println("result:", result.Data())
42 |
43 | // 打印尝试执行的错误
44 | // Print the error of the attempt to execute
45 | fmt.Println("tryError:", result.TryError())
46 |
47 | // 打印执行过程中的所有错误
48 | // Print all errors during execution
49 | fmt.Println("execErrors:", result.ExecErrors())
50 |
51 | // 打印是否成功执行
52 | // Print whether the execution was successful
53 | fmt.Println("isSuccess:", result.IsSuccess())
54 |
55 | // 尝试执行 testFunc2 函数,如果遇到冲突则进行重试
56 | // Try to execute the testFunc2 function, retry if there is a conflict
57 | result = r.TryOnConflict(testFunc2)
58 |
59 | // 打印 testFunc2 执行结果
60 | // Print the testFunc2 execution result
61 | fmt.Println("========= testFunc2 =========")
62 |
63 | // 打印执行结果
64 | // Print the execution result
65 | fmt.Println("result:", result.Data())
66 |
67 | // 打印尝试执行的错误
68 | // Print the error of the attempt to execute
69 | fmt.Println("tryError:", result.TryError())
70 |
71 | // 打印执行过程中的所有错误
72 | // Print all errors during execution
73 | fmt.Println("execErrors:", result.ExecErrors())
74 |
75 | // 打印是否成功执行
76 | // Print whether the execution was successful
77 | fmt.Println("isSuccess:", result.IsSuccess())
78 | }
79 |
--------------------------------------------------------------------------------
/examples/normal/demo.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/shengyanli1982/retry"
7 | )
8 |
9 | // 定义一个可重试的函数
10 | // Define a retryable function
11 | func testFunc() (any, error) {
12 | // 此函数返回一个字符串 "lee" 和一个 nil 错误
13 | // This function returns a string "lee" and a nil error
14 | return "lee", nil
15 | }
16 |
17 | func main() {
18 | // 使用默认的重试策略调用 testFunc 函数
19 | // Call the testFunc function using the default retry strategy
20 | result := retry.DoWithDefault(testFunc)
21 |
22 | // 打印执行结果
23 | // Print the execution result
24 | fmt.Println("result:", result.Data())
25 |
26 | // 打印尝试执行的错误
27 | // Print the error of the attempt to execute
28 | fmt.Println("tryError:", result.TryError())
29 |
30 | // 打印执行过程中的所有错误
31 | // Print all errors during execution
32 | fmt.Println("execErrors:", result.ExecErrors())
33 |
34 | // 打印是否成功执行
35 | // Print whether the execution was successful
36 | fmt.Println("isSuccess:", result.IsSuccess())
37 | }
38 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/shengyanli1982/retry
2 |
3 | go 1.19
4 |
5 | require github.com/stretchr/testify v1.8.4
6 |
7 | require (
8 | github.com/davecgh/go-spew v1.1.1 // indirect
9 | github.com/pmezard/go-difflib v1.0.0 // indirect
10 | gopkg.in/yaml.v3 v3.0.1 // indirect
11 | )
12 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
6 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
11 |
--------------------------------------------------------------------------------
/interface.go:
--------------------------------------------------------------------------------
1 | package retry
2 |
3 | import "time"
4 |
5 | // Callback 接口用于定义重试回调函数
6 | // The Callback interface is used to define the retry callback function.
7 | type Callback interface {
8 | // OnRetry 方法在每次重试时调用,传入当前的重试次数、延迟时间和错误信息
9 | // The OnRetry method is called on each retry, passing in the current retry count, delay time, and error information
10 | OnRetry(count int64, delay time.Duration, err error)
11 | }
12 |
13 | // RetryResult 接口定义了执行结果的相关方法
14 | // The RetryResult interface defines methods related to execution results
15 | type RetryResult = interface {
16 | // Data 方法返回执行结果的数据
17 | // The Data method returns the data of the execution result
18 | Data() any
19 |
20 | // TryError 方法返回尝试执行时的错误
21 | // The TryError method returns the error when trying to execute
22 | TryError() error
23 |
24 | // ExecErrors 方法返回所有执行错误的列表
25 | // The ExecErrors method returns a list of all execution errors
26 | ExecErrors() []error
27 |
28 | // IsSuccess 方法返回执行是否成功
29 | // The IsSuccess method returns whether the execution was successful
30 | IsSuccess() bool
31 |
32 | // LastExecError 方法返回最后一次执行的错误
33 | // The LastExecError method returns the error of the last execution
34 | LastExecError() error
35 |
36 | // FirstExecError 方法返回第一次执行的错误
37 | // The FirstExecError method returns the error of the first execution
38 | FirstExecError() error
39 |
40 | // ExecErrorByIndex 方法返回指定索引处的执行错误
41 | // The ExecErrorByIndex method returns the execution error at the specified index
42 | ExecErrorByIndex(idx int) error
43 |
44 | // Count 方法返回执行的次数
45 | // The Count method returns the number of executions
46 | Count() int64
47 | }
48 |
--------------------------------------------------------------------------------
/retry.go:
--------------------------------------------------------------------------------
1 | package retry
2 |
3 | import (
4 | "math/rand"
5 | "time"
6 | )
7 |
8 | // Result 结构体用于存储执行结果
9 | // The Result struct is used to store the execution result
10 | type Result struct {
11 | count uint64 // 执行次数 Execution count
12 | data any // 执行结果数据 Execution result data
13 | tryError error // 尝试执行时的错误 Error when trying to execute
14 | execErrors []error // 执行错误列表 List of execution errors
15 | }
16 |
17 | // NewResult 函数用于创建一个新的 Result 实例
18 | // The NewResult function is used to create a new Result instance
19 | func NewResult() *Result {
20 | return &Result{execErrors: make([]error, 0)}
21 | }
22 |
23 | // Data 方法返回执行结果的数据
24 | // The Data method returns the data of the execution result
25 | func (r *Result) Data() any {
26 | return r.data
27 | }
28 |
29 | // TryError 方法返回尝试执行时的错误
30 | // The TryError method returns the error when trying to execute
31 | func (r *Result) TryError() error {
32 | return r.tryError
33 | }
34 |
35 | // ExecErrors 方法返回所有执行错误的列表
36 | // The ExecErrors method returns a list of all execution errors
37 | func (r *Result) ExecErrors() []error {
38 | return r.execErrors
39 | }
40 |
41 | // IsSuccess 方法返回执行是否成功
42 | // The IsSuccess method returns whether the execution was successful
43 | func (r *Result) IsSuccess() bool {
44 | return r.tryError == nil
45 | }
46 |
47 | // LastExecError 方法返回最后一次执行的错误
48 | // The LastExecError method returns the error of the last execution
49 | func (r *Result) LastExecError() error {
50 | if len(r.execErrors) > 0 {
51 | return r.execErrors[len(r.execErrors)-1]
52 | }
53 | return ErrorExecErrNotFound
54 | }
55 |
56 | // FirstExecError 方法返回第一次执行的错误
57 | // The FirstExecError method returns the error of the first execution
58 | func (r *Result) FirstExecError() error {
59 | if len(r.execErrors) > 0 {
60 | return r.execErrors[0]
61 | }
62 | return ErrorExecErrNotFound
63 | }
64 |
65 | // ExecErrorByIndex 方法返回指定索引处的执行错误
66 | // The ExecErrorByIndex method returns the execution error at the specified index
67 | func (r *Result) ExecErrorByIndex(idx int) error {
68 | if len(r.execErrors) >= 0 && idx < len(r.execErrors) {
69 | return r.execErrors[idx]
70 | }
71 | return ErrorExecErrByIndexOutOfBound
72 | }
73 |
74 | // Count 方法返回执行的次数
75 | // The Count method returns the number of executions
76 | func (r *Result) Count() int64 {
77 | return int64(r.count)
78 | }
79 |
80 | // RetryableFunc 类型定义了一个可重试的函数
81 | // The RetryableFunc type defines a retryable function
82 | type RetryableFunc = func() (any, error)
83 |
84 | // Retry 结构体用于定义重试的配置
85 | // The Retry struct is used to define the retry configuration
86 | type Retry struct {
87 | config *Config // 重试的配置 Retry configuration
88 | }
89 |
90 | // New 函数用于创建一个新的 Retry 实例。它接受一个 Config 结构体作为参数,该结构体包含了重试的配置信息。
91 | // The New function is used to create a new Retry instance. It accepts a Config structure as a parameter, which contains the configuration information for retrying.
92 | func New(conf *Config) *Retry {
93 | conf = isConfigValid(conf)
94 | return &Retry{config: conf}
95 | }
96 |
97 | // TryOnConflict 方法尝试执行 fn 函数,如果遇到冲突则进行重试
98 | // The TryOnConflict method attempts to execute the fn function, and retries if a conflict is encountered
99 | func (r *Retry) TryOnConflict(fn RetryableFunc) *Result {
100 | // 如果 fn 函数为空,则返回 nil。这是因为没有函数可以执行,所以没有必要进行重试。
101 | // If the fn function is null, return nil. This is because there is no function to execute, so there is no need to retry.
102 | if fn == nil {
103 | return nil
104 | }
105 |
106 | // 创建一个新的定时器,定时器的延迟时间是 Config 中配置的延迟时间。定时器用于控制重试的间隔。
107 | // Create a new timer. The delay time of the timer is the delay time configured in Config. The timer is used to control the interval between retries.
108 | tr := time.NewTimer(r.config.delay)
109 |
110 | // 使用 defer 关键字确保定时器在函数结束时停止,避免资源泄露。
111 | // Use the defer keyword to ensure that the timer stops when the function ends, to avoid resource leaks.
112 | defer tr.Stop()
113 |
114 | // 创建一个新的 Result 实例来存储执行结果。Result 结构体包含了执行的结果和错误信息。
115 | // Create a new Result instance to store the execution result. The Result structure contains the execution result and error information.
116 | result := NewResult()
117 |
118 | // 循环尝试执行 fn 函数,直到满足退出条件
119 | // Loop to try to execute the fn function until the exit condition is met
120 | for {
121 | select {
122 | // 如果上下文已完成(例如,超时或手动取消),则将上下文的错误设置为结果的错误,并返回结果
123 | // If the context is done (for example, timeout or manually cancelled), set the error of the context as the error of the result and return the result
124 | case <-r.config.ctx.Done():
125 | result.tryError = r.config.ctx.Err()
126 | return result
127 |
128 | // 如果定时器到时,则尝试执行 fn 函数。定时器的时间间隔由 Config 中的退避函数和抖动决定。
129 | // If the timer is up, try to execute the fn function. The time interval of the timer is determined by the backoff function and jitter in Config.
130 | case <-tr.C:
131 | // 调用 fn 函数,获取返回的数据和错误
132 | // Call the fn function to get the returned data and error
133 | data, err := fn()
134 |
135 | // 增加执行次数
136 | // Increase the execution count
137 | result.count++
138 |
139 | // 如果没有错误,则返回结果
140 | // If there is no error, return the result
141 | if err == nil {
142 | // 将数据和错误(此时为 nil)设置到结果中
143 | // Set the data and error (which is nil at this time) to the result
144 | result.data = data
145 | result.tryError = err
146 |
147 | // 返回结果
148 | // Return the result
149 | return result
150 | }
151 |
152 | // 如果需要详细信息,则添加执行错误
153 | // If details are needed, add execution errors
154 | if r.config.detail {
155 | // 将错误添加到结果的执行错误列表中
156 | // Add the error to the execution error list of the result
157 | result.execErrors = append(result.execErrors, err)
158 | }
159 |
160 | // 如果不需要重试,则返回结果
161 | // If no retry is needed, return the result
162 | if !r.config.retryIfFunc(err) {
163 | // 将错误设置到结果中
164 | // Set the error to the result
165 | result.tryError = ErrorRetryIf
166 |
167 | // 返回结果
168 | // Return the result
169 | return result
170 | }
171 | // 计算下一次重试的延迟时间,这里使用了一个随机的抖动和重试次数的乘积作为因子
172 | // Calculate the delay time for the next retry, here a random jitter and the product of the number of retries are used as factors
173 | delay := int64(rand.Float64()*float64(r.config.jitter) + float64(result.count)*r.config.factor)
174 |
175 | // 如果计算出的延迟时间小于等于 0,则设置为默认的延迟时间
176 | // If the calculated delay time is less than or equal to 0, set it to the default delay time
177 | if delay <= 0 {
178 | delay = defaultDelayNum
179 | }
180 |
181 | // 计算退避时间,这里使用了配置中的退避函数和延迟时间
182 | // Calculate the backoff time, here the backoff function and delay time in the configuration are used
183 | backoff := r.config.backoffFunc(int64(delay)) + r.config.delay
184 |
185 | // 调用配置中的回调函数,传入重试次数、退避时间和错误
186 | // Call the callback function in the configuration, passing in the number of retries, backoff time, and error
187 | r.config.callback.OnRetry(int64(result.count), backoff, err)
188 |
189 | // 首先,我们检查特定错误的重试次数是否已经超过限制
190 | // First, we check if the retry count for a specific error has exceeded the limit
191 | // 如果错误次数超过限制,则返回结果
192 | // If the number of errors exceeds the limit, return the result
193 | if errAttempts, ok := r.config.attemptsByError[err]; ok {
194 | // 如果特定错误的重试次数已经用完,则返回一个错误,表示按错误类型的重试次数已经超过
195 | // If the retry count for a specific error has been used up, return an error indicating that the retry count by error type has been exceeded
196 | if errAttempts <= 0 {
197 | // 将错误设置到结果中,这个错误表示特定错误的重试次数已经超过了限制
198 | // Set the error to the result, this error indicates that the retry count for a specific error has exceeded the limit
199 | result.tryError = ErrorRetryAttemptsByErrorExceeded
200 |
201 | // 返回结果,这个结果包含了执行的次数、最后一次的错误和尝试的错误
202 | // Return the result, this result includes the number of executions, the last error, and the attempted error
203 | return result
204 | }
205 |
206 | // 如果还有剩余的重试次数,则减少一次重试次数,并更新到配置中
207 | // If there are remaining retry counts, decrease the retry count by one and update it in the configuration
208 | errAttempts--
209 | r.config.attemptsByError[err] = errAttempts
210 | }
211 |
212 | // 然后,我们检查总的执行次数是否已经超过限制
213 | // Then, we check if the total number of executions has exceeded the limit
214 | // 如果执行次数超过限制,则返回结果
215 | // If the number of executions exceeds the limit, return the result
216 | if result.count >= r.config.attempts {
217 | // 将错误设置到结果中,这个错误表示总的执行次数已经超过了限制
218 | // Set the error to the result, this error indicates that the total number of executions has exceeded the limit
219 | result.tryError = ErrorRetryAttemptsExceeded
220 |
221 | // 返回结果,这个结果包含了执行的次数、最后一次的错误和尝试的错误
222 | // Return the result, this result includes the number of executions, the last error, and the attempted error
223 | return result
224 | }
225 |
226 | // 重置定时器
227 | // Reset the timer
228 | tr.Reset(backoff)
229 | }
230 | }
231 | }
232 |
233 | // TryOnConflict 方法尝试执行 RetryableFunc 函数,如果发生冲突,则进行重试
234 | // The TryOnConflict method tries to execute the RetryableFunc function, and retries if a conflict occurs
235 | func (r *Retry) TryOnConflictVal(fn RetryableFunc) RetryResult {
236 | return r.TryOnConflict(fn)
237 | }
238 |
239 | // Do 函数尝试执行 fn 函数,如果遇到冲突则根据 conf 配置进行重试
240 | // The Do function attempts to execute the fn function, and retries according to the conf configuration if a conflict is encountered
241 | func Do(fn RetryableFunc, conf *Config) RetryResult {
242 | // 创建一个新的 Retry 实例并尝试执行 fn 函数
243 | // Create a new Retry instance and try to execute the fn function
244 | return New(conf).TryOnConflict(fn)
245 | }
246 |
247 | // DoWithDefault 函数尝试执行 fn 函数,如果遇到冲突则使用默认配置进行重试
248 | // The DoWithDefault function attempts to execute the fn function, and retries with the default configuration if a conflict is encountered
249 | func DoWithDefault(fn RetryableFunc) RetryResult {
250 | // 创建一个新的 Retry 实例并尝试执行 fn 函数
251 | // Create a new Retry instance and try to execute the fn function
252 | return New(nil).TryOnConflict(fn)
253 | }
254 |
--------------------------------------------------------------------------------
/retry_test.go:
--------------------------------------------------------------------------------
1 | package retry
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "math"
8 | "sync"
9 | "testing"
10 | "time"
11 |
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | type callback struct{}
16 |
17 | func (cb *callback) OnRetry(count int64, delay time.Duration, err error) {
18 | fmt.Println("OnRetry", count, delay.String(), err)
19 | }
20 |
21 | func TestRetry_Do(t *testing.T) {
22 | m := map[error]uint64{}
23 | e := errors.New("test")
24 | m[e] = 1
25 |
26 | cfg := NewConfig().WithAttemptsByError(m).WithDetail(true)
27 |
28 | count := 0
29 | testFunc := func() (any, error) {
30 | if count > 0 {
31 | return "lee", nil
32 | } else {
33 | count++
34 | return nil, e
35 | }
36 | }
37 |
38 | result := Do(testFunc, cfg)
39 | assert.NotNil(t, result)
40 |
41 | assert.Equal(t, result.IsSuccess(), true)
42 | assert.Equal(t, result.LastExecError(), e)
43 | assert.Equal(t, result.FirstExecError(), e)
44 | assert.Equal(t, result.ExecErrors(), []error{e})
45 | assert.Equal(t, result.Data(), "lee")
46 | assert.Equal(t, result.Count(), int64(2))
47 |
48 | }
49 |
50 | func TestRetry_DoWithDefault(t *testing.T) {
51 | m := map[error]uint64{}
52 | e := errors.New("test")
53 | m[e] = 1
54 |
55 | count := 0
56 | testFunc := func() (any, error) {
57 | if count > 0 {
58 | return "lee", nil
59 | } else {
60 | count++
61 | return nil, e
62 | }
63 | }
64 |
65 | result := DoWithDefault(testFunc)
66 | assert.NotNil(t, result)
67 |
68 | assert.Equal(t, result.IsSuccess(), true)
69 | assert.Equal(t, result.LastExecError(), ErrorExecErrNotFound)
70 | assert.Equal(t, result.FirstExecError(), ErrorExecErrNotFound)
71 | assert.Equal(t, result.ExecErrors(), []error{})
72 | assert.Equal(t, result.Data(), "lee")
73 | assert.Equal(t, result.Count(), int64(2))
74 | }
75 |
76 | func TestRetry_TryOnConflictSuccess(t *testing.T) {
77 | r := New(nil)
78 | assert.NotNil(t, r)
79 |
80 | testFunc := func() (any, error) {
81 | return "lee", nil
82 | }
83 |
84 | result := r.TryOnConflictVal(testFunc)
85 | assert.NotNil(t, result)
86 |
87 | assert.Equal(t, result.IsSuccess(), true)
88 | assert.Equal(t, result.Data(), "lee")
89 | assert.Equal(t, result.Count(), int64(1))
90 | }
91 | func TestRetry_TryOnConflictContext(t *testing.T) {
92 | ctx, cancel := context.WithCancel(context.Background())
93 | cancel()
94 |
95 | cfg := NewConfig().WithContext(ctx)
96 |
97 | r := New(cfg)
98 | assert.NotNil(t, r)
99 |
100 | testFunc := func() (any, error) {
101 | return nil, errors.New("test")
102 | }
103 |
104 | result := r.TryOnConflictVal(testFunc)
105 | assert.NotNil(t, result)
106 |
107 | assert.Equal(t, result.TryError(), context.Canceled)
108 | assert.Equal(t, result.Count(), int64(0))
109 | }
110 |
111 | func TestRetry_TryOnConflictCancelContext(t *testing.T) {
112 | ctx, cancel := context.WithCancel(context.Background())
113 | cancel()
114 |
115 | cfg := NewConfig().WithContext(ctx)
116 |
117 | r := New(cfg)
118 | assert.NotNil(t, r)
119 |
120 | testFunc := func() (any, error) {
121 | return "lee", nil
122 | }
123 |
124 | result := r.TryOnConflictVal(testFunc)
125 | assert.NotNil(t, result)
126 |
127 | assert.Equal(t, result.TryError(), context.Canceled)
128 | assert.Equal(t, result.Count(), int64(0))
129 | }
130 |
131 | func TestRetry_TryOnConflictCallback(t *testing.T) {
132 | cfg := NewConfig().WithDetail(true).WithAttempts(5).WithCallback(&callback{})
133 | e := errors.New("test")
134 |
135 | r := New(cfg)
136 | assert.NotNil(t, r)
137 |
138 | testFunc := func() (any, error) {
139 | return nil, e
140 | }
141 |
142 | result := r.TryOnConflictVal(testFunc)
143 | assert.NotNil(t, result)
144 |
145 | assert.Equal(t, result.IsSuccess(), false)
146 | assert.Equal(t, result.LastExecError(), e)
147 | assert.Equal(t, result.FirstExecError(), e)
148 | assert.Equal(t, result.ExecErrors(), []error{e, e, e, e, e})
149 | assert.Equal(t, result.TryError(), ErrorRetryAttemptsExceeded)
150 | assert.Equal(t, result.Count(), int64(5))
151 | }
152 |
153 | func TestRetry_TryOnConflictRetryIf(t *testing.T) {
154 | e := errors.New("test")
155 |
156 | retryIf := func(err error) bool {
157 | return !errors.Is(err, e)
158 | }
159 |
160 | cfg := NewConfig().WithRetryIfFunc(retryIf)
161 |
162 | r := New(cfg)
163 | assert.NotNil(t, r)
164 |
165 | testFunc := func() (any, error) {
166 | return nil, e
167 | }
168 |
169 | result := r.TryOnConflictVal(testFunc)
170 | assert.NotNil(t, result)
171 |
172 | assert.Equal(t, result.TryError(), ErrorRetryIf)
173 | assert.Equal(t, result.Count(), int64(1))
174 | }
175 |
176 | func TestRetry_TryOnConflictRetryIfExceeded(t *testing.T) {
177 | cfg := NewConfig().WithAttempts(2)
178 |
179 | r := New(cfg)
180 | assert.NotNil(t, r)
181 |
182 | testFunc := func() (any, error) {
183 | return nil, errors.New("test")
184 | }
185 |
186 | result := r.TryOnConflictVal(testFunc)
187 | assert.NotNil(t, result)
188 |
189 | assert.Equal(t, result.TryError(), ErrorRetryAttemptsExceeded)
190 | assert.Equal(t, result.Count(), int64(2))
191 | }
192 |
193 | func TestRetry_TryOnConflictAttemptsByError(t *testing.T) {
194 | m := map[error]uint64{}
195 | e := errors.New("test")
196 | m[e] = 1
197 |
198 | cfg := NewConfig().WithAttemptsByError(m)
199 |
200 | r := New(cfg)
201 | assert.NotNil(t, r)
202 |
203 | testFunc := func() (any, error) {
204 | return nil, e
205 | }
206 |
207 | result := r.TryOnConflictVal(testFunc)
208 | assert.NotNil(t, result)
209 |
210 | assert.Equal(t, result.TryError(), ErrorRetryAttemptsByErrorExceeded)
211 | assert.Equal(t, result.Count(), int64(2))
212 | }
213 |
214 | func TestRetry_TryOnConflictAttemptsExceeded(t *testing.T) {
215 | cfg := NewConfig().WithAttempts(2)
216 |
217 | r := New(cfg)
218 | assert.NotNil(t, r)
219 |
220 | testFunc := func() (any, error) {
221 | return nil, errors.New("test")
222 | }
223 |
224 | result := r.TryOnConflictVal(testFunc)
225 | assert.NotNil(t, result)
226 |
227 | assert.Equal(t, result.TryError(), ErrorRetryAttemptsExceeded)
228 | assert.Equal(t, result.Count(), int64(2))
229 | }
230 |
231 | func TestRetry_TryOnConflictMultiRetryableFuncs(t *testing.T) {
232 | cfg := NewConfig().WithCallback(&callback{})
233 |
234 | r := New(cfg)
235 | assert.NotNil(t, r)
236 |
237 | testFunc1 := func() (any, error) {
238 | return nil, errors.New("testFunc1")
239 | }
240 |
241 | testFunc2 := func() (any, error) {
242 | return nil, errors.New("testFunc2")
243 | }
244 |
245 | result := r.TryOnConflictVal(testFunc1)
246 | assert.NotNil(t, result)
247 | assert.Equal(t, result.TryError(), ErrorRetryAttemptsExceeded)
248 | assert.Equal(t, result.Count(), int64(defaultAttempts))
249 |
250 | result = r.TryOnConflictVal(testFunc2)
251 | assert.NotNil(t, result)
252 | assert.Equal(t, result.TryError(), ErrorRetryAttemptsExceeded)
253 | assert.Equal(t, result.Count(), int64(defaultAttempts))
254 | }
255 |
256 | func TestRetry_TryOnConflictMultiRetryableFuncsParallel(t *testing.T) {
257 | cfg := NewConfig().WithCallback(&callback{})
258 |
259 | r := New(cfg)
260 | assert.NotNil(t, r)
261 |
262 | testFunc1 := func() (any, error) {
263 | return nil, errors.New("testFunc1")
264 | }
265 |
266 | testFunc2 := func() (any, error) {
267 | return nil, errors.New("testFunc2")
268 | }
269 |
270 | wg := sync.WaitGroup{}
271 | wg.Add(2)
272 |
273 | go func() {
274 | defer wg.Done()
275 | result1 := r.TryOnConflictVal(testFunc1)
276 | assert.NotNil(t, result1)
277 | assert.Equal(t, result1.TryError(), ErrorRetryAttemptsExceeded)
278 | assert.Equal(t, result1.Count(), int64(defaultAttempts))
279 | }()
280 |
281 | go func() {
282 | defer wg.Done()
283 | result2 := r.TryOnConflictVal(testFunc2)
284 | assert.NotNil(t, result2)
285 | assert.Equal(t, result2.TryError(), ErrorRetryAttemptsExceeded)
286 | assert.Equal(t, result2.Count(), int64(defaultAttempts))
287 | }()
288 |
289 | wg.Wait()
290 | }
291 |
292 | func TestRetry_ZeroConfig(t *testing.T) {
293 | cfg := NewConfig()
294 | r := New(cfg)
295 | assert.NotNil(t, r)
296 |
297 | result := r.TryOnConflictVal(func() (any, error) {
298 | return nil, nil
299 | })
300 |
301 | assert.True(t, result.IsSuccess())
302 | assert.Equal(t, int64(1), result.Count())
303 | }
304 |
305 | func TestRetry_MultipleErrorTypes(t *testing.T) {
306 | errType1 := errors.New("type1")
307 | errType2 := errors.New("type2")
308 |
309 | m := map[error]uint64{
310 | errType1: 2,
311 | errType2: 3,
312 | }
313 |
314 | cfg := NewConfig().WithAttemptsByError(m).WithDetail(true)
315 | r := New(cfg)
316 |
317 | count := 0
318 | result := r.TryOnConflictVal(func() (any, error) {
319 | count++
320 | if count <= 3 {
321 | return nil, errType1
322 | }
323 | return nil, errType2
324 | })
325 |
326 | assert.Equal(t, int64(3), result.Count())
327 | assert.Equal(t, ErrorRetryAttemptsByErrorExceeded, result.TryError())
328 |
329 | errors := result.ExecErrors()
330 | assert.NotNil(t, errors)
331 | assert.NotEmpty(t, errors)
332 |
333 | assert.Equal(t, 3, len(errors))
334 | for _, err := range errors {
335 | assert.Equal(t, errType1, err)
336 | }
337 | }
338 |
339 | func TestRetry_ConcurrentStress(t *testing.T) {
340 | cfg := NewConfig().WithAttempts(5)
341 | r := New(cfg)
342 |
343 | var wg sync.WaitGroup
344 | concurrent := 10
345 | wg.Add(concurrent)
346 |
347 | for i := 0; i < concurrent; i++ {
348 | go func(id int) {
349 | defer wg.Done()
350 | result := r.TryOnConflictVal(func() (any, error) {
351 | if id%2 == 0 {
352 | return fmt.Sprintf("success-%d", id), nil
353 | }
354 | return nil, fmt.Errorf("error-%d", id)
355 | })
356 |
357 | if id%2 == 0 {
358 | assert.True(t, result.IsSuccess())
359 | assert.Equal(t, fmt.Sprintf("success-%d", id), result.Data())
360 | } else {
361 | assert.False(t, result.IsSuccess())
362 | }
363 | }(i)
364 | }
365 |
366 | wg.Wait()
367 | }
368 |
369 | func TestRetry_ConfigEdgeCases(t *testing.T) {
370 | tests := []struct {
371 | name string
372 | cfg *Config
373 | expected error
374 | }{
375 | {
376 | name: "zero attempts",
377 | cfg: NewConfig().WithAttempts(0),
378 | expected: ErrorRetryAttemptsExceeded,
379 | },
380 | {
381 | name: "max attempts",
382 | cfg: NewConfig().WithAttempts(math.MaxUint64),
383 | expected: ErrorRetryAttemptsExceeded,
384 | },
385 | }
386 |
387 | for _, tt := range tests {
388 | t.Run(tt.name, func(t *testing.T) {
389 | r := New(tt.cfg)
390 | result := r.TryOnConflictVal(func() (any, error) {
391 | return nil, errors.New("test")
392 | })
393 | assert.Equal(t, tt.expected, result.TryError())
394 | })
395 | }
396 | }
397 |
--------------------------------------------------------------------------------