let src = Logs.Src.create "requests.retry" ~doc:"HTTP Request Retry Logic" module Log = (val Logs.src_log src : Logs.LOG) type config = { max_retries : int; backoff_factor : float; backoff_max : float; status_forcelist : int list; allowed_methods : Method.t list; respect_retry_after : bool; jitter : bool; } let default_config = { max_retries = 3; backoff_factor = 0.3; backoff_max = 120.0; status_forcelist = [408; 429; 500; 502; 503; 504]; allowed_methods = [`GET; `HEAD; `PUT; `DELETE; `OPTIONS; `TRACE]; respect_retry_after = true; jitter = true; } let create_config ?(max_retries = 3) ?(backoff_factor = 0.3) ?(backoff_max = 120.0) ?(status_forcelist = [408; 429; 500; 502; 503; 504]) ?(allowed_methods = [`GET; `HEAD; `PUT; `DELETE; `OPTIONS; `TRACE]) ?(respect_retry_after = true) ?(jitter = true) () = Log.debug (fun m -> m "Creating retry config: max_retries=%d backoff_factor=%.2f" max_retries backoff_factor); { max_retries; backoff_factor; backoff_max; status_forcelist; allowed_methods; respect_retry_after; jitter; } let should_retry ~config ~method_ ~status = let should = List.mem method_ config.allowed_methods && List.mem status config.status_forcelist in Log.debug (fun m -> m "Should retry? method=%s status=%d -> %b" (Method.to_string method_) status should); should let calculate_backoff ~config ~attempt = let base_delay = config.backoff_factor *. (2.0 ** float_of_int attempt) in let delay = if config.jitter then (* Add random jitter between 0 and base_delay *) base_delay +. Random.float base_delay else base_delay in let final_delay = min delay config.backoff_max in Log.debug (fun m -> m "Backoff calculation: attempt=%d base=%.2f jitter=%b -> %.2f seconds" attempt base_delay config.jitter final_delay); final_delay let parse_retry_after value = Log.debug (fun m -> m "Parsing Retry-After header: %s" value); (* First try to parse as integer (delay in seconds) *) match int_of_string_opt value with | Some seconds -> Log.debug (fun m -> m "Retry-After is %d seconds" seconds); Some (float_of_int seconds) | None -> (* Try to parse as HTTP date *) (* This is simplified - real implementation would use a proper HTTP date parser *) try let time, _tz_offset, _tz_string = Ptime.of_rfc3339 value |> Result.get_ok in let now = Unix.time () in let target = Ptime.to_float_s time in let delay = max 0.0 (target -. now) in Log.debug (fun m -> m "Retry-After is HTTP date, delay=%.2f seconds" delay); Some delay with _ -> Log.warn (fun m -> m "Failed to parse Retry-After header: %s" value); None let with_retry ~sw:_ ~clock ~config ~f ~should_retry_exn = let rec attempt_with_retry attempt = Log.info (fun m -> m "Attempt %d/%d" attempt (config.max_retries + 1)); match f () with | result -> if attempt > 1 then Log.info (fun m -> m "Request succeeded after %d attempts" attempt); result | exception exn when attempt <= config.max_retries && should_retry_exn exn -> let delay = calculate_backoff ~config ~attempt in Log.warn (fun m -> m "Request failed (attempt %d/%d): %s. Retrying in %.2f seconds..." attempt (config.max_retries + 1) (Printexc.to_string exn) delay); (* Sleep for the backoff duration *) Eio.Time.sleep clock delay; attempt_with_retry (attempt + 1) | exception exn -> if attempt > config.max_retries then Log.err (fun m -> m "Request failed after %d attempts: %s" attempt (Printexc.to_string exn)) else Log.err (fun m -> m "Request failed and won't be retried: %s" (Printexc.to_string exn)); raise exn in attempt_with_retry 1 let pp_config ppf config = Format.fprintf ppf "@[Retry Configuration:@,\ @[\ max_retries: %d@,\ backoff_factor: %.2f@,\ backoff_max: %.1f seconds@,\ status_forcelist: [%a]@,\ allowed_methods: [%a]@,\ respect_retry_after: %b@,\ jitter: %b\ @]@]" config.max_retries config.backoff_factor config.backoff_max Format.(pp_print_list ~pp_sep:(fun ppf () -> fprintf ppf ", ") pp_print_int) config.status_forcelist Format.(pp_print_list ~pp_sep:(fun ppf () -> fprintf ppf ", ") (fun ppf m -> pp_print_string ppf (Method.to_string m))) config.allowed_methods config.respect_retry_after config.jitter let log_retry ~attempt ~delay ~reason = Log.info (fun m -> m "Retry attempt %d scheduled in %.2f seconds. Reason: %s" attempt delay reason)