├── .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 | logo 7 |
8 | 9 | [![Go Report Card](https://goreportcard.com/badge/github.com/shengyanli1982/retry)](https://goreportcard.com/report/github.com/shengyanli1982/retry) 10 | [![Build Status](https://github.com/shengyanli1982/retry/actions/workflows/test.yaml/badge.svg)](github.com/shengyanli1982/retry/actions) 11 | [![Go Reference](https://pkg.go.dev/badge/github.com/shengyanli1982/retry.svg)](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 | logo 7 |
8 | 9 | [![Go Report Card](https://goreportcard.com/badge/github.com/shengyanli1982/retry)](https://goreportcard.com/report/github.com/shengyanli1982/retry) 10 | [![Build Status](https://github.com/shengyanli1982/retry/actions/workflows/test.yaml/badge.svg)](github.com/shengyanli1982/retry/actions) 11 | [![Go Reference](https://pkg.go.dev/badge/github.com/shengyanli1982/retry.svg)](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 | --------------------------------------------------------------------------------