|
| 1 | +(* This file is part of Lwt, released under the MIT license. See LICENSE.md for |
| 2 | + details, or visit https://github.com/ocsigen/lwt/blob/master/LICENSE.md. *) |
| 3 | + |
| 4 | + |
| 5 | + |
| 6 | +(** Utilities for retrying Lwt computations |
| 7 | +
|
| 8 | + These utilities are useful for dealing with failure-prone computations that |
| 9 | + are expected to succeed after some number of repeated attempts. E.g., |
| 10 | +
|
| 11 | + {[ |
| 12 | + let flaky_computation () = match try_to_get_resource () with |
| 13 | + | Flaky_error msg -> Error (`Retry msg) |
| 14 | + | Fatal_error err -> Error (`Fatal err) |
| 15 | + | Success result -> Ok result |
| 16 | +
|
| 17 | + let error_tolerant_computation () = |
| 18 | + Lwt_retry.(flaky_computation |
| 19 | + |> on_error (* Retry when [`Retry]able results are produced. *) |
| 20 | + |> with_sleep (* Add a delay between attempts, with an exponential backoff. *) |
| 21 | + |> n_times 10 (* Try up to 10 times, so long as errors are retryable. *) |
| 22 | + ) |
| 23 | + ]} |
| 24 | +
|
| 25 | + This library provides a few combinators, but retry attempts are produced on |
| 26 | + demand in an {!type:Lwt_stream.t}, and they can be consumed and traversed |
| 27 | + using the {!module:Lwt_stream} functions directly. *) |
| 28 | + |
| 29 | +type ('retry, 'fatal) error = |
| 30 | + [ `Retry of 'retry |
| 31 | + | `Fatal of 'fatal |
| 32 | + ] |
| 33 | +(** The type of errors that a retryable computation can produce. |
| 34 | +
|
| 35 | + - [`Retry r] when [r] represents an error that can be retried. |
| 36 | + - [`Fatal f] when [f] represents an error that cannot be retried. *) |
| 37 | + |
| 38 | +type ('ok, 'retry, 'fatal) attempt = ('ok, ('retry, 'fatal) error * int) result |
| 39 | +(** A [('ok, 'retry, 'fatal) attempt] is the [result] of a retryable computation, |
| 40 | + with its the erroneous results enumerated. |
| 41 | +
|
| 42 | + - [Ok v] is a successfully computed value [v] |
| 43 | + - [Error (err, n)] is the {!type:error} [err] produced on the [n]th |
| 44 | + attempt |
| 45 | +
|
| 46 | + The enumeration of attempts is 1-based, because making 0 attempts means |
| 47 | + making no attempts all, making 1 attempt means {i trying} once, and (when |
| 48 | + [i>0]) making [n] attempts means trying once and then {i retrying} up to |
| 49 | + [n-1] times. *) |
| 50 | + |
| 51 | +val pp_error : |
| 52 | + ?retry:(Format.formatter -> 'retry -> unit) -> |
| 53 | + ?fatal:(Format.formatter -> 'fatal -> unit) -> |
| 54 | + Format.formatter -> ('retry, 'fatal) error -> unit |
| 55 | +(** [pp_error ~retry ~fatal] is a pretty printer for {!type:error}s that formats |
| 56 | + fatal and retryable errors according to the provided printers. |
| 57 | +
|
| 58 | + If a printers is not provided, values of the type are represented as |
| 59 | + ["<opaque>"]. *) |
| 60 | + |
| 61 | +val equal_error : |
| 62 | + retry:('retry -> 'retry -> bool) -> |
| 63 | + fatal:('fatal -> 'fatal -> bool) -> |
| 64 | + ('retry, 'fatal) error -> |
| 65 | + ('retry, 'fatal) error -> |
| 66 | + bool |
| 67 | + |
| 68 | +val on_error : |
| 69 | + (unit -> ('ok, ('retry, 'fatal) error) result Lwt.t) -> |
| 70 | + ('ok, 'retry, 'fatal) attempt Lwt_stream.t |
| 71 | +(** [Lwt_retry.on_error f] is a stream of attempts to compute [f], with attempts |
| 72 | + made on demand. Attempts will be added to the stream when results are |
| 73 | + requested until the computation either succeeds or produces a fatal error. |
| 74 | +
|
| 75 | + Examples |
| 76 | +
|
| 77 | + {[ |
| 78 | + # let success () = Lwt.return_ok ();; |
| 79 | + val success : unit -> (unit, 'a) result Lwt.t = <fun> |
| 80 | + # Lwt_retry.(success |> on_error) |> Lwt_stream.to_list;; |
| 81 | + - : (unit, 'a, 'b) Lwt_retry.attempt list = [Ok ()] |
| 82 | +
|
| 83 | + # let fatal_failure () = Lwt.return_error (`Fatal ());; |
| 84 | + val fatal_failure : unit -> ('a, [> `Fatal of unit ]) result Lwt.t = <fun> |
| 85 | + # Lwt_retry.(fatal_failure |> on_error) |> Lwt_stream.to_list;; |
| 86 | + - : ('a, 'b, unit) Lwt_retry.attempt list = [Error (`Fatal (), 1)] |
| 87 | +
|
| 88 | + # let retryable_error () = Lwt.return_error (`Retry ());; |
| 89 | + val retryable_error : unit -> ('a, [> `Retry of unit ]) result Lwt.t = <fun> |
| 90 | + # Lwt_retry.(retryable_error |> on_error) |> Lwt_stream.nget 3;; |
| 91 | + - : ('a, unit, 'b) Lwt_retry.attempt list = |
| 92 | + [Error (`Retry (), 1); Error (`Retry (), 2); Error (`Retry (), 3)] |
| 93 | + ]}*) |
| 94 | + |
| 95 | +val with_sleep : |
| 96 | + ?duration:(int -> float) -> |
| 97 | + ('ok, 'retry, 'fatal) attempt Lwt_stream.t -> |
| 98 | + ('ok, 'retry, 'fatal) attempt Lwt_stream.t |
| 99 | +(** [with_sleep ~duration attempts] is the stream of [attempts] with a sleep of |
| 100 | + [duration n] seconds added before computing each [n]th retryable attempt. |
| 101 | +
|
| 102 | + @param duration the optional sleep duration calculation, defaulting to |
| 103 | + {!val:default_sleep_duration}. |
| 104 | +
|
| 105 | + Examples |
| 106 | +
|
| 107 | + {[ |
| 108 | + # let f () = Lwt.return_error (`Retry ());; |
| 109 | + # let attempts_with_sleeps = Lwt_retry.(f |> on_error |> with_sleep);; |
| 110 | +
|
| 111 | + # Lwt_stream.get attempts_with_sleeps;; |
| 112 | + (* computed immediately *) |
| 113 | + Some (Error (`Retry (), 1)) |
| 114 | +
|
| 115 | + # Lwt_stream.get attempts_with_sleeps;; |
| 116 | + (* computed after 3 seconds *) |
| 117 | + Some (Error (`Retry (), 2)) |
| 118 | +
|
| 119 | + # Lwt_stream.get attempts_with_sleeps;; |
| 120 | + (* computed after 9 seconds *) |
| 121 | + Some (Error (`Retry (), 3)) |
| 122 | +
|
| 123 | + (* a stream with a constant 1s sleep between attempts *) |
| 124 | + # let attempts_with_constant_sleeps = |
| 125 | + Lwt_retry.(f |> on_error |> with_sleep ~duration:(fun _ -> 1.0));; |
| 126 | + ]} *) |
| 127 | + |
| 128 | +val default_sleep_duration : int -> float |
| 129 | +(** [default_sleep_duration n] is an exponential backoff computed as [n] * 2 * |
| 130 | + (2 ^ [n]), which gives the sequence [ [0.; 4.; 16.; 48.; 128.; 320.; 768.; |
| 131 | + 1792.; ...] ]. *) |
| 132 | + |
| 133 | +val n_times : |
| 134 | + int -> |
| 135 | + ('ok, 'retry, 'fatal) attempt Lwt_stream.t -> |
| 136 | + ('ok, 'retry, 'fatal) attempt Lwt.t |
| 137 | +(** [n_times n attempts] is [Ok v] if one of the [attempts] succeeds within [n] |
| 138 | + retries (or [n+1] attempts), [Error (`Fatal f, n+1)] if any of the attempts |
| 139 | + results in the fatal error, or [Error (`Retry r, n+1)] if all [n] retries are |
| 140 | + exhausted and the [n+1]th attempt results in a retry error. |
| 141 | +
|
| 142 | + In particular [n_times 0 attempts] will *try* 1 attempt but *re-try* 0, so |
| 143 | + it is guaranteed to produce some result. |
| 144 | +
|
| 145 | + [n_times] forces up to [n] elements of the on-demand stream of attempts. |
| 146 | +
|
| 147 | + Examples |
| 148 | +
|
| 149 | + {[ |
| 150 | + # let f () = |
| 151 | + let i = ref 0 in |
| 152 | + fun () -> Lwt.return_error (if !i < 3 then (incr i; `Retry ()) else `Fatal "error!");; |
| 153 | + # Lwt_retry.(f () |> on_error |> n_times 0);; |
| 154 | + Error (`Retry (), 1) |
| 155 | + # Lwt_retry.(f () |> on_error |> n_times 4);; |
| 156 | + Error (`Fatal "error!", 3) |
| 157 | + ]} *) |
0 commit comments