├── .github └── workflows │ └── pull_request.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples └── hello_world.rs └── src ├── backoff.rs ├── jitter.rs └── lib.rs /.github/workflows/pull_request.yaml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | paths: 9 | - Cargo.toml 10 | - '**/Cargo.toml' 11 | - Cargo.lock 12 | - '**.rs' 13 | 14 | jobs: 15 | test: 16 | name: Test 17 | runs-on: ubuntu-24.04 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: dtolnay/rust-toolchain@stable 22 | - uses: Swatinem/rust-cache@v2 23 | with: 24 | key: rust-toolchain-cache 25 | - name: Run tests 26 | run: cargo test --all 27 | 28 | fmt: 29 | name: Rustfmt 30 | runs-on: ubuntu-24.04 31 | steps: 32 | - uses: actions/checkout@v3 33 | - uses: dtolnay/rust-toolchain@stable 34 | with: 35 | components: rustfmt 36 | - uses: Swatinem/rust-cache@v2 37 | with: 38 | key: rust-toolchain-cache 39 | - name: Version Check 40 | run: cargo fmt --version 41 | - name: Enforce formatting 42 | run: cargo fmt --check 43 | 44 | clippy: 45 | name: Clippy 46 | runs-on: ubuntu-24.04 47 | steps: 48 | - uses: actions/checkout@v3 49 | - uses: dtolnay/rust-toolchain@stable 50 | with: 51 | components: clippy 52 | - uses: Swatinem/rust-cache@v2 53 | with: 54 | key: rust-toolchain-cache 55 | - name: Version Check 56 | run: cargo clippy --version 57 | - name: Linting 58 | run: cargo clippy --all-targets -- -D warnings 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | # - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/doublify/pre-commit-rust 9 | rev: v1.0 10 | hooks: 11 | - id: fmt 12 | - id: clippy 13 | args: [ --all-targets, --, -D, clippy::all ] 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v0.2.0 4 | 5 | e84b1e0 feat: add `.on_retry(fn)` 6 | bc1c1dd fix: use `license` instead of `license-file` field 7 | 87d22fb feat: remove mutual exclusion of tokio and async-std features 8 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "async-channel" 22 | version = "1.9.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" 25 | dependencies = [ 26 | "concurrent-queue", 27 | "event-listener 2.5.3", 28 | "futures-core", 29 | ] 30 | 31 | [[package]] 32 | name = "async-channel" 33 | version = "2.3.1" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" 36 | dependencies = [ 37 | "concurrent-queue", 38 | "event-listener-strategy", 39 | "futures-core", 40 | "pin-project-lite", 41 | ] 42 | 43 | [[package]] 44 | name = "async-executor" 45 | version = "1.13.1" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" 48 | dependencies = [ 49 | "async-task", 50 | "concurrent-queue", 51 | "fastrand", 52 | "futures-lite", 53 | "slab", 54 | ] 55 | 56 | [[package]] 57 | name = "async-global-executor" 58 | version = "2.4.1" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" 61 | dependencies = [ 62 | "async-channel 2.3.1", 63 | "async-executor", 64 | "async-io", 65 | "async-lock", 66 | "blocking", 67 | "futures-lite", 68 | "once_cell", 69 | ] 70 | 71 | [[package]] 72 | name = "async-io" 73 | version = "2.3.4" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" 76 | dependencies = [ 77 | "async-lock", 78 | "cfg-if", 79 | "concurrent-queue", 80 | "futures-io", 81 | "futures-lite", 82 | "parking", 83 | "polling", 84 | "rustix", 85 | "slab", 86 | "tracing", 87 | "windows-sys 0.59.0", 88 | ] 89 | 90 | [[package]] 91 | name = "async-lock" 92 | version = "3.4.0" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" 95 | dependencies = [ 96 | "event-listener 5.3.1", 97 | "event-listener-strategy", 98 | "pin-project-lite", 99 | ] 100 | 101 | [[package]] 102 | name = "async-std" 103 | version = "1.13.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" 106 | dependencies = [ 107 | "async-channel 1.9.0", 108 | "async-global-executor", 109 | "async-io", 110 | "async-lock", 111 | "crossbeam-utils", 112 | "futures-channel", 113 | "futures-core", 114 | "futures-io", 115 | "futures-lite", 116 | "gloo-timers", 117 | "kv-log-macro", 118 | "log", 119 | "memchr", 120 | "once_cell", 121 | "pin-project-lite", 122 | "pin-utils", 123 | "slab", 124 | "wasm-bindgen-futures", 125 | ] 126 | 127 | [[package]] 128 | name = "async-task" 129 | version = "4.7.1" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" 132 | 133 | [[package]] 134 | name = "atomic-waker" 135 | version = "1.1.2" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 138 | 139 | [[package]] 140 | name = "autocfg" 141 | version = "1.4.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 144 | 145 | [[package]] 146 | name = "backtrace" 147 | version = "0.3.74" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 150 | dependencies = [ 151 | "addr2line", 152 | "cfg-if", 153 | "libc", 154 | "miniz_oxide", 155 | "object", 156 | "rustc-demangle", 157 | "windows-targets", 158 | ] 159 | 160 | [[package]] 161 | name = "bitflags" 162 | version = "2.6.0" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 165 | 166 | [[package]] 167 | name = "blocking" 168 | version = "1.6.1" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" 171 | dependencies = [ 172 | "async-channel 2.3.1", 173 | "async-task", 174 | "futures-io", 175 | "futures-lite", 176 | "piper", 177 | ] 178 | 179 | [[package]] 180 | name = "bumpalo" 181 | version = "3.16.0" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 184 | 185 | [[package]] 186 | name = "byteorder" 187 | version = "1.5.0" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 190 | 191 | [[package]] 192 | name = "bytes" 193 | version = "1.8.0" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" 196 | 197 | [[package]] 198 | name = "cfg-if" 199 | version = "1.0.0" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 202 | 203 | [[package]] 204 | name = "concurrent-queue" 205 | version = "2.5.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 208 | dependencies = [ 209 | "crossbeam-utils", 210 | ] 211 | 212 | [[package]] 213 | name = "crossbeam-utils" 214 | version = "0.8.20" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 217 | 218 | [[package]] 219 | name = "errno" 220 | version = "0.3.9" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 223 | dependencies = [ 224 | "libc", 225 | "windows-sys 0.52.0", 226 | ] 227 | 228 | [[package]] 229 | name = "event-listener" 230 | version = "2.5.3" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" 233 | 234 | [[package]] 235 | name = "event-listener" 236 | version = "5.3.1" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" 239 | dependencies = [ 240 | "concurrent-queue", 241 | "parking", 242 | "pin-project-lite", 243 | ] 244 | 245 | [[package]] 246 | name = "event-listener-strategy" 247 | version = "0.5.2" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" 250 | dependencies = [ 251 | "event-listener 5.3.1", 252 | "pin-project-lite", 253 | ] 254 | 255 | [[package]] 256 | name = "fastrand" 257 | version = "2.1.1" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" 260 | 261 | [[package]] 262 | name = "futures-channel" 263 | version = "0.3.31" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 266 | dependencies = [ 267 | "futures-core", 268 | ] 269 | 270 | [[package]] 271 | name = "futures-core" 272 | version = "0.3.31" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 275 | 276 | [[package]] 277 | name = "futures-io" 278 | version = "0.3.31" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 281 | 282 | [[package]] 283 | name = "futures-lite" 284 | version = "2.3.0" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" 287 | dependencies = [ 288 | "fastrand", 289 | "futures-core", 290 | "futures-io", 291 | "parking", 292 | "pin-project-lite", 293 | ] 294 | 295 | [[package]] 296 | name = "getrandom" 297 | version = "0.2.15" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 300 | dependencies = [ 301 | "cfg-if", 302 | "libc", 303 | "wasi", 304 | ] 305 | 306 | [[package]] 307 | name = "gimli" 308 | version = "0.31.1" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 311 | 312 | [[package]] 313 | name = "gloo-timers" 314 | version = "0.3.0" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" 317 | dependencies = [ 318 | "futures-channel", 319 | "futures-core", 320 | "js-sys", 321 | "wasm-bindgen", 322 | ] 323 | 324 | [[package]] 325 | name = "hermit-abi" 326 | version = "0.3.9" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 329 | 330 | [[package]] 331 | name = "hermit-abi" 332 | version = "0.4.0" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 335 | 336 | [[package]] 337 | name = "js-sys" 338 | version = "0.3.72" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" 341 | dependencies = [ 342 | "wasm-bindgen", 343 | ] 344 | 345 | [[package]] 346 | name = "kv-log-macro" 347 | version = "1.0.7" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" 350 | dependencies = [ 351 | "log", 352 | ] 353 | 354 | [[package]] 355 | name = "libc" 356 | version = "0.2.161" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" 359 | 360 | [[package]] 361 | name = "linux-raw-sys" 362 | version = "0.4.14" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 365 | 366 | [[package]] 367 | name = "lock_api" 368 | version = "0.4.12" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 371 | dependencies = [ 372 | "autocfg", 373 | "scopeguard", 374 | ] 375 | 376 | [[package]] 377 | name = "log" 378 | version = "0.4.22" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 381 | dependencies = [ 382 | "value-bag", 383 | ] 384 | 385 | [[package]] 386 | name = "memchr" 387 | version = "2.7.4" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 390 | 391 | [[package]] 392 | name = "miniz_oxide" 393 | version = "0.8.0" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 396 | dependencies = [ 397 | "adler2", 398 | ] 399 | 400 | [[package]] 401 | name = "mio" 402 | version = "1.0.2" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 405 | dependencies = [ 406 | "hermit-abi 0.3.9", 407 | "libc", 408 | "wasi", 409 | "windows-sys 0.52.0", 410 | ] 411 | 412 | [[package]] 413 | name = "mulligan" 414 | version = "0.3.0" 415 | dependencies = [ 416 | "async-std", 417 | "rand", 418 | "tokio", 419 | ] 420 | 421 | [[package]] 422 | name = "object" 423 | version = "0.36.5" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" 426 | dependencies = [ 427 | "memchr", 428 | ] 429 | 430 | [[package]] 431 | name = "once_cell" 432 | version = "1.20.2" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 435 | 436 | [[package]] 437 | name = "parking" 438 | version = "2.2.1" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 441 | 442 | [[package]] 443 | name = "parking_lot" 444 | version = "0.12.3" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 447 | dependencies = [ 448 | "lock_api", 449 | "parking_lot_core", 450 | ] 451 | 452 | [[package]] 453 | name = "parking_lot_core" 454 | version = "0.9.10" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 457 | dependencies = [ 458 | "cfg-if", 459 | "libc", 460 | "redox_syscall", 461 | "smallvec", 462 | "windows-targets", 463 | ] 464 | 465 | [[package]] 466 | name = "pin-project-lite" 467 | version = "0.2.15" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" 470 | 471 | [[package]] 472 | name = "pin-utils" 473 | version = "0.1.0" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 476 | 477 | [[package]] 478 | name = "piper" 479 | version = "0.2.4" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" 482 | dependencies = [ 483 | "atomic-waker", 484 | "fastrand", 485 | "futures-io", 486 | ] 487 | 488 | [[package]] 489 | name = "polling" 490 | version = "3.7.3" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" 493 | dependencies = [ 494 | "cfg-if", 495 | "concurrent-queue", 496 | "hermit-abi 0.4.0", 497 | "pin-project-lite", 498 | "rustix", 499 | "tracing", 500 | "windows-sys 0.59.0", 501 | ] 502 | 503 | [[package]] 504 | name = "ppv-lite86" 505 | version = "0.2.20" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 508 | dependencies = [ 509 | "zerocopy", 510 | ] 511 | 512 | [[package]] 513 | name = "proc-macro2" 514 | version = "1.0.89" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 517 | dependencies = [ 518 | "unicode-ident", 519 | ] 520 | 521 | [[package]] 522 | name = "quote" 523 | version = "1.0.37" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 526 | dependencies = [ 527 | "proc-macro2", 528 | ] 529 | 530 | [[package]] 531 | name = "rand" 532 | version = "0.8.5" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 535 | dependencies = [ 536 | "libc", 537 | "rand_chacha", 538 | "rand_core", 539 | ] 540 | 541 | [[package]] 542 | name = "rand_chacha" 543 | version = "0.3.1" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 546 | dependencies = [ 547 | "ppv-lite86", 548 | "rand_core", 549 | ] 550 | 551 | [[package]] 552 | name = "rand_core" 553 | version = "0.6.4" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 556 | dependencies = [ 557 | "getrandom", 558 | ] 559 | 560 | [[package]] 561 | name = "redox_syscall" 562 | version = "0.5.7" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 565 | dependencies = [ 566 | "bitflags", 567 | ] 568 | 569 | [[package]] 570 | name = "rustc-demangle" 571 | version = "0.1.24" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 574 | 575 | [[package]] 576 | name = "rustix" 577 | version = "0.38.37" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" 580 | dependencies = [ 581 | "bitflags", 582 | "errno", 583 | "libc", 584 | "linux-raw-sys", 585 | "windows-sys 0.52.0", 586 | ] 587 | 588 | [[package]] 589 | name = "scopeguard" 590 | version = "1.2.0" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 593 | 594 | [[package]] 595 | name = "signal-hook-registry" 596 | version = "1.4.2" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 599 | dependencies = [ 600 | "libc", 601 | ] 602 | 603 | [[package]] 604 | name = "slab" 605 | version = "0.4.9" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 608 | dependencies = [ 609 | "autocfg", 610 | ] 611 | 612 | [[package]] 613 | name = "smallvec" 614 | version = "1.13.2" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 617 | 618 | [[package]] 619 | name = "socket2" 620 | version = "0.5.7" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 623 | dependencies = [ 624 | "libc", 625 | "windows-sys 0.52.0", 626 | ] 627 | 628 | [[package]] 629 | name = "syn" 630 | version = "2.0.85" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" 633 | dependencies = [ 634 | "proc-macro2", 635 | "quote", 636 | "unicode-ident", 637 | ] 638 | 639 | [[package]] 640 | name = "tokio" 641 | version = "1.41.0" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" 644 | dependencies = [ 645 | "backtrace", 646 | "bytes", 647 | "libc", 648 | "mio", 649 | "parking_lot", 650 | "pin-project-lite", 651 | "signal-hook-registry", 652 | "socket2", 653 | "tokio-macros", 654 | "windows-sys 0.52.0", 655 | ] 656 | 657 | [[package]] 658 | name = "tokio-macros" 659 | version = "2.4.0" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 662 | dependencies = [ 663 | "proc-macro2", 664 | "quote", 665 | "syn", 666 | ] 667 | 668 | [[package]] 669 | name = "tracing" 670 | version = "0.1.40" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 673 | dependencies = [ 674 | "pin-project-lite", 675 | "tracing-core", 676 | ] 677 | 678 | [[package]] 679 | name = "tracing-core" 680 | version = "0.1.32" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 683 | 684 | [[package]] 685 | name = "unicode-ident" 686 | version = "1.0.13" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 689 | 690 | [[package]] 691 | name = "value-bag" 692 | version = "1.10.0" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" 695 | 696 | [[package]] 697 | name = "wasi" 698 | version = "0.11.0+wasi-snapshot-preview1" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 701 | 702 | [[package]] 703 | name = "wasm-bindgen" 704 | version = "0.2.95" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" 707 | dependencies = [ 708 | "cfg-if", 709 | "once_cell", 710 | "wasm-bindgen-macro", 711 | ] 712 | 713 | [[package]] 714 | name = "wasm-bindgen-backend" 715 | version = "0.2.95" 716 | source = "registry+https://github.com/rust-lang/crates.io-index" 717 | checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" 718 | dependencies = [ 719 | "bumpalo", 720 | "log", 721 | "once_cell", 722 | "proc-macro2", 723 | "quote", 724 | "syn", 725 | "wasm-bindgen-shared", 726 | ] 727 | 728 | [[package]] 729 | name = "wasm-bindgen-futures" 730 | version = "0.4.45" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" 733 | dependencies = [ 734 | "cfg-if", 735 | "js-sys", 736 | "wasm-bindgen", 737 | "web-sys", 738 | ] 739 | 740 | [[package]] 741 | name = "wasm-bindgen-macro" 742 | version = "0.2.95" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" 745 | dependencies = [ 746 | "quote", 747 | "wasm-bindgen-macro-support", 748 | ] 749 | 750 | [[package]] 751 | name = "wasm-bindgen-macro-support" 752 | version = "0.2.95" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" 755 | dependencies = [ 756 | "proc-macro2", 757 | "quote", 758 | "syn", 759 | "wasm-bindgen-backend", 760 | "wasm-bindgen-shared", 761 | ] 762 | 763 | [[package]] 764 | name = "wasm-bindgen-shared" 765 | version = "0.2.95" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" 768 | 769 | [[package]] 770 | name = "web-sys" 771 | version = "0.3.72" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" 774 | dependencies = [ 775 | "js-sys", 776 | "wasm-bindgen", 777 | ] 778 | 779 | [[package]] 780 | name = "windows-sys" 781 | version = "0.52.0" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 784 | dependencies = [ 785 | "windows-targets", 786 | ] 787 | 788 | [[package]] 789 | name = "windows-sys" 790 | version = "0.59.0" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 793 | dependencies = [ 794 | "windows-targets", 795 | ] 796 | 797 | [[package]] 798 | name = "windows-targets" 799 | version = "0.52.6" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 802 | dependencies = [ 803 | "windows_aarch64_gnullvm", 804 | "windows_aarch64_msvc", 805 | "windows_i686_gnu", 806 | "windows_i686_gnullvm", 807 | "windows_i686_msvc", 808 | "windows_x86_64_gnu", 809 | "windows_x86_64_gnullvm", 810 | "windows_x86_64_msvc", 811 | ] 812 | 813 | [[package]] 814 | name = "windows_aarch64_gnullvm" 815 | version = "0.52.6" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 818 | 819 | [[package]] 820 | name = "windows_aarch64_msvc" 821 | version = "0.52.6" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 824 | 825 | [[package]] 826 | name = "windows_i686_gnu" 827 | version = "0.52.6" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 830 | 831 | [[package]] 832 | name = "windows_i686_gnullvm" 833 | version = "0.52.6" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 836 | 837 | [[package]] 838 | name = "windows_i686_msvc" 839 | version = "0.52.6" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 842 | 843 | [[package]] 844 | name = "windows_x86_64_gnu" 845 | version = "0.52.6" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 848 | 849 | [[package]] 850 | name = "windows_x86_64_gnullvm" 851 | version = "0.52.6" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 854 | 855 | [[package]] 856 | name = "windows_x86_64_msvc" 857 | version = "0.52.6" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 860 | 861 | [[package]] 862 | name = "zerocopy" 863 | version = "0.7.35" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 866 | dependencies = [ 867 | "byteorder", 868 | "zerocopy-derive", 869 | ] 870 | 871 | [[package]] 872 | name = "zerocopy-derive" 873 | version = "0.7.35" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 876 | dependencies = [ 877 | "proc-macro2", 878 | "quote", 879 | "syn", 880 | ] 881 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mulligan" 3 | version = "0.3.0" 4 | edition = "2021" 5 | description = "A flexible retry library for Rust async operations with configurable backoff strategies and jitter." 6 | license = "MIT" 7 | homepage = "https://github.com/theelderbeever/mulligan" 8 | repository = "https://github.com/theelderbeever/mulligan" 9 | documentation = "https://docs.rs/mulligan" 10 | 11 | [features] 12 | default = ["tokio"] # Make tokio the default runtime 13 | tokio = ["dep:tokio"] # Depend on tokio when this feature is enabled 14 | async-std = ["dep:async-std"] # Depend on async-std when this feature is enabled 15 | 16 | [dependencies] 17 | tokio = { version = "1", optional = true, features = ["time"] } 18 | async-std = { version = "1", optional = true } 19 | rand = { version = "0.8"} 20 | 21 | [[example]] 22 | name = "hello_world" 23 | required-features = ["tokio"] 24 | 25 | [example.hello_world.dependencies] 26 | tokio = { version = "1", features = ["full"] } 27 | 28 | [dev-dependencies] 29 | tokio = { version = "1", features = ["full"]} 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Taylor Beever 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 | # mulligan 2 | 3 | A flexible retry library for Rust async operations with configurable backoff strategies and jitter. 4 | 5 | [![Crates.io](https://img.shields.io/crates/v/mulligan.svg)](https://crates.io/crates/mulligan) 6 | [![Documentation](https://docs.rs/mulligan/badge.svg)](https://docs.rs/mulligan) 7 | 8 | `mulligan` provides a fluent API for retrying async operations with customizable retry policies, backoff strategies, and jitter. It supports both `tokio` and `async-std` runtimes. 9 | 10 | ## Features 11 | 12 | - Multiple backoff strategies: 13 | - Fixed delay 14 | - Linear backoff 15 | - Exponential backoff 16 | - Configurable jitter options: 17 | - Full jitter 18 | - Equal jitter 19 | - Decorrelated jitter 20 | - Maximum retry attempts 21 | - Maximum delay caps 22 | - Custom retry conditions 23 | - Async runtime support: 24 | - `tokio` (via `tokio` feature) 25 | - `async-std` (via `async-std` feature) 26 | 27 | ## Contributing 28 | 29 | Formatting and linting hooks are run via `pre-commit` and will run prior to each commit. If the hooks fail they will reject the commit. The `end-of-file-fixer` and `trailing-whitespace` will automatically make the necessary fixes and you can just `git add ... && git commit -m ...` again immediately. The `fmt` and `clippy` lints will require your intervention. 30 | 31 | If you _MUST_ bypass the commit hooks to get things on a branch you can `git commit --no-verify -m ...` to skip the hooks. 32 | 33 | ``` 34 | brew install pre-commit 35 | 36 | pre-commit install 37 | ``` 38 | 39 | ```yaml 40 | repos: 41 | - repo: https://github.com/pre-commit/pre-commit-hooks 42 | rev: v4.5.0 43 | hooks: 44 | # - id: check-yaml 45 | - id: end-of-file-fixer 46 | - id: trailing-whitespace 47 | - repo: https://github.com/doublify/pre-commit-rust 48 | rev: v1.0 49 | hooks: 50 | - id: fmt 51 | - id: clippy 52 | args: [ --all-targets, --, -D, clippy::all ] 53 | ``` 54 | 55 | ## Quick Start 56 | 57 | ```rust 58 | use std::time::Duration; 59 | 60 | async fn fallible_operation(msg: &str) -> std::io::Result<()> { 61 | // Your potentially failing operation here 62 | Err(std::io::Error::other(msg)) 63 | } 64 | 65 | #[tokio::main] 66 | async fn main() { 67 | let result = mulligan::until_ok() 68 | .stop_after(5) // Try up to 5 times 69 | .max_delay(Duration::from_secs(3)) // Cap maximum delay at 3 seconds 70 | .exponential(Duration::from_secs(1)) // Use exponential backoff 71 | .full_jitter() // Add randomized jitter 72 | .execute(|| async { 73 | fallible_operation("connection failed").await 74 | }) 75 | .await; 76 | } 77 | ``` 78 | 79 | Alternatively, you may provide a custom stopping condition. `mulligan::until_ok()` is equivalent to the custom stopping condition shown below. 80 | 81 | ```rust 82 | #[tokio::main] 83 | async fn main() { 84 | let result = mulligan::until(|res| res.is_ok()) 85 | .stop_after(5) // Try up to 5 times 86 | .max_delay(Duration::from_secs(3)) // Cap maximum delay at 3 seconds 87 | .exponential(Duration::from_secs(1)) // Use exponential backoff 88 | .full_jitter() // Add randomized jitter 89 | .after_attempt(|prev, attempts| { // Run before each retry. 90 | println!("In the {}-th attempt, the returned result is {:?}.", attempts, prev); 91 | println!("Start next attempt"); 92 | }) 93 | .execute(|| async { 94 | fallible_operation("connection failed").await 95 | }) 96 | .await; 97 | } 98 | ``` 99 | 100 | ## Installation 101 | 102 | Add this to your `Cargo.toml`: 103 | 104 | ```toml 105 | [dependencies] 106 | mulligan = { version = "0.1", features = ["tokio"] } # or ["async-std"] 107 | ``` 108 | -------------------------------------------------------------------------------- /examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | async fn this_errors(msg: &str) -> std::io::Result<()> { 4 | println!("{msg}"); 5 | Err(std::io::Error::other("uh oh!")) 6 | } 7 | 8 | #[tokio::main()] 9 | async fn main() { 10 | let hello = tokio::spawn(async move { 11 | mulligan::until_ok() 12 | .stop_after(5) 13 | .max_delay(Duration::from_secs(3)) 14 | .full_jitter() 15 | .exponential(Duration::from_secs(1)) 16 | .execute(|| async { this_errors("hello").await }) 17 | .await 18 | }); 19 | let world = tokio::spawn(async move { 20 | mulligan::until(|r| r.is_ok()) 21 | .stop_after(10) 22 | .jitter(mulligan::jitter::Full) 23 | .fixed(Duration::from_secs(1)) 24 | .execute(|| async { this_errors("world").await }) 25 | .await 26 | }); 27 | 28 | let retry = tokio::spawn(async move { 29 | mulligan::until_ok() 30 | .stop_after(10) 31 | .full_jitter() 32 | .fixed(Duration::from_millis(200)) 33 | .after_attempt(|res, attempt| println!("Attempt = {}, result = {:?}", attempt, res)) 34 | .execute(|| async { this_errors("Oh uh!!!").await }) 35 | .await 36 | }); 37 | 38 | let _ = hello.await; 39 | let _ = world.await; 40 | let _ = retry.await; 41 | 42 | let _ = tokio::spawn(async move { 43 | mulligan::until_ok() 44 | .stop_after(3) 45 | .full_jitter() 46 | .fixed(Duration::from_millis(200)) 47 | .after_attempt(|res, attempt| println!("Attempt = {}, result = {:?}", attempt, res)) 48 | .execute(|| async { this_errors("Uh oh!!!").await }) 49 | .await 50 | }) 51 | .await; 52 | } 53 | -------------------------------------------------------------------------------- /src/backoff.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | pub trait Backoff { 4 | fn delay(&self, attempt: u32) -> Duration; 5 | fn base(&self) -> Duration; 6 | } 7 | 8 | pub struct Fixed(Duration); 9 | 10 | impl Fixed { 11 | pub fn base(dur: Duration) -> Self { 12 | Self(dur) 13 | } 14 | } 15 | 16 | impl Backoff for Fixed { 17 | fn base(&self) -> Duration { 18 | self.0 19 | } 20 | fn delay(&self, _attempt: u32) -> Duration { 21 | self.0 22 | } 23 | } 24 | 25 | pub struct Linear(Duration); 26 | 27 | impl Linear { 28 | pub fn base(dur: Duration) -> Self { 29 | Self(dur) 30 | } 31 | } 32 | 33 | impl Backoff for Linear { 34 | fn base(&self) -> Duration { 35 | self.0 36 | } 37 | fn delay(&self, attempt: u32) -> Duration { 38 | self.0 * attempt 39 | } 40 | } 41 | 42 | pub struct Exponential(Duration); 43 | 44 | impl Exponential { 45 | pub fn base(dur: Duration) -> Self { 46 | Self(dur) 47 | } 48 | } 49 | 50 | impl Backoff for Exponential { 51 | fn base(&self) -> Duration { 52 | self.0 53 | } 54 | fn delay(&self, attempt: u32) -> Duration { 55 | self.0 * 2u32.pow(attempt) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/jitter.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use rand::Rng; 4 | 5 | pub trait Jitter { 6 | fn jitter(&mut self, delay: Duration, max: Option) -> Duration; 7 | } 8 | 9 | pub struct NoJitter; 10 | 11 | impl Jitter for NoJitter { 12 | fn jitter(&mut self, delay: Duration, max: Option) -> Duration { 13 | max.map_or(delay, |max| max.min(delay)) 14 | } 15 | } 16 | 17 | pub struct Full; 18 | 19 | impl Jitter for Full { 20 | fn jitter(&mut self, delay: Duration, max: Option) -> Duration { 21 | let capped = max.map_or(delay, |max| max.min(delay)); 22 | rand::thread_rng().gen_range(Duration::from_micros(0)..=capped) 23 | } 24 | } 25 | 26 | pub struct Equal; 27 | 28 | impl Jitter for Equal { 29 | fn jitter(&mut self, delay: Duration, max: Option) -> Duration { 30 | let capped = max.map_or(delay, |max| max.min(delay)); 31 | rand::thread_rng().gen_range((capped / 2)..=capped) 32 | } 33 | } 34 | 35 | pub struct Decorrelated { 36 | base: Duration, 37 | previous: Duration, 38 | } 39 | 40 | impl Decorrelated { 41 | pub fn base(dur: Duration) -> Self { 42 | Self { 43 | base: dur, 44 | previous: Duration::from_secs(0), 45 | } 46 | } 47 | } 48 | 49 | impl Jitter for Decorrelated { 50 | fn jitter(&mut self, delay: Duration, max: Option) -> Duration { 51 | self.previous = delay; // TODO: Need to check if this is correct? 52 | let next = rand::thread_rng().gen_range(self.base..=self.previous * 3); 53 | max.map_or_else(|| next, |max| max.min(next)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::type_complexity)] 2 | 3 | #[cfg(not(any(feature = "tokio", feature = "async-std")))] 4 | compile_error!("At least on of 'tokio' or 'async-std' feature must be enabled"); 5 | 6 | pub mod backoff; 7 | pub mod jitter; 8 | 9 | use std::{future::Future, marker::PhantomData, time::Duration}; 10 | 11 | pub use backoff::{Backoff, Exponential, Fixed, Linear}; 12 | pub use jitter::{Decorrelated, Equal, Full, Jitter, NoJitter}; 13 | 14 | /// Continues retrying the provided future until a successful result is obtained. 15 | /// 16 | /// # Examples 17 | /// 18 | /// ``` 19 | /// use std::time::Duration; 20 | /// 21 | /// async fn this_errors(msg: &str) -> std::io::Result<()> { 22 | /// println!("{msg}"); 23 | /// Err(std::io::Error::other("uh oh!")) 24 | /// } 25 | /// 26 | /// # async fn example() { 27 | /// mulligan::until_ok() 28 | /// .stop_after(5) 29 | /// .max_delay(Duration::from_secs(3)) 30 | /// .full_jitter() 31 | /// .exponential(Duration::from_secs(1)) 32 | /// .execute(|| async { this_errors("hello").await }) 33 | /// .await; 34 | /// # } 35 | /// ``` 36 | pub fn until_ok() -> Mulligan) -> bool, NoJitter, Fixed> { 37 | until::(|result: &Result| result.is_ok()) 38 | } 39 | 40 | /// Continues retrying the provided future until a custom condition is met. 41 | /// 42 | /// # Examples 43 | /// 44 | /// ``` 45 | /// use std::time::Duration; 46 | /// 47 | /// async fn this_errors(msg: &str) -> std::io::Result<()> { 48 | /// println!("{msg}"); 49 | /// Err(std::io::Error::other("uh oh!")) 50 | /// } 51 | /// 52 | /// # async fn example() { 53 | /// mulligan::until(|res| res.is_ok()) 54 | /// .stop_after(5) 55 | /// .max_delay(Duration::from_secs(3)) 56 | /// .full_jitter() 57 | /// .exponential(Duration::from_secs(1)) 58 | /// .execute(|| async { this_errors("hello").await }) 59 | /// .await; 60 | /// # } 61 | /// ``` 62 | pub fn until(f: Cond) -> Mulligan 63 | where 64 | Cond: Fn(&Result) -> bool, 65 | { 66 | Mulligan { 67 | stop_after: None, 68 | until: f, 69 | backoff: Fixed::base(Duration::from_secs(0)), 70 | jitterable: jitter::NoJitter, 71 | max: None, 72 | before_attempt: None, 73 | after_attempt: None, 74 | _phantom: PhantomData, 75 | } 76 | } 77 | 78 | /// Not meant to be constructed directly. Use `mulligan::until_ok()` or `mulligan::until(...)` to construct. 79 | pub struct Mulligan 80 | where 81 | Cond: Fn(&Result) -> bool, 82 | Jit: jitter::Jitter, 83 | Back: backoff::Backoff, 84 | { 85 | stop_after: Option, 86 | until: Cond, 87 | backoff: Back, 88 | jitterable: Jit, 89 | max: Option, 90 | before_attempt: Option>, 91 | after_attempt: Option, u32) + Send + Sync + 'static>>, 92 | _phantom: PhantomData<(T, E)>, 93 | } 94 | 95 | impl Mulligan 96 | where 97 | Cond: Fn(&Result) -> bool, 98 | Jit: jitter::Jitter, 99 | Back: backoff::Backoff, 100 | { 101 | /// Retries a provided future until the stopping condition has been met. The default settings will 102 | /// retry forever with no delay between attempts. Backoff, Maximum Backoff, and Maximum Attempts 103 | /// can be configured with the other methods on the struct. 104 | /// 105 | /// # Examples 106 | /// 107 | /// ``` 108 | /// use std::time::Duration; 109 | /// 110 | /// async fn this_errors(msg: &str) -> std::io::Result<()> { 111 | /// println!("{msg}"); 112 | /// Err(std::io::Error::other("uh oh!")) 113 | /// } 114 | /// 115 | /// # async fn example() { 116 | /// mulligan::until_ok() 117 | /// .execute(|| async { this_errors("hello").await }) 118 | /// .await; 119 | /// # } 120 | /// ``` 121 | pub async fn execute(mut self, f: F) -> Result 122 | where 123 | F: Fn() -> Fut + 'static, 124 | Fut: Future> + Send, 125 | { 126 | let mut attempt: u32 = 0; 127 | loop { 128 | if let Some(before_attempt) = &self.before_attempt { 129 | before_attempt(attempt); 130 | } 131 | 132 | let res = f().await; 133 | 134 | if self.stop_after.map_or(false, |max| attempt >= max) | (self.until)(&res) { 135 | return res; 136 | } 137 | 138 | let delay = self.calculate_delay(attempt); 139 | 140 | Self::sleep(delay).await; 141 | 142 | if let Some(after_attempt) = &self.after_attempt { 143 | after_attempt(&res, attempt); 144 | } 145 | 146 | attempt += 1; 147 | } 148 | } 149 | /// Retries a provided function until the stopping condition has been met. The default settings will 150 | /// retry forever with no delay between attempts. Backoff, Maximum Backoff, and Maximum Attempts 151 | /// can be configured with the other methods on the struct. 152 | /// 153 | /// # Examples 154 | /// 155 | /// ``` 156 | /// use std::time::Duration; 157 | /// 158 | /// fn this_errors(msg: &str) -> std::io::Result<()> { 159 | /// println!("{msg}"); 160 | /// Err(std::io::Error::other("uh oh!")) 161 | /// } 162 | /// 163 | /// # async fn example() { 164 | /// mulligan::until_ok() 165 | /// .stop_after(2) 166 | /// .execute_sync(move || { this_errors("hello") }); 167 | /// # } 168 | /// ``` 169 | pub fn execute_sync(mut self, f: F) -> Result 170 | where 171 | F: Fn() -> Result, 172 | { 173 | let mut attempt: u32 = 0; 174 | loop { 175 | if let Some(before_attempt) = &self.before_attempt { 176 | before_attempt(attempt); 177 | } 178 | 179 | let res = f(); 180 | 181 | if self.stop_after.map_or(false, |max| attempt >= max) | (self.until)(&res) { 182 | return res; 183 | } 184 | 185 | let delay = self.calculate_delay(attempt); 186 | 187 | std::thread::sleep(delay); 188 | 189 | if let Some(after_attempt) = &self.after_attempt { 190 | after_attempt(&res, attempt); 191 | } 192 | attempt += 1; 193 | } 194 | } 195 | /// Sets the function to be called before each retry; 196 | /// it will not be called before the first execution. 197 | /// 198 | /// For the incoming function, the first parameter represents 199 | /// the result of the last execution, and the second parameter 200 | /// represents the number of times it has been executed. 201 | pub fn before_attempt(mut self, before_attempt: F) -> Self 202 | where 203 | F: Fn(u32) + Send + Sync + 'static, 204 | { 205 | self.before_attempt = Some(Box::new(before_attempt)); 206 | self 207 | } 208 | /// Sets the function to be called before each retry; 209 | /// it will not be called before the first execution. 210 | /// 211 | /// For the incoming function, the first parameter represents 212 | /// the result of the last execution, and the second parameter 213 | /// represents the number of times it has been executed. 214 | pub fn after_attempt(mut self, after_attempt: F) -> Self 215 | where 216 | F: Fn(&Result, u32) + Send + Sync + 'static, 217 | { 218 | self.after_attempt = Some(Box::new(after_attempt)); 219 | self 220 | } 221 | /// Sets the maximum number of attempts to retry before stopping regardless of whether `until` condition has been met. 222 | pub fn stop_after(mut self, attempts: u32) -> Self { 223 | self.stop_after = Some(attempts); 224 | self 225 | } 226 | fn calculate_delay(&mut self, attempt: u32) -> Duration { 227 | let delay = self.backoff.delay(attempt); 228 | self.jitterable.jitter(delay, self.max) 229 | } 230 | /// Adjust the backoff by the provided jitter strategy 231 | pub fn jitter(self, jitter: J) -> Mulligan 232 | where 233 | J: jitter::Jitter, 234 | { 235 | Mulligan { 236 | stop_after: self.stop_after, 237 | until: self.until, 238 | backoff: self.backoff, 239 | jitterable: jitter, 240 | max: self.max, 241 | before_attempt: self.before_attempt, 242 | after_attempt: self.after_attempt, 243 | _phantom: PhantomData, 244 | } 245 | } 246 | /// Adjust the calculated backoff by choosing a random delay between 0 and the backoff value 247 | pub fn full_jitter(self) -> Mulligan { 248 | Mulligan { 249 | stop_after: self.stop_after, 250 | until: self.until, 251 | backoff: self.backoff, 252 | jitterable: jitter::Full, 253 | max: self.max, 254 | before_attempt: self.before_attempt, 255 | after_attempt: self.after_attempt, 256 | _phantom: PhantomData, 257 | } 258 | } 259 | /// Adjust the calculated backoff by choosing a random delay between backoff / 2 and the backoff value 260 | pub fn equal_jitter(self) -> Mulligan { 261 | Mulligan { 262 | stop_after: self.stop_after, 263 | until: self.until, 264 | backoff: self.backoff, 265 | jitterable: jitter::Equal, 266 | max: self.max, 267 | before_attempt: self.before_attempt, 268 | after_attempt: self.after_attempt, 269 | _phantom: PhantomData, 270 | } 271 | } 272 | /// Adjust the calculated backoff by choosing a min(max_backoff, random(base_backoff, previous_backoff * 3)) 273 | pub fn decorrelated_jitter( 274 | self, 275 | base: Duration, 276 | ) -> Mulligan { 277 | Mulligan { 278 | stop_after: self.stop_after, 279 | until: self.until, 280 | backoff: self.backoff, 281 | jitterable: jitter::Decorrelated::base(base), 282 | max: self.max, 283 | before_attempt: self.before_attempt, 284 | after_attempt: self.after_attempt, 285 | _phantom: PhantomData, 286 | } 287 | } 288 | /// Delay by the calculated backoff strategy. 289 | pub fn backoff(self, backoff: B) -> Mulligan 290 | where 291 | B: Backoff, 292 | { 293 | Mulligan { 294 | stop_after: self.stop_after, 295 | until: self.until, 296 | backoff, 297 | jitterable: self.jitterable, 298 | max: self.max, 299 | before_attempt: self.before_attempt, 300 | after_attempt: self.after_attempt, 301 | _phantom: PhantomData, 302 | } 303 | } 304 | /// Wait a fixed amount of time between each retry. 305 | pub fn fixed(self, dur: Duration) -> Mulligan { 306 | Mulligan { 307 | stop_after: self.stop_after, 308 | until: self.until, 309 | backoff: Fixed::base(dur), 310 | jitterable: self.jitterable, 311 | max: self.max, 312 | before_attempt: self.before_attempt, 313 | after_attempt: self.after_attempt, 314 | _phantom: PhantomData, 315 | } 316 | } 317 | /// Wait a growing amount of time between each retry `base * attempt` 318 | pub fn linear(self, dur: Duration) -> Mulligan { 319 | Mulligan { 320 | stop_after: self.stop_after, 321 | until: self.until, 322 | backoff: Linear::base(dur), 323 | jitterable: self.jitterable, 324 | max: self.max, 325 | before_attempt: self.before_attempt, 326 | after_attempt: self.after_attempt, 327 | _phantom: PhantomData, 328 | } 329 | } 330 | /// Wait a growing amount of time between each retry `base * 2.pow(attempt)` 331 | pub fn exponential(self, dur: Duration) -> Mulligan { 332 | Mulligan { 333 | stop_after: self.stop_after, 334 | until: self.until, 335 | backoff: Exponential::base(dur), 336 | jitterable: self.jitterable, 337 | max: self.max, 338 | before_attempt: self.before_attempt, 339 | after_attempt: self.after_attempt, 340 | _phantom: PhantomData, 341 | } 342 | } 343 | /// Cap the maximum amount of time between retries even when the calculated backoff is larger. 344 | pub fn max_delay(mut self, dur: Duration) -> Self { 345 | self.max = Some(dur); 346 | self 347 | } 348 | 349 | #[cfg(feature = "tokio")] 350 | async fn sleep(dur: Duration) { 351 | tokio::time::sleep(dur).await; 352 | } 353 | #[cfg(all(feature = "async-std", not(feature = "tokio")))] 354 | async fn sleep(dur: Duration) { 355 | async_std::future::sleep(dur).await; 356 | } 357 | } 358 | --------------------------------------------------------------------------------