
# Module `Lwt_retry`

Utilities for retrying Lwt computations

These utilities are useful for dealing with failure-prone computations that are expected to succeed after some number of repeated attempts. E.g.,

```ocaml
  let flaky_computation () = match try_to_get_resource () with
    | Flaky_error msg -> Error (`Retry msg)
    | Fatal_error err -> Error (`Fatal err)
    | Success  result -> Ok result

  let error_tolerant_computation () =
    Lwt_retry.(flaky_computation
               |> on_error    (* Retry when [`Retry]able results are produced. *)
               |> with_sleep  (* Add a delay between attempts, with an exponential backoff. *)
               |> n_times 10  (* Try up to 10 times, so long as errors are retryable. *)
              )
```
This library provides a few combinators, but retry attempts are produced on demand in an [`Lwt_stream.t`](./../lwt/Lwt_stream.md#type-t), and they can be consumed and traversed using the [`Lwt_stream`](./../lwt/Lwt_stream.md) functions directly.

```ocaml
type ('retry, 'fatal) error = [ 
  | `Retry of 'retry
  | `Fatal of 'fatal
 ]
```
The type of errors that a retryable computation can produce.

- ``Retry r` when `r` represents an error that can be retried.
- ``Fatal f` when `f` represents an error that cannot be retried.
```ocaml
type ('ok, 'retry, 'fatal) attempt =
  ('ok, ('retry, 'fatal) error * int) Stdlib.result
```
A `('ok, 'retry, 'fatal) attempt` is the `result` of a retryable computation, with its the erroneous results enumerated.

- `Ok v` is a successfully computed value `v`
- `Error (err, n)` is the [`error`](./#type-error) `err` produced on the `n`th attempt
The enumeration of attempts is 1-based, because making 0 attempts means making no attempts all, making 1 attempt means *trying* once, and (when `i>0`) making `n` attempts means trying once and then *retrying* up to `n-1` times.

```ocaml
val pp_error : 
  ?retry:(Stdlib.Format.formatter -> 'retry -> unit) ->
  ?fatal:(Stdlib.Format.formatter -> 'fatal -> unit) ->
  Stdlib.Format.formatter ->
  ('retry, 'fatal) error ->
  unit
```
`pp_error ~retry ~fatal` is a pretty printer for [`error`](./#type-error)s that formats fatal and retryable errors according to the provided printers.

If a printers is not provided, values of the type are represented as `"<opaque>"`.

```ocaml
val equal_error : 
  retry:('retry -> 'retry -> bool) ->
  fatal:('fatal -> 'fatal -> bool) ->
  ('retry, 'fatal) error ->
  ('retry, 'fatal) error ->
  bool
```
```ocaml
val on_error : 
  (unit -> ('ok, ('retry, 'fatal) error) Stdlib.result Lwt.t) ->
  ('ok, 'retry, 'fatal) attempt Lwt_stream.t
```
`Lwt_retry.on_error f` is a stream of attempts to compute `f`, with attempts made on demand. Attempts will be added to the stream when results are requested until the computation either succeeds or produces a fatal error.

Examples

```ocaml
  # let success () = Lwt.return_ok ();;
  val success : unit -> (unit, 'a) result Lwt.t = <fun>
  # Lwt_retry.(success |> on_error) |> Lwt_stream.to_list;;
  - : (unit, 'a, 'b) Lwt_retry.attempt list = [Ok ()]

  # let fatal_failure () = Lwt.return_error (`Fatal ());;
  val fatal_failure : unit -> ('a, [> `Fatal of unit ]) result Lwt.t = <fun>
  # Lwt_retry.(fatal_failure |> on_error) |> Lwt_stream.to_list;;
  - : ('a, 'b, unit) Lwt_retry.attempt list = [Error (`Fatal (), 1)]

  # let retryable_error () = Lwt.return_error (`Retry ());;
  val retryable_error : unit -> ('a, [> `Retry of unit ]) result Lwt.t = <fun>
  # Lwt_retry.(retryable_error |> on_error) |> Lwt_stream.nget 3;;
  - : ('a, unit, 'b) Lwt_retry.attempt list =
  [Error (`Retry (), 1); Error (`Retry (), 2); Error (`Retry (), 3)]
```
```ocaml
val with_sleep : 
  ?duration:(int -> float) ->
  ('ok, 'retry, 'fatal) attempt Lwt_stream.t ->
  ('ok, 'retry, 'fatal) attempt Lwt_stream.t
```
`with_sleep ~duration attempts` is the stream of `attempts` with a sleep of `duration n` seconds added before computing each `n`th retryable attempt.

parameter duration the optional sleep duration calculation, defaulting to default\_sleep\_duration.
Examples

```ocaml
  # let f () = Lwt.return_error (`Retry ());;
  # let attempts_with_sleeps = Lwt_retry.(f |> on_error |> with_sleep);;

  # Lwt_stream.get attempts_with_sleeps;;
  (* computed immediately *)
  Some (Error (`Retry (), 1))

  # Lwt_stream.get attempts_with_sleeps;;
  (* computed after 3 seconds *)
  Some (Error (`Retry (), 2))

  # Lwt_stream.get attempts_with_sleeps;;
  (* computed after 9 seconds *)
  Some (Error (`Retry (), 3))

  (* a stream with a constant 1s sleep between attempts *)
  # let attempts_with_constant_sleeps =
      Lwt_retry.(f |> on_error |> with_sleep ~duration:(fun _ -> 1.0));;
```
```ocaml
val default_sleep_duration : int -> float
```
`default_sleep_duration n` is an exponential backoff computed as `n` \* 2 \* (2 ^ `n`), which gives the sequence ` [0.; 4.; 16.; 48.; 128.; 320.; 768.; 1792.; ...] `.

```ocaml
val n_times : 
  int ->
  ('ok, 'retry, 'fatal) attempt Lwt_stream.t ->
  ('ok, 'retry, 'fatal) attempt Lwt.t
```
`n_times n attempts` is `Ok v` if one of the `attempts` succeeds within `n` retries (or `n+1` attempts), `Error (`Fatal f, n+1)` if any of the attempts results in the fatal error, or `Error (`Retry r, n+1)` if all `n` retries are exhausted and the `n+1`th attempt results in a retry error.

In particular `n_times 0 attempts` will \*try\* 1 attempt but \*re-try\* 0, so it is guaranteed to produce some result.

`n_times` forces up to `n` elements of the on-demand stream of attempts.

Examples

```ocaml
  # let f () =
      let i = ref 0 in
      fun () -> Lwt.return_error (if !i < 3 then (incr i; `Retry ()) else `Fatal "error!");;
  # Lwt_retry.(f () |> on_error |> n_times 0);;
  Error (`Retry (), 1)
  # Lwt_retry.(f () |> on_error |> n_times 4);;
  Error (`Fatal "error!", 3)
```