Current section

Files

Jump to
persevero src persevero.gleam
Raw

src/persevero.gleam

//// `persevero` executes a fallible operation multiple times.
import gleam/erlang/process
import gleam/int
import gleam/list
import gleam/yielder.{type Yielder}
/// Represents errors that can occur during execution attempts.
pub type Error(a) {
/// Indicates that all execution attempts have been exhausted. Contains an
/// ordered list of all errors encountered during the execution attempts.
RetriesExhausted(errors: List(a))
/// Indicates that an error that wasn't allowed was encountered. Contains the
/// specific error that caused execution to stop.
UnallowedError(error: a)
}
type RetryResult(a, b) =
Result(a, Error(b))
@internal
pub type RetryData(a, b) {
RetryData(result: RetryResult(a, b), wait_times: List(Int))
}
/// Creates a new configuration with the specified `wait_time` and `backoff`
/// function.
///
/// The `backoff` function determines how the wait time changes between
/// attempts. It takes the previous wait time as input and returns the next wait
/// time.
pub fn new(
wait_time wait_time: Int,
backoff backoff: fn(Int) -> Int,
) -> Yielder(Int) {
yielder.unfold(wait_time, fn(acc) { yielder.Next(acc, backoff(acc)) })
}
/// Adds a random integer between [1, `upper_bound`] to each wait time.
pub fn apply_jitter(
yielder yielder: Yielder(Int),
upper_bound upper_bound: Int,
) -> Yielder(Int) {
apply_constant(yielder: yielder, adjustment: int.random(upper_bound) + 1)
}
/// Adds a constant integer to each wait time.
pub fn apply_constant(
yielder yielder: Yielder(Int),
adjustment adjustment: Int,
) -> Yielder(Int) {
yielder |> yielder.map(int.add(_, adjustment))
}
/// Sets a maximum time limit to wait between execution attempts.
pub fn max_wait_time(
yielder yielder: Yielder(Int),
max_wait_time max_wait_time: Int,
) -> Yielder(Int) {
yielder |> yielder.map(int.min(_, max_wait_time))
}
/// Initiates the execution process with the specified operation.
///
/// `allow` sets the logic for determining whether an error should trigger
/// another attempt. Expects a function that takes an error and returns a
/// boolean. Use this function to match on the encountered error and return
/// `True` for errors that should trigger another attempt, and `False` for
/// errors that should not. To allow all errors, use `fn(_) { True }`.
pub fn execute(
yielder yielder: Yielder(Int),
allow allow: fn(b) -> Bool,
max_attempts max_attempts: Int,
operation operation: fn() -> Result(a, b),
) -> RetryResult(a, b) {
execute_with_wait(
yielder: yielder,
allow: allow,
max_attempts: max_attempts,
operation: fn(_) { operation() },
wait_function: process.sleep,
).result
}
@internal
pub fn execute_with_wait(
yielder yielder: Yielder(Int),
allow allow: fn(b) -> Bool,
max_attempts max_attempts: Int,
operation operation: fn(Int) -> Result(a, b),
wait_function wait_function: fn(Int) -> Nil,
) -> RetryData(a, b) {
case max_attempts <= 0 {
True -> RetryData(result: Error(RetriesExhausted([])), wait_times: [])
False -> {
let yielder = yielder |> yielder.take(max_attempts - 1)
let yielder =
yielder.from_list([0])
|> yielder.append(yielder)
|> yielder.map(int.max(_, 0))
do_execute(
yielder: yielder,
allow: allow,
max_attempts: max_attempts,
operation: operation,
wait_function: wait_function,
wait_time_acc: [],
errors_acc: [],
attempt_number: 0,
)
}
}
}
fn do_execute(
yielder yielder: Yielder(Int),
allow allow: fn(b) -> Bool,
max_attempts max_attempts: Int,
operation operation: fn(Int) -> Result(a, b),
wait_function wait_function: fn(Int) -> Nil,
wait_time_acc wait_time_acc: List(Int),
errors_acc errors_acc: List(b),
attempt_number attempt_number: Int,
) -> RetryData(a, b) {
case yielder |> yielder.step() {
yielder.Next(wait_time, yielder) -> {
wait_function(wait_time)
let wait_time_acc = [wait_time, ..wait_time_acc]
case operation(attempt_number) {
Ok(result) ->
RetryData(
result: Ok(result),
wait_times: wait_time_acc |> list.reverse,
)
Error(error) -> {
case allow(error) {
True ->
do_execute(
yielder: yielder,
allow: allow,
max_attempts: max_attempts,
operation: operation,
wait_function: wait_function,
wait_time_acc: wait_time_acc,
errors_acc: [error, ..errors_acc],
attempt_number: attempt_number + 1,
)
False ->
RetryData(
result: Error(UnallowedError(error)),
wait_times: wait_time_acc |> list.reverse,
)
}
}
}
}
yielder.Done ->
RetryData(
result: Error(RetriesExhausted(errors_acc |> list.reverse)),
wait_times: wait_time_acc |> list.reverse,
)
}
}