Exponential backoff retry logic
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(ocaml-atp,ocaml-clcw,ocaml-retry): pass all merlint checks

ocaml-atp: add test_naming.ml for hermest/lib/naming.ml (E605)
ocaml-clcw: merge fuzz_diff.ml into fuzz_clcw.ml to fix E710
(fuzz_diff had no corresponding library module)
ocaml-retry: extract test_retry.ml module with suite tuple (E605)

+694 -775
+1
test/dune
··· 1 1 (test 2 2 (name test) 3 + (modules test test_retry) 3 4 (libraries retry alcotest eio_main re))
+1 -775
test/test.ml
··· 1 - (* Config tests *) 2 - 3 - let default_config () = 4 - let config = Retry.default_config in 5 - Alcotest.(check int) "max_retries" 3 config.max_retries; 6 - Alcotest.(check (float 0.01)) "backoff_factor" 0.3 config.backoff_factor; 7 - Alcotest.(check (float 0.01)) "backoff_max" 120.0 config.backoff_max; 8 - Alcotest.(check bool) "jitter" true config.jitter 9 - 10 - let config_defaults () = 11 - let config = Retry.config () in 12 - Alcotest.(check int) "max_retries" 3 config.max_retries; 13 - Alcotest.(check (float 0.01)) "backoff_factor" 0.3 config.backoff_factor; 14 - Alcotest.(check (float 0.01)) "backoff_max" 120.0 config.backoff_max; 15 - Alcotest.(check bool) "jitter" true config.jitter 16 - 17 - let config_custom () = 18 - let config = 19 - Retry.config ~max_retries:10 ~backoff_factor:1.0 ~backoff_max:60.0 20 - ~jitter:false () 21 - in 22 - Alcotest.(check int) "max_retries" 10 config.max_retries; 23 - Alcotest.(check (float 0.01)) "backoff_factor" 1.0 config.backoff_factor; 24 - Alcotest.(check (float 0.01)) "backoff_max" 60.0 config.backoff_max; 25 - Alcotest.(check bool) "jitter" false config.jitter 26 - 27 - let config_partial () = 28 - let config = Retry.config ~max_retries:5 ~jitter:false () in 29 - Alcotest.(check int) "max_retries" 5 config.max_retries; 30 - Alcotest.(check (float 0.01)) "backoff_factor" 0.3 config.backoff_factor; 31 - Alcotest.(check (float 0.01)) "backoff_max" 120.0 config.backoff_max; 32 - Alcotest.(check bool) "jitter" false config.jitter 33 - 34 - let pp_config () = 35 - let config = 36 - Retry.config ~max_retries:5 ~backoff_factor:0.5 ~backoff_max:30.0 37 - ~jitter:true () 38 - in 39 - let output = Fmt.str "%a" Retry.pp_config config in 40 - (* Check exact substrings using a ref flag *) 41 - let found_max_retries = ref false in 42 - let found_backoff_factor = ref false in 43 - let found_jitter = ref false in 44 - String.iter 45 - (fun _ -> 46 - if String.length output >= 12 && String.sub output 0 12 = "Retry Config" 47 - then found_max_retries := true) 48 - output; 49 - (* Just verify output is formatted correctly by checking length *) 50 - Alcotest.(check bool) "non-empty output" true (String.length output > 20); 51 - (* Check for expected patterns in the output *) 52 - let re_max = Re.(compile (str "max_retries: 5")) in 53 - let re_backoff = Re.(compile (str "backoff_factor: 0.50")) in 54 - let re_jitter = Re.(compile (str "jitter: true")) in 55 - found_max_retries := Re.execp re_max output; 56 - found_backoff_factor := Re.execp re_backoff output; 57 - found_jitter := Re.execp re_jitter output; 58 - Alcotest.(check bool) "contains max_retries: 5" true !found_max_retries; 59 - Alcotest.(check bool) "contains backoff_factor" true !found_backoff_factor; 60 - Alcotest.(check bool) "contains jitter: true" true !found_jitter 61 - 62 - (* Backoff calculation tests *) 63 - (* Based on AWS exponential backoff recommendations: 64 - https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ 65 - Formula: delay = min(cap, base * 2^attempt) 66 - With jitter: delay = base_delay + random(0, base_delay) 67 - *) 68 - 69 - (* Test vectors: backoff_factor=1.0, no cap, no jitter 70 - | attempt | expected_delay | 71 - |---------|----------------| 72 - | 0 | 1.0 | (1.0 * 2^0) 73 - | 1 | 2.0 | (1.0 * 2^1) 74 - | 2 | 4.0 | (1.0 * 2^2) 75 - | 3 | 8.0 | (1.0 * 2^3) 76 - | 4 | 16.0 | (1.0 * 2^4) 77 - | 5 | 32.0 | (1.0 * 2^5) 78 - | 10 | 1024.0 | (1.0 * 2^10) 79 - *) 80 - let backoff_test_vectors_no_cap = 81 - [ (0, 1.0); (1, 2.0); (2, 4.0); (3, 8.0); (4, 16.0); (5, 32.0); (10, 1024.0) ] 82 - 83 - let backoff_test_vectors () = 84 - let config = 85 - Retry.config ~backoff_factor:1.0 ~backoff_max:10000.0 ~jitter:false () 86 - in 87 - List.iter 88 - (fun (attempt, expected) -> 89 - let delay = Retry.calculate_backoff ~config ~attempt in 90 - Alcotest.(check (float 0.001)) 91 - (Fmt.str "attempt %d" attempt) 92 - expected delay) 93 - backoff_test_vectors_no_cap 94 - 95 - (* Test vectors: backoff_factor=0.3 (default), cap=120.0, no jitter 96 - | attempt | base_delay | capped_delay | 97 - |---------|--------------|--------------| 98 - | 1 | 0.6 | 0.6 | (0.3 * 2^1) 99 - | 2 | 1.2 | 1.2 | (0.3 * 2^2) 100 - | 3 | 2.4 | 2.4 | (0.3 * 2^3) 101 - | 4 | 4.8 | 4.8 | (0.3 * 2^4) 102 - | 5 | 9.6 | 9.6 | (0.3 * 2^5) 103 - | 6 | 19.2 | 19.2 | (0.3 * 2^6) 104 - | 7 | 38.4 | 38.4 | (0.3 * 2^7) 105 - | 8 | 76.8 | 76.8 | (0.3 * 2^8) 106 - | 9 | 153.6 | 120.0 | capped 107 - | 10 | 307.2 | 120.0 | capped 108 - *) 109 - let backoff_test_vectors_default_config = 110 - [ 111 - (1, 0.6); 112 - (2, 1.2); 113 - (3, 2.4); 114 - (4, 4.8); 115 - (5, 9.6); 116 - (6, 19.2); 117 - (7, 38.4); 118 - (8, 76.8); 119 - (9, 120.0); 120 - (10, 120.0); 121 - ] 122 - 123 - let backoff_default_config_vectors () = 124 - let config = 125 - Retry.config ~backoff_factor:0.3 ~backoff_max:120.0 ~jitter:false () 126 - in 127 - List.iter 128 - (fun (attempt, expected) -> 129 - let delay = Retry.calculate_backoff ~config ~attempt in 130 - Alcotest.(check (float 0.01)) 131 - (Fmt.str "attempt %d" attempt) 132 - expected delay) 133 - backoff_test_vectors_default_config 134 - 135 - (* AWS-style backoff: base=100ms, cap=10s 136 - | attempt | base_delay(ms) | capped_delay(ms) | 137 - |---------|----------------|------------------| 138 - | 1 | 200 | 200 | 139 - | 2 | 400 | 400 | 140 - | 3 | 800 | 800 | 141 - | 4 | 1600 | 1600 | 142 - | 5 | 3200 | 3200 | 143 - | 6 | 6400 | 6400 | 144 - | 7 | 12800 | 10000 | capped 145 - *) 146 - let backoff_test_vectors_aws_style = 147 - [ 148 - (1, 0.2); 149 - (2, 0.4); 150 - (3, 0.8); 151 - (4, 1.6); 152 - (5, 3.2); 153 - (6, 6.4); 154 - (7, 10.0); 155 - (8, 10.0); 156 - ] 157 - 158 - let backoff_aws_style () = 159 - let config = 160 - Retry.config ~backoff_factor:0.1 ~backoff_max:10.0 ~jitter:false () 161 - in 162 - List.iter 163 - (fun (attempt, expected) -> 164 - let delay = Retry.calculate_backoff ~config ~attempt in 165 - Alcotest.(check (float 0.01)) 166 - (Fmt.str "AWS-style attempt %d" attempt) 167 - expected delay) 168 - backoff_test_vectors_aws_style 169 - 170 - (* Google Cloud style: initial=1s, multiplier=2, max=32s 171 - | attempt | delay(s) | 172 - |---------|----------| 173 - | 0 | 1 | 174 - | 1 | 2 | 175 - | 2 | 4 | 176 - | 3 | 8 | 177 - | 4 | 16 | 178 - | 5 | 32 | 179 - | 6 | 32 | capped 180 - *) 181 - let backoff_test_vectors_gcp_style = 182 - [ (0, 1.0); (1, 2.0); (2, 4.0); (3, 8.0); (4, 16.0); (5, 32.0); (6, 32.0) ] 183 - 184 - let backoff_gcp_style () = 185 - let config = 186 - Retry.config ~backoff_factor:1.0 ~backoff_max:32.0 ~jitter:false () 187 - in 188 - List.iter 189 - (fun (attempt, expected) -> 190 - let delay = Retry.calculate_backoff ~config ~attempt in 191 - Alcotest.(check (float 0.01)) 192 - (Fmt.str "GCP-style attempt %d" attempt) 193 - expected delay) 194 - backoff_test_vectors_gcp_style 195 - 196 - let backoff_no_jitter () = 197 - let config = 198 - Retry.config ~backoff_factor:1.0 ~backoff_max:1000.0 ~jitter:false () 199 - in 200 - (* attempt 1: 1.0 * 2^1 = 2.0 *) 201 - let delay1 = Retry.calculate_backoff ~config ~attempt:1 in 202 - Alcotest.(check (float 0.01)) "attempt 1" 2.0 delay1; 203 - (* attempt 2: 1.0 * 2^2 = 4.0 *) 204 - let delay2 = Retry.calculate_backoff ~config ~attempt:2 in 205 - Alcotest.(check (float 0.01)) "attempt 2" 4.0 delay2; 206 - (* attempt 3: 1.0 * 2^3 = 8.0 *) 207 - let delay3 = Retry.calculate_backoff ~config ~attempt:3 in 208 - Alcotest.(check (float 0.01)) "attempt 3" 8.0 delay3 209 - 210 - let backoff_exponential () = 211 - let config = 212 - Retry.config ~backoff_factor:0.5 ~backoff_max:1000.0 ~jitter:false () 213 - in 214 - (* attempt 1: 0.5 * 2^1 = 1.0 *) 215 - let delay1 = Retry.calculate_backoff ~config ~attempt:1 in 216 - Alcotest.(check (float 0.01)) "attempt 1" 1.0 delay1; 217 - (* attempt 4: 0.5 * 2^4 = 8.0 *) 218 - let delay4 = Retry.calculate_backoff ~config ~attempt:4 in 219 - Alcotest.(check (float 0.01)) "attempt 4" 8.0 delay4; 220 - (* attempt 5: 0.5 * 2^5 = 16.0 *) 221 - let delay5 = Retry.calculate_backoff ~config ~attempt:5 in 222 - Alcotest.(check (float 0.01)) "attempt 5" 16.0 delay5 223 - 224 - let backoff_capped () = 225 - let config = 226 - Retry.config ~backoff_factor:1.0 ~backoff_max:5.0 ~jitter:false () 227 - in 228 - (* attempt 1: min(2.0, 5.0) = 2.0 *) 229 - let delay1 = Retry.calculate_backoff ~config ~attempt:1 in 230 - Alcotest.(check (float 0.01)) "attempt 1 not capped" 2.0 delay1; 231 - (* attempt 2: min(4.0, 5.0) = 4.0 *) 232 - let delay2 = Retry.calculate_backoff ~config ~attempt:2 in 233 - Alcotest.(check (float 0.01)) "attempt 2 not capped" 4.0 delay2; 234 - (* attempt 3: min(8.0, 5.0) = 5.0 *) 235 - let delay3 = Retry.calculate_backoff ~config ~attempt:3 in 236 - Alcotest.(check (float 0.01)) "attempt 3 capped" 5.0 delay3; 237 - (* attempt 10: still capped at 5.0 *) 238 - let delay10 = Retry.calculate_backoff ~config ~attempt:10 in 239 - Alcotest.(check (float 0.01)) "attempt 10 capped" 5.0 delay10 240 - 241 - let backoff_with_jitter () = 242 - let config = 243 - Retry.config ~backoff_factor:1.0 ~backoff_max:1000.0 ~jitter:true () 244 - in 245 - (* With jitter, delay is base + random(0, base), so between base and 2*base *) 246 - (* attempt 1: base = 2.0, so delay in [2.0, 4.0) *) 247 - let delays = 248 - List.init 10 (fun _ -> Retry.calculate_backoff ~config ~attempt:1) 249 - in 250 - List.iter 251 - (fun delay -> 252 - Alcotest.(check bool) "delay >= 2.0" true (delay >= 2.0); 253 - Alcotest.(check bool) "delay < 4.0" true (delay < 4.0)) 254 - delays 255 - 256 - let backoff_zero_factor () = 257 - let config = 258 - Retry.config ~backoff_factor:0.0 ~backoff_max:1000.0 ~jitter:false () 259 - in 260 - let delay = Retry.calculate_backoff ~config ~attempt:5 in 261 - Alcotest.(check (float 0.01)) "zero factor gives zero delay" 0.0 delay 262 - 263 - let backoff_attempt_zero () = 264 - let config = 265 - Retry.config ~backoff_factor:1.0 ~backoff_max:1000.0 ~jitter:false () 266 - in 267 - (* attempt 0: 1.0 * 2^0 = 1.0 *) 268 - let delay = Retry.calculate_backoff ~config ~attempt:0 in 269 - Alcotest.(check (float 0.01)) "attempt 0" 1.0 delay 270 - 271 - (* with_retry tests *) 272 - 273 - let success_first_try () = 274 - Eio_main.run @@ fun env -> 275 - let clock = Eio.Stdenv.clock env in 276 - let config = Retry.config ~max_retries:3 () in 277 - let calls = ref 0 in 278 - let result = 279 - Retry.with_retry ~clock ~config 280 - ~should_retry:(fun _ -> true) 281 - (fun () -> 282 - incr calls; 283 - "success") 284 - in 285 - Alcotest.(check string) "result" "success" result; 286 - Alcotest.(check int) "called once" 1 !calls 287 - 288 - let retry_then_success () = 289 - Eio_main.run @@ fun env -> 290 - let clock = Eio.Stdenv.clock env in 291 - let config = 292 - Retry.config ~max_retries:3 ~backoff_factor:0.001 ~jitter:false () 293 - in 294 - let calls = ref 0 in 295 - let result = 296 - Retry.with_retry ~clock ~config 297 - ~should_retry:(fun _ -> true) 298 - (fun () -> 299 - incr calls; 300 - if !calls < 3 then failwith "not yet"; 301 - "success") 302 - in 303 - Alcotest.(check string) "result" "success" result; 304 - Alcotest.(check int) "called 3 times" 3 !calls 305 - 306 - let exhaust_retries () = 307 - Eio_main.run @@ fun env -> 308 - let clock = Eio.Stdenv.clock env in 309 - let config = 310 - Retry.config ~max_retries:2 ~backoff_factor:0.001 ~jitter:false () 311 - in 312 - let calls = ref 0 in 313 - let raised = 314 - try 315 - let _ = 316 - Retry.with_retry ~clock ~config 317 - ~should_retry:(fun _ -> true) 318 - (fun () -> 319 - incr calls; 320 - failwith "always fail") 321 - in 322 - false 323 - with Failure _ -> true 324 - in 325 - Alcotest.(check bool) "raised" true raised; 326 - Alcotest.(check int) "called max+1 times" 3 !calls 327 - 328 - let should_retry_false () = 329 - Eio_main.run @@ fun env -> 330 - let clock = Eio.Stdenv.clock env in 331 - let config = 332 - Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 333 - in 334 - let calls = ref 0 in 335 - let raised = 336 - try 337 - let _ = 338 - Retry.with_retry ~clock ~config 339 - ~should_retry:(fun _ -> false) 340 - (fun () -> 341 - incr calls; 342 - failwith "fail") 343 - in 344 - false 345 - with Failure _ -> true 346 - in 347 - Alcotest.(check bool) "raised" true raised; 348 - Alcotest.(check int) "called only once (no retry)" 1 !calls 349 - 350 - let selective_retry () = 351 - Eio_main.run @@ fun env -> 352 - let clock = Eio.Stdenv.clock env in 353 - let config = 354 - Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 355 - in 356 - let calls = ref 0 in 357 - let should_retry = function 358 - | Failure msg -> String.equal msg "transient" 359 - | _ -> false 360 - in 361 - let result = 362 - Retry.with_retry ~clock ~config ~should_retry (fun () -> 363 - incr calls; 364 - if !calls < 3 then failwith "transient"; 365 - "success") 366 - in 367 - Alcotest.(check string) "result" "success" result; 368 - Alcotest.(check int) "called 3 times" 3 !calls 369 - 370 - let selective_retry_permanent_failure () = 371 - Eio_main.run @@ fun env -> 372 - let clock = Eio.Stdenv.clock env in 373 - let config = 374 - Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 375 - in 376 - let calls = ref 0 in 377 - let should_retry = function 378 - | Failure msg -> String.equal msg "transient" 379 - | _ -> false 380 - in 381 - let raised = 382 - try 383 - let _ = 384 - Retry.with_retry ~clock ~config ~should_retry (fun () -> 385 - incr calls; 386 - failwith "permanent") 387 - in 388 - false 389 - with Failure msg -> String.equal msg "permanent" 390 - in 391 - Alcotest.(check bool) "raised permanent" true raised; 392 - Alcotest.(check int) "called only once" 1 !calls 393 - 394 - let zero_retries () = 395 - Eio_main.run @@ fun env -> 396 - let clock = Eio.Stdenv.clock env in 397 - let config = 398 - Retry.config ~max_retries:0 ~backoff_factor:0.001 ~jitter:false () 399 - in 400 - let calls = ref 0 in 401 - let raised = 402 - try 403 - let _ = 404 - Retry.with_retry ~clock ~config 405 - ~should_retry:(fun _ -> true) 406 - (fun () -> 407 - incr calls; 408 - failwith "fail") 409 - in 410 - false 411 - with Failure _ -> true 412 - in 413 - Alcotest.(check bool) "raised" true raised; 414 - Alcotest.(check int) "called only once (no retries)" 1 !calls 415 - 416 - let success_on_last_attempt () = 417 - Eio_main.run @@ fun env -> 418 - let clock = Eio.Stdenv.clock env in 419 - let config = 420 - Retry.config ~max_retries:3 ~backoff_factor:0.001 ~jitter:false () 421 - in 422 - let calls = ref 0 in 423 - let result = 424 - Retry.with_retry ~clock ~config 425 - ~should_retry:(fun _ -> true) 426 - (fun () -> 427 - incr calls; 428 - if !calls < 4 then failwith "not yet"; 429 - "success") 430 - in 431 - Alcotest.(check string) "result" "success" result; 432 - Alcotest.(check int) "called 4 times (1 + 3 retries)" 4 !calls 433 - 434 - let different_exception_types () = 435 - Eio_main.run @@ fun env -> 436 - let clock = Eio.Stdenv.clock env in 437 - let config = 438 - Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 439 - in 440 - let calls = ref 0 in 441 - let should_retry = function 442 - | Invalid_argument _ -> true 443 - | Failure _ -> true 444 - | Not_found -> false 445 - | _ -> false 446 - in 447 - let result = 448 - Retry.with_retry ~clock ~config ~should_retry (fun () -> 449 - incr calls; 450 - match !calls with 451 - | 1 -> invalid_arg "arg1" 452 - | 2 -> failwith "fail2" 453 - | 3 -> invalid_arg "arg3" 454 - | _ -> "success") 455 - in 456 - Alcotest.(check string) "result" "success" result; 457 - Alcotest.(check int) "called 4 times" 4 !calls 458 - 459 - let non_retryable_exception () = 460 - Eio_main.run @@ fun env -> 461 - let clock = Eio.Stdenv.clock env in 462 - let config = 463 - Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 464 - in 465 - let calls = ref 0 in 466 - let should_retry = function Failure _ -> true | _ -> false in 467 - let raised = 468 - try 469 - let _ = 470 - Retry.with_retry ~clock ~config ~should_retry (fun () -> 471 - incr calls; 472 - raise Not_found) 473 - in 474 - false 475 - with Not_found -> true 476 - in 477 - Alcotest.(check bool) "raised Not_found" true raised; 478 - Alcotest.(check int) "called only once" 1 !calls 479 - 480 - (* with_retry_result tests *) 481 - 482 - let result_success_first_try () = 483 - Eio_main.run @@ fun env -> 484 - let clock = Eio.Stdenv.clock env in 485 - let config = Retry.config ~max_retries:3 () in 486 - let calls = ref 0 in 487 - let result = 488 - Retry.with_retry_result ~clock ~config 489 - ~should_retry:(fun _ -> true) 490 - (fun () -> 491 - incr calls; 492 - Ok "success") 493 - in 494 - Alcotest.(check (result string string)) "result" (Ok "success") result; 495 - Alcotest.(check int) "called once" 1 !calls 496 - 497 - let result_retry () = 498 - Eio_main.run @@ fun env -> 499 - let clock = Eio.Stdenv.clock env in 500 - let config = 501 - Retry.config ~max_retries:3 ~backoff_factor:0.001 ~jitter:false () 502 - in 503 - let calls = ref 0 in 504 - let result = 505 - Retry.with_retry_result ~clock ~config 506 - ~should_retry:(fun _ -> true) 507 - (fun () -> 508 - incr calls; 509 - if !calls < 2 then Error "not yet" else Ok "success") 510 - in 511 - Alcotest.(check (result string string)) "result" (Ok "success") result; 512 - Alcotest.(check int) "called 2 times" 2 !calls 513 - 514 - let result_exhaust_retries () = 515 - Eio_main.run @@ fun env -> 516 - let clock = Eio.Stdenv.clock env in 517 - let config = 518 - Retry.config ~max_retries:2 ~backoff_factor:0.001 ~jitter:false () 519 - in 520 - let calls = ref 0 in 521 - let result = 522 - Retry.with_retry_result ~clock ~config 523 - ~should_retry:(fun _ -> true) 524 - (fun () -> 525 - incr calls; 526 - Error "always fail") 527 - in 528 - Alcotest.(check (result string string)) "result" (Error "always fail") result; 529 - Alcotest.(check int) "called max+1 times" 3 !calls 530 - 531 - let result_should_retry_false () = 532 - Eio_main.run @@ fun env -> 533 - let clock = Eio.Stdenv.clock env in 534 - let config = 535 - Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 536 - in 537 - let calls = ref 0 in 538 - let result = 539 - Retry.with_retry_result ~clock ~config 540 - ~should_retry:(fun _ -> false) 541 - (fun () -> 542 - incr calls; 543 - Error "fail") 544 - in 545 - Alcotest.(check (result string string)) "result" (Error "fail") result; 546 - Alcotest.(check int) "called only once" 1 !calls 547 - 548 - let result_selective_retry () = 549 - Eio_main.run @@ fun env -> 550 - let clock = Eio.Stdenv.clock env in 551 - let config = 552 - Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 553 - in 554 - let calls = ref 0 in 555 - let should_retry err = String.equal err "transient" in 556 - let result = 557 - Retry.with_retry_result ~clock ~config ~should_retry (fun () -> 558 - incr calls; 559 - if !calls < 3 then Error "transient" else Ok "success") 560 - in 561 - Alcotest.(check (result string string)) "result" (Ok "success") result; 562 - Alcotest.(check int) "called 3 times" 3 !calls 563 - 564 - let result_permanent_error () = 565 - Eio_main.run @@ fun env -> 566 - let clock = Eio.Stdenv.clock env in 567 - let config = 568 - Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 569 - in 570 - let calls = ref 0 in 571 - let should_retry err = String.equal err "transient" in 572 - let result = 573 - Retry.with_retry_result ~clock ~config ~should_retry (fun () -> 574 - incr calls; 575 - Error "permanent") 576 - in 577 - Alcotest.(check (result string string)) "result" (Error "permanent") result; 578 - Alcotest.(check int) "called only once" 1 !calls 579 - 580 - let result_zero_retries () = 581 - Eio_main.run @@ fun env -> 582 - let clock = Eio.Stdenv.clock env in 583 - let config = 584 - Retry.config ~max_retries:0 ~backoff_factor:0.001 ~jitter:false () 585 - in 586 - let calls = ref 0 in 587 - let result = 588 - Retry.with_retry_result ~clock ~config 589 - ~should_retry:(fun _ -> true) 590 - (fun () -> 591 - incr calls; 592 - Error "fail") 593 - in 594 - Alcotest.(check (result string string)) "result" (Error "fail") result; 595 - Alcotest.(check int) "called only once" 1 !calls 596 - 597 - let result_success_on_last_attempt () = 598 - Eio_main.run @@ fun env -> 599 - let clock = Eio.Stdenv.clock env in 600 - let config = 601 - Retry.config ~max_retries:3 ~backoff_factor:0.001 ~jitter:false () 602 - in 603 - let calls = ref 0 in 604 - let result = 605 - Retry.with_retry_result ~clock ~config 606 - ~should_retry:(fun _ -> true) 607 - (fun () -> 608 - incr calls; 609 - if !calls < 4 then Error "not yet" else Ok "success") 610 - in 611 - Alcotest.(check (result string string)) "result" (Ok "success") result; 612 - Alcotest.(check int) "called 4 times" 4 !calls 613 - 614 - type error_kind = Transient | Permanent | Unknown 615 - 616 - let error_kind_equal a b = 617 - match (a, b) with 618 - | Transient, Transient | Permanent, Permanent | Unknown, Unknown -> true 619 - | _ -> false 620 - 621 - let pp_error_kind ppf = function 622 - | Transient -> Format.pp_print_string ppf "Transient" 623 - | Permanent -> Format.pp_print_string ppf "Permanent" 624 - | Unknown -> Format.pp_print_string ppf "Unknown" 625 - 626 - let error_kind_testable = Alcotest.testable pp_error_kind error_kind_equal 627 - 628 - let result_typed_errors () = 629 - Eio_main.run @@ fun env -> 630 - let clock = Eio.Stdenv.clock env in 631 - let config = 632 - Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 633 - in 634 - let calls = ref 0 in 635 - let should_retry = function 636 - | Transient -> true 637 - | Permanent -> false 638 - | Unknown -> false 639 - in 640 - let result = 641 - Retry.with_retry_result ~clock ~config ~should_retry (fun () -> 642 - incr calls; 643 - match !calls with 644 - | 1 -> Error Transient 645 - | 2 -> Error Transient 646 - | 3 -> Error Transient 647 - | _ -> Ok "success") 648 - in 649 - Alcotest.(check (result string error_kind_testable)) 650 - "result" (Ok "success") result; 651 - Alcotest.(check int) "called 4 times" 4 !calls 652 - 653 - let result_typed_permanent_error () = 654 - Eio_main.run @@ fun env -> 655 - let clock = Eio.Stdenv.clock env in 656 - let config = 657 - Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 658 - in 659 - let calls = ref 0 in 660 - let should_retry = function 661 - | Transient -> true 662 - | Permanent -> false 663 - | Unknown -> false 664 - in 665 - let result = 666 - Retry.with_retry_result ~clock ~config ~should_retry (fun () -> 667 - incr calls; 668 - match !calls with 1 -> Error Transient | _ -> Error Permanent) 669 - in 670 - Alcotest.(check (result string error_kind_testable)) 671 - "result" (Error Permanent) result; 672 - Alcotest.(check int) "called 2 times" 2 !calls 673 - 674 - let result_unknown_error () = 675 - Eio_main.run @@ fun env -> 676 - let clock = Eio.Stdenv.clock env in 677 - let config = 678 - Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 679 - in 680 - let calls = ref 0 in 681 - let should_retry = function 682 - | Transient -> true 683 - | Permanent -> false 684 - | Unknown -> false 685 - in 686 - let result = 687 - Retry.with_retry_result ~clock ~config ~should_retry (fun () -> 688 - incr calls; 689 - Error Unknown) 690 - in 691 - Alcotest.(check (result string error_kind_testable)) 692 - "result" (Error Unknown) result; 693 - Alcotest.(check int) "called only once" 1 !calls 694 - 695 - (* Large retry count test *) 696 - 697 - let many_retries () = 698 - Eio_main.run @@ fun env -> 699 - let clock = Eio.Stdenv.clock env in 700 - let config = 701 - Retry.config ~max_retries:20 ~backoff_factor:0.0001 ~jitter:false () 702 - in 703 - let calls = ref 0 in 704 - let result = 705 - Retry.with_retry ~clock ~config 706 - ~should_retry:(fun _ -> true) 707 - (fun () -> 708 - incr calls; 709 - if !calls < 15 then failwith "not yet"; 710 - "success") 711 - in 712 - Alcotest.(check string) "result" "success" result; 713 - Alcotest.(check int) "called 15 times" 15 !calls 714 - 715 - let () = 716 - Alcotest.run "retry" 717 - [ 718 - ( "config", 719 - [ 720 - Alcotest.test_case "default config" `Quick default_config; 721 - Alcotest.test_case "config defaults" `Quick config_defaults; 722 - Alcotest.test_case "config custom" `Quick config_custom; 723 - Alcotest.test_case "config partial" `Quick config_partial; 724 - Alcotest.test_case "pp_config" `Quick pp_config; 725 - ] ); 726 - ( "backoff", 727 - [ 728 - Alcotest.test_case "test vectors (no cap)" `Quick backoff_test_vectors; 729 - Alcotest.test_case "test vectors (default config)" `Quick 730 - backoff_default_config_vectors; 731 - Alcotest.test_case "AWS-style backoff" `Quick backoff_aws_style; 732 - Alcotest.test_case "GCP-style backoff" `Quick backoff_gcp_style; 733 - Alcotest.test_case "no jitter" `Quick backoff_no_jitter; 734 - Alcotest.test_case "exponential" `Quick backoff_exponential; 735 - Alcotest.test_case "capped" `Quick backoff_capped; 736 - Alcotest.test_case "with jitter" `Quick backoff_with_jitter; 737 - Alcotest.test_case "zero factor" `Quick backoff_zero_factor; 738 - Alcotest.test_case "attempt zero" `Quick backoff_attempt_zero; 739 - ] ); 740 - ( "with_retry", 741 - [ 742 - Alcotest.test_case "success first try" `Quick success_first_try; 743 - Alcotest.test_case "retry then success" `Quick retry_then_success; 744 - Alcotest.test_case "exhaust retries" `Quick exhaust_retries; 745 - Alcotest.test_case "should_retry false" `Quick should_retry_false; 746 - Alcotest.test_case "selective retry" `Quick selective_retry; 747 - Alcotest.test_case "selective retry permanent" `Quick 748 - selective_retry_permanent_failure; 749 - Alcotest.test_case "zero retries" `Quick zero_retries; 750 - Alcotest.test_case "success on last attempt" `Quick 751 - success_on_last_attempt; 752 - Alcotest.test_case "different exception types" `Quick 753 - different_exception_types; 754 - Alcotest.test_case "non-retryable exception" `Quick 755 - non_retryable_exception; 756 - Alcotest.test_case "many retries" `Quick many_retries; 757 - ] ); 758 - ( "with_retry_result", 759 - [ 760 - Alcotest.test_case "success first try" `Quick result_success_first_try; 761 - Alcotest.test_case "retry" `Quick result_retry; 762 - Alcotest.test_case "exhaust retries" `Quick result_exhaust_retries; 763 - Alcotest.test_case "should_retry false" `Quick 764 - result_should_retry_false; 765 - Alcotest.test_case "selective retry" `Quick result_selective_retry; 766 - Alcotest.test_case "permanent error" `Quick result_permanent_error; 767 - Alcotest.test_case "zero retries" `Quick result_zero_retries; 768 - Alcotest.test_case "success on last attempt" `Quick 769 - result_success_on_last_attempt; 770 - Alcotest.test_case "typed errors" `Quick result_typed_errors; 771 - Alcotest.test_case "typed permanent error" `Quick 772 - result_typed_permanent_error; 773 - Alcotest.test_case "unknown error" `Quick result_unknown_error; 774 - ] ); 775 - ] 1 + let () = Alcotest.run "retry" [ Test_retry.suite ]
+688
test/test_retry.ml
··· 1 + (** Tests for Retry module. *) 2 + 3 + (* Config tests *) 4 + 5 + let default_config () = 6 + let config = Retry.default_config in 7 + Alcotest.(check int) "max_retries" 3 config.max_retries; 8 + Alcotest.(check (float 0.01)) "backoff_factor" 0.3 config.backoff_factor; 9 + Alcotest.(check (float 0.01)) "backoff_max" 120.0 config.backoff_max; 10 + Alcotest.(check bool) "jitter" true config.jitter 11 + 12 + let config_defaults () = 13 + let config = Retry.config () in 14 + Alcotest.(check int) "max_retries" 3 config.max_retries; 15 + Alcotest.(check (float 0.01)) "backoff_factor" 0.3 config.backoff_factor; 16 + Alcotest.(check (float 0.01)) "backoff_max" 120.0 config.backoff_max; 17 + Alcotest.(check bool) "jitter" true config.jitter 18 + 19 + let config_custom () = 20 + let config = 21 + Retry.config ~max_retries:10 ~backoff_factor:1.0 ~backoff_max:60.0 22 + ~jitter:false () 23 + in 24 + Alcotest.(check int) "max_retries" 10 config.max_retries; 25 + Alcotest.(check (float 0.01)) "backoff_factor" 1.0 config.backoff_factor; 26 + Alcotest.(check (float 0.01)) "backoff_max" 60.0 config.backoff_max; 27 + Alcotest.(check bool) "jitter" false config.jitter 28 + 29 + let config_partial () = 30 + let config = Retry.config ~max_retries:5 ~jitter:false () in 31 + Alcotest.(check int) "max_retries" 5 config.max_retries; 32 + Alcotest.(check (float 0.01)) "backoff_factor" 0.3 config.backoff_factor; 33 + Alcotest.(check (float 0.01)) "backoff_max" 120.0 config.backoff_max; 34 + Alcotest.(check bool) "jitter" false config.jitter 35 + 36 + let pp_config () = 37 + let config = 38 + Retry.config ~max_retries:5 ~backoff_factor:0.5 ~backoff_max:30.0 39 + ~jitter:true () 40 + in 41 + let output = Fmt.str "%a" Retry.pp_config config in 42 + Alcotest.(check bool) "non-empty output" true (String.length output > 20); 43 + let re_max = Re.(compile (str "max_retries: 5")) in 44 + let re_backoff = Re.(compile (str "backoff_factor: 0.50")) in 45 + let re_jitter = Re.(compile (str "jitter: true")) in 46 + Alcotest.(check bool) "contains max_retries: 5" true (Re.execp re_max output); 47 + Alcotest.(check bool) 48 + "contains backoff_factor" true 49 + (Re.execp re_backoff output); 50 + Alcotest.(check bool) "contains jitter: true" true (Re.execp re_jitter output) 51 + 52 + (* Backoff calculation tests *) 53 + 54 + let backoff_test_vectors_no_cap = 55 + [ (0, 1.0); (1, 2.0); (2, 4.0); (3, 8.0); (4, 16.0); (5, 32.0); (10, 1024.0) ] 56 + 57 + let backoff_test_vectors () = 58 + let config = 59 + Retry.config ~backoff_factor:1.0 ~backoff_max:10000.0 ~jitter:false () 60 + in 61 + List.iter 62 + (fun (attempt, expected) -> 63 + let delay = Retry.calculate_backoff ~config ~attempt in 64 + Alcotest.(check (float 0.001)) 65 + (Fmt.str "attempt %d" attempt) 66 + expected delay) 67 + backoff_test_vectors_no_cap 68 + 69 + let backoff_test_vectors_default_config = 70 + [ 71 + (1, 0.6); 72 + (2, 1.2); 73 + (3, 2.4); 74 + (4, 4.8); 75 + (5, 9.6); 76 + (6, 19.2); 77 + (7, 38.4); 78 + (8, 76.8); 79 + (9, 120.0); 80 + (10, 120.0); 81 + ] 82 + 83 + let backoff_default_config_vectors () = 84 + let config = 85 + Retry.config ~backoff_factor:0.3 ~backoff_max:120.0 ~jitter:false () 86 + in 87 + List.iter 88 + (fun (attempt, expected) -> 89 + let delay = Retry.calculate_backoff ~config ~attempt in 90 + Alcotest.(check (float 0.01)) 91 + (Fmt.str "attempt %d" attempt) 92 + expected delay) 93 + backoff_test_vectors_default_config 94 + 95 + let backoff_test_vectors_aws_style = 96 + [ 97 + (1, 0.2); 98 + (2, 0.4); 99 + (3, 0.8); 100 + (4, 1.6); 101 + (5, 3.2); 102 + (6, 6.4); 103 + (7, 10.0); 104 + (8, 10.0); 105 + ] 106 + 107 + let backoff_aws_style () = 108 + let config = 109 + Retry.config ~backoff_factor:0.1 ~backoff_max:10.0 ~jitter:false () 110 + in 111 + List.iter 112 + (fun (attempt, expected) -> 113 + let delay = Retry.calculate_backoff ~config ~attempt in 114 + Alcotest.(check (float 0.01)) 115 + (Fmt.str "AWS-style attempt %d" attempt) 116 + expected delay) 117 + backoff_test_vectors_aws_style 118 + 119 + let backoff_test_vectors_gcp_style = 120 + [ (0, 1.0); (1, 2.0); (2, 4.0); (3, 8.0); (4, 16.0); (5, 32.0); (6, 32.0) ] 121 + 122 + let backoff_gcp_style () = 123 + let config = 124 + Retry.config ~backoff_factor:1.0 ~backoff_max:32.0 ~jitter:false () 125 + in 126 + List.iter 127 + (fun (attempt, expected) -> 128 + let delay = Retry.calculate_backoff ~config ~attempt in 129 + Alcotest.(check (float 0.01)) 130 + (Fmt.str "GCP-style attempt %d" attempt) 131 + expected delay) 132 + backoff_test_vectors_gcp_style 133 + 134 + let backoff_no_jitter () = 135 + let config = 136 + Retry.config ~backoff_factor:1.0 ~backoff_max:1000.0 ~jitter:false () 137 + in 138 + let delay1 = Retry.calculate_backoff ~config ~attempt:1 in 139 + Alcotest.(check (float 0.01)) "attempt 1" 2.0 delay1; 140 + let delay2 = Retry.calculate_backoff ~config ~attempt:2 in 141 + Alcotest.(check (float 0.01)) "attempt 2" 4.0 delay2; 142 + let delay3 = Retry.calculate_backoff ~config ~attempt:3 in 143 + Alcotest.(check (float 0.01)) "attempt 3" 8.0 delay3 144 + 145 + let backoff_exponential () = 146 + let config = 147 + Retry.config ~backoff_factor:0.5 ~backoff_max:1000.0 ~jitter:false () 148 + in 149 + let delay1 = Retry.calculate_backoff ~config ~attempt:1 in 150 + Alcotest.(check (float 0.01)) "attempt 1" 1.0 delay1; 151 + let delay4 = Retry.calculate_backoff ~config ~attempt:4 in 152 + Alcotest.(check (float 0.01)) "attempt 4" 8.0 delay4; 153 + let delay5 = Retry.calculate_backoff ~config ~attempt:5 in 154 + Alcotest.(check (float 0.01)) "attempt 5" 16.0 delay5 155 + 156 + let backoff_capped () = 157 + let config = 158 + Retry.config ~backoff_factor:1.0 ~backoff_max:5.0 ~jitter:false () 159 + in 160 + let delay1 = Retry.calculate_backoff ~config ~attempt:1 in 161 + Alcotest.(check (float 0.01)) "attempt 1 not capped" 2.0 delay1; 162 + let delay2 = Retry.calculate_backoff ~config ~attempt:2 in 163 + Alcotest.(check (float 0.01)) "attempt 2 not capped" 4.0 delay2; 164 + let delay3 = Retry.calculate_backoff ~config ~attempt:3 in 165 + Alcotest.(check (float 0.01)) "attempt 3 capped" 5.0 delay3; 166 + let delay10 = Retry.calculate_backoff ~config ~attempt:10 in 167 + Alcotest.(check (float 0.01)) "attempt 10 capped" 5.0 delay10 168 + 169 + let backoff_with_jitter () = 170 + let config = 171 + Retry.config ~backoff_factor:1.0 ~backoff_max:1000.0 ~jitter:true () 172 + in 173 + let delays = 174 + List.init 10 (fun _ -> Retry.calculate_backoff ~config ~attempt:1) 175 + in 176 + List.iter 177 + (fun delay -> 178 + Alcotest.(check bool) "delay >= 2.0" true (delay >= 2.0); 179 + Alcotest.(check bool) "delay < 4.0" true (delay < 4.0)) 180 + delays 181 + 182 + let backoff_zero_factor () = 183 + let config = 184 + Retry.config ~backoff_factor:0.0 ~backoff_max:1000.0 ~jitter:false () 185 + in 186 + let delay = Retry.calculate_backoff ~config ~attempt:5 in 187 + Alcotest.(check (float 0.01)) "zero factor gives zero delay" 0.0 delay 188 + 189 + let backoff_attempt_zero () = 190 + let config = 191 + Retry.config ~backoff_factor:1.0 ~backoff_max:1000.0 ~jitter:false () 192 + in 193 + let delay = Retry.calculate_backoff ~config ~attempt:0 in 194 + Alcotest.(check (float 0.01)) "attempt 0" 1.0 delay 195 + 196 + (* with_retry tests *) 197 + 198 + let success_first_try () = 199 + Eio_main.run @@ fun env -> 200 + let clock = Eio.Stdenv.clock env in 201 + let config = Retry.config ~max_retries:3 () in 202 + let calls = ref 0 in 203 + let result = 204 + Retry.with_retry ~clock ~config 205 + ~should_retry:(fun _ -> true) 206 + (fun () -> 207 + incr calls; 208 + "success") 209 + in 210 + Alcotest.(check string) "result" "success" result; 211 + Alcotest.(check int) "called once" 1 !calls 212 + 213 + let retry_then_success () = 214 + Eio_main.run @@ fun env -> 215 + let clock = Eio.Stdenv.clock env in 216 + let config = 217 + Retry.config ~max_retries:3 ~backoff_factor:0.001 ~jitter:false () 218 + in 219 + let calls = ref 0 in 220 + let result = 221 + Retry.with_retry ~clock ~config 222 + ~should_retry:(fun _ -> true) 223 + (fun () -> 224 + incr calls; 225 + if !calls < 3 then failwith "not yet"; 226 + "success") 227 + in 228 + Alcotest.(check string) "result" "success" result; 229 + Alcotest.(check int) "called 3 times" 3 !calls 230 + 231 + let exhaust_retries () = 232 + Eio_main.run @@ fun env -> 233 + let clock = Eio.Stdenv.clock env in 234 + let config = 235 + Retry.config ~max_retries:2 ~backoff_factor:0.001 ~jitter:false () 236 + in 237 + let calls = ref 0 in 238 + let raised = 239 + try 240 + let _ = 241 + Retry.with_retry ~clock ~config 242 + ~should_retry:(fun _ -> true) 243 + (fun () -> 244 + incr calls; 245 + failwith "always fail") 246 + in 247 + false 248 + with Failure _ -> true 249 + in 250 + Alcotest.(check bool) "raised" true raised; 251 + Alcotest.(check int) "called max+1 times" 3 !calls 252 + 253 + let should_retry_false () = 254 + Eio_main.run @@ fun env -> 255 + let clock = Eio.Stdenv.clock env in 256 + let config = 257 + Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 258 + in 259 + let calls = ref 0 in 260 + let raised = 261 + try 262 + let _ = 263 + Retry.with_retry ~clock ~config 264 + ~should_retry:(fun _ -> false) 265 + (fun () -> 266 + incr calls; 267 + failwith "fail") 268 + in 269 + false 270 + with Failure _ -> true 271 + in 272 + Alcotest.(check bool) "raised" true raised; 273 + Alcotest.(check int) "called only once (no retry)" 1 !calls 274 + 275 + let selective_retry () = 276 + Eio_main.run @@ fun env -> 277 + let clock = Eio.Stdenv.clock env in 278 + let config = 279 + Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 280 + in 281 + let calls = ref 0 in 282 + let should_retry = function 283 + | Failure msg -> String.equal msg "transient" 284 + | _ -> false 285 + in 286 + let result = 287 + Retry.with_retry ~clock ~config ~should_retry (fun () -> 288 + incr calls; 289 + if !calls < 3 then failwith "transient"; 290 + "success") 291 + in 292 + Alcotest.(check string) "result" "success" result; 293 + Alcotest.(check int) "called 3 times" 3 !calls 294 + 295 + let selective_retry_permanent_failure () = 296 + Eio_main.run @@ fun env -> 297 + let clock = Eio.Stdenv.clock env in 298 + let config = 299 + Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 300 + in 301 + let calls = ref 0 in 302 + let should_retry = function 303 + | Failure msg -> String.equal msg "transient" 304 + | _ -> false 305 + in 306 + let raised = 307 + try 308 + let _ = 309 + Retry.with_retry ~clock ~config ~should_retry (fun () -> 310 + incr calls; 311 + failwith "permanent") 312 + in 313 + false 314 + with Failure msg -> String.equal msg "permanent" 315 + in 316 + Alcotest.(check bool) "raised permanent" true raised; 317 + Alcotest.(check int) "called only once" 1 !calls 318 + 319 + let zero_retries () = 320 + Eio_main.run @@ fun env -> 321 + let clock = Eio.Stdenv.clock env in 322 + let config = 323 + Retry.config ~max_retries:0 ~backoff_factor:0.001 ~jitter:false () 324 + in 325 + let calls = ref 0 in 326 + let raised = 327 + try 328 + let _ = 329 + Retry.with_retry ~clock ~config 330 + ~should_retry:(fun _ -> true) 331 + (fun () -> 332 + incr calls; 333 + failwith "fail") 334 + in 335 + false 336 + with Failure _ -> true 337 + in 338 + Alcotest.(check bool) "raised" true raised; 339 + Alcotest.(check int) "called only once (no retries)" 1 !calls 340 + 341 + let success_on_last_attempt () = 342 + Eio_main.run @@ fun env -> 343 + let clock = Eio.Stdenv.clock env in 344 + let config = 345 + Retry.config ~max_retries:3 ~backoff_factor:0.001 ~jitter:false () 346 + in 347 + let calls = ref 0 in 348 + let result = 349 + Retry.with_retry ~clock ~config 350 + ~should_retry:(fun _ -> true) 351 + (fun () -> 352 + incr calls; 353 + if !calls < 4 then failwith "not yet"; 354 + "success") 355 + in 356 + Alcotest.(check string) "result" "success" result; 357 + Alcotest.(check int) "called 4 times (1 + 3 retries)" 4 !calls 358 + 359 + let different_exception_types () = 360 + Eio_main.run @@ fun env -> 361 + let clock = Eio.Stdenv.clock env in 362 + let config = 363 + Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 364 + in 365 + let calls = ref 0 in 366 + let should_retry = function 367 + | Invalid_argument _ -> true 368 + | Failure _ -> true 369 + | Not_found -> false 370 + | _ -> false 371 + in 372 + let result = 373 + Retry.with_retry ~clock ~config ~should_retry (fun () -> 374 + incr calls; 375 + match !calls with 376 + | 1 -> invalid_arg "arg1" 377 + | 2 -> failwith "fail2" 378 + | 3 -> invalid_arg "arg3" 379 + | _ -> "success") 380 + in 381 + Alcotest.(check string) "result" "success" result; 382 + Alcotest.(check int) "called 4 times" 4 !calls 383 + 384 + let non_retryable_exception () = 385 + Eio_main.run @@ fun env -> 386 + let clock = Eio.Stdenv.clock env in 387 + let config = 388 + Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 389 + in 390 + let calls = ref 0 in 391 + let should_retry = function Failure _ -> true | _ -> false in 392 + let raised = 393 + try 394 + let _ = 395 + Retry.with_retry ~clock ~config ~should_retry (fun () -> 396 + incr calls; 397 + raise Not_found) 398 + in 399 + false 400 + with Not_found -> true 401 + in 402 + Alcotest.(check bool) "raised Not_found" true raised; 403 + Alcotest.(check int) "called only once" 1 !calls 404 + 405 + (* with_retry_result tests *) 406 + 407 + let result_success_first_try () = 408 + Eio_main.run @@ fun env -> 409 + let clock = Eio.Stdenv.clock env in 410 + let config = Retry.config ~max_retries:3 () in 411 + let calls = ref 0 in 412 + let result = 413 + Retry.with_retry_result ~clock ~config 414 + ~should_retry:(fun _ -> true) 415 + (fun () -> 416 + incr calls; 417 + Ok "success") 418 + in 419 + Alcotest.(check (result string string)) "result" (Ok "success") result; 420 + Alcotest.(check int) "called once" 1 !calls 421 + 422 + let result_retry () = 423 + Eio_main.run @@ fun env -> 424 + let clock = Eio.Stdenv.clock env in 425 + let config = 426 + Retry.config ~max_retries:3 ~backoff_factor:0.001 ~jitter:false () 427 + in 428 + let calls = ref 0 in 429 + let result = 430 + Retry.with_retry_result ~clock ~config 431 + ~should_retry:(fun _ -> true) 432 + (fun () -> 433 + incr calls; 434 + if !calls < 2 then Error "not yet" else Ok "success") 435 + in 436 + Alcotest.(check (result string string)) "result" (Ok "success") result; 437 + Alcotest.(check int) "called 2 times" 2 !calls 438 + 439 + let result_exhaust_retries () = 440 + Eio_main.run @@ fun env -> 441 + let clock = Eio.Stdenv.clock env in 442 + let config = 443 + Retry.config ~max_retries:2 ~backoff_factor:0.001 ~jitter:false () 444 + in 445 + let calls = ref 0 in 446 + let result = 447 + Retry.with_retry_result ~clock ~config 448 + ~should_retry:(fun _ -> true) 449 + (fun () -> 450 + incr calls; 451 + Error "always fail") 452 + in 453 + Alcotest.(check (result string string)) "result" (Error "always fail") result; 454 + Alcotest.(check int) "called max+1 times" 3 !calls 455 + 456 + let result_should_retry_false () = 457 + Eio_main.run @@ fun env -> 458 + let clock = Eio.Stdenv.clock env in 459 + let config = 460 + Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 461 + in 462 + let calls = ref 0 in 463 + let result = 464 + Retry.with_retry_result ~clock ~config 465 + ~should_retry:(fun _ -> false) 466 + (fun () -> 467 + incr calls; 468 + Error "fail") 469 + in 470 + Alcotest.(check (result string string)) "result" (Error "fail") result; 471 + Alcotest.(check int) "called only once" 1 !calls 472 + 473 + let result_selective_retry () = 474 + Eio_main.run @@ fun env -> 475 + let clock = Eio.Stdenv.clock env in 476 + let config = 477 + Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 478 + in 479 + let calls = ref 0 in 480 + let should_retry err = String.equal err "transient" in 481 + let result = 482 + Retry.with_retry_result ~clock ~config ~should_retry (fun () -> 483 + incr calls; 484 + if !calls < 3 then Error "transient" else Ok "success") 485 + in 486 + Alcotest.(check (result string string)) "result" (Ok "success") result; 487 + Alcotest.(check int) "called 3 times" 3 !calls 488 + 489 + let result_permanent_error () = 490 + Eio_main.run @@ fun env -> 491 + let clock = Eio.Stdenv.clock env in 492 + let config = 493 + Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 494 + in 495 + let calls = ref 0 in 496 + let should_retry err = String.equal err "transient" in 497 + let result = 498 + Retry.with_retry_result ~clock ~config ~should_retry (fun () -> 499 + incr calls; 500 + Error "permanent") 501 + in 502 + Alcotest.(check (result string string)) "result" (Error "permanent") result; 503 + Alcotest.(check int) "called only once" 1 !calls 504 + 505 + let result_zero_retries () = 506 + Eio_main.run @@ fun env -> 507 + let clock = Eio.Stdenv.clock env in 508 + let config = 509 + Retry.config ~max_retries:0 ~backoff_factor:0.001 ~jitter:false () 510 + in 511 + let calls = ref 0 in 512 + let result = 513 + Retry.with_retry_result ~clock ~config 514 + ~should_retry:(fun _ -> true) 515 + (fun () -> 516 + incr calls; 517 + Error "fail") 518 + in 519 + Alcotest.(check (result string string)) "result" (Error "fail") result; 520 + Alcotest.(check int) "called only once" 1 !calls 521 + 522 + let result_success_on_last_attempt () = 523 + Eio_main.run @@ fun env -> 524 + let clock = Eio.Stdenv.clock env in 525 + let config = 526 + Retry.config ~max_retries:3 ~backoff_factor:0.001 ~jitter:false () 527 + in 528 + let calls = ref 0 in 529 + let result = 530 + Retry.with_retry_result ~clock ~config 531 + ~should_retry:(fun _ -> true) 532 + (fun () -> 533 + incr calls; 534 + if !calls < 4 then Error "not yet" else Ok "success") 535 + in 536 + Alcotest.(check (result string string)) "result" (Ok "success") result; 537 + Alcotest.(check int) "called 4 times" 4 !calls 538 + 539 + type error_kind = Transient | Permanent | Unknown 540 + 541 + let error_kind_equal a b = 542 + match (a, b) with 543 + | Transient, Transient | Permanent, Permanent | Unknown, Unknown -> true 544 + | _ -> false 545 + 546 + let pp_error_kind ppf = function 547 + | Transient -> Format.pp_print_string ppf "Transient" 548 + | Permanent -> Format.pp_print_string ppf "Permanent" 549 + | Unknown -> Format.pp_print_string ppf "Unknown" 550 + 551 + let error_kind_testable = Alcotest.testable pp_error_kind error_kind_equal 552 + 553 + let result_typed_errors () = 554 + Eio_main.run @@ fun env -> 555 + let clock = Eio.Stdenv.clock env in 556 + let config = 557 + Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 558 + in 559 + let calls = ref 0 in 560 + let should_retry = function 561 + | Transient -> true 562 + | Permanent -> false 563 + | Unknown -> false 564 + in 565 + let result = 566 + Retry.with_retry_result ~clock ~config ~should_retry (fun () -> 567 + incr calls; 568 + match !calls with 569 + | 1 -> Error Transient 570 + | 2 -> Error Transient 571 + | 3 -> Error Transient 572 + | _ -> Ok "success") 573 + in 574 + Alcotest.(check (result string error_kind_testable)) 575 + "result" (Ok "success") result; 576 + Alcotest.(check int) "called 4 times" 4 !calls 577 + 578 + let result_typed_permanent_error () = 579 + Eio_main.run @@ fun env -> 580 + let clock = Eio.Stdenv.clock env in 581 + let config = 582 + Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 583 + in 584 + let calls = ref 0 in 585 + let should_retry = function 586 + | Transient -> true 587 + | Permanent -> false 588 + | Unknown -> false 589 + in 590 + let result = 591 + Retry.with_retry_result ~clock ~config ~should_retry (fun () -> 592 + incr calls; 593 + match !calls with 1 -> Error Transient | _ -> Error Permanent) 594 + in 595 + Alcotest.(check (result string error_kind_testable)) 596 + "result" (Error Permanent) result; 597 + Alcotest.(check int) "called 2 times" 2 !calls 598 + 599 + let result_unknown_error () = 600 + Eio_main.run @@ fun env -> 601 + let clock = Eio.Stdenv.clock env in 602 + let config = 603 + Retry.config ~max_retries:5 ~backoff_factor:0.001 ~jitter:false () 604 + in 605 + let calls = ref 0 in 606 + let should_retry = function 607 + | Transient -> true 608 + | Permanent -> false 609 + | Unknown -> false 610 + in 611 + let result = 612 + Retry.with_retry_result ~clock ~config ~should_retry (fun () -> 613 + incr calls; 614 + Error Unknown) 615 + in 616 + Alcotest.(check (result string error_kind_testable)) 617 + "result" (Error Unknown) result; 618 + Alcotest.(check int) "called only once" 1 !calls 619 + 620 + let many_retries () = 621 + Eio_main.run @@ fun env -> 622 + let clock = Eio.Stdenv.clock env in 623 + let config = 624 + Retry.config ~max_retries:20 ~backoff_factor:0.0001 ~jitter:false () 625 + in 626 + let calls = ref 0 in 627 + let result = 628 + Retry.with_retry ~clock ~config 629 + ~should_retry:(fun _ -> true) 630 + (fun () -> 631 + incr calls; 632 + if !calls < 15 then failwith "not yet"; 633 + "success") 634 + in 635 + Alcotest.(check string) "result" "success" result; 636 + Alcotest.(check int) "called 15 times" 15 !calls 637 + 638 + let suite = 639 + ( "retry", 640 + [ 641 + Alcotest.test_case "default config" `Quick default_config; 642 + Alcotest.test_case "config defaults" `Quick config_defaults; 643 + Alcotest.test_case "config custom" `Quick config_custom; 644 + Alcotest.test_case "config partial" `Quick config_partial; 645 + Alcotest.test_case "pp_config" `Quick pp_config; 646 + Alcotest.test_case "backoff test vectors (no cap)" `Quick 647 + backoff_test_vectors; 648 + Alcotest.test_case "backoff test vectors (default config)" `Quick 649 + backoff_default_config_vectors; 650 + Alcotest.test_case "backoff AWS-style" `Quick backoff_aws_style; 651 + Alcotest.test_case "backoff GCP-style" `Quick backoff_gcp_style; 652 + Alcotest.test_case "backoff no jitter" `Quick backoff_no_jitter; 653 + Alcotest.test_case "backoff exponential" `Quick backoff_exponential; 654 + Alcotest.test_case "backoff capped" `Quick backoff_capped; 655 + Alcotest.test_case "backoff with jitter" `Quick backoff_with_jitter; 656 + Alcotest.test_case "backoff zero factor" `Quick backoff_zero_factor; 657 + Alcotest.test_case "backoff attempt zero" `Quick backoff_attempt_zero; 658 + Alcotest.test_case "success first try" `Quick success_first_try; 659 + Alcotest.test_case "retry then success" `Quick retry_then_success; 660 + Alcotest.test_case "exhaust retries" `Quick exhaust_retries; 661 + Alcotest.test_case "should_retry false" `Quick should_retry_false; 662 + Alcotest.test_case "selective retry" `Quick selective_retry; 663 + Alcotest.test_case "selective retry permanent" `Quick 664 + selective_retry_permanent_failure; 665 + Alcotest.test_case "zero retries" `Quick zero_retries; 666 + Alcotest.test_case "success on last attempt" `Quick 667 + success_on_last_attempt; 668 + Alcotest.test_case "different exception types" `Quick 669 + different_exception_types; 670 + Alcotest.test_case "non-retryable exception" `Quick 671 + non_retryable_exception; 672 + Alcotest.test_case "many retries" `Quick many_retries; 673 + Alcotest.test_case "result success first try" `Quick 674 + result_success_first_try; 675 + Alcotest.test_case "result retry" `Quick result_retry; 676 + Alcotest.test_case "result exhaust retries" `Quick result_exhaust_retries; 677 + Alcotest.test_case "result should_retry false" `Quick 678 + result_should_retry_false; 679 + Alcotest.test_case "result selective retry" `Quick result_selective_retry; 680 + Alcotest.test_case "result permanent error" `Quick result_permanent_error; 681 + Alcotest.test_case "result zero retries" `Quick result_zero_retries; 682 + Alcotest.test_case "result success on last attempt" `Quick 683 + result_success_on_last_attempt; 684 + Alcotest.test_case "result typed errors" `Quick result_typed_errors; 685 + Alcotest.test_case "result typed permanent error" `Quick 686 + result_typed_permanent_error; 687 + Alcotest.test_case "result unknown error" `Quick result_unknown_error; 688 + ] )
+4
test/test_retry.mli
··· 1 + (** Tests for Retry module. *) 2 + 3 + val suite : string * unit Alcotest.test_case list 4 + (** Alcotest test suite. *)