Current section
Files
Jump to
Current section
Files
src/persevero.erl
-module(persevero).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/persevero.gleam").
-export([all_errors/1, custom_backoff/2, no_backoff/0, constant_backoff/1, linear_backoff/2, exponential_backoff/2, apply_constant/2, apply_jitter/2, apply_multiplier/2, apply_cap/2, execute_with_options/6, execute/4]).
-export_type([error/1, retry_data/2, mode/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" `persevero` executes a fallible operation multiple times.\n"
"\n"
" ```gleam\n"
" import gleam/http/request\n"
" import gleam/httpc\n"
" import gleam/io\n"
" import persevero\n"
"\n"
" pub fn main() {\n"
" let assert Ok(request) = request.to(\"https://www.apple.com\")\n"
"\n"
" let response = {\n"
" use <- persevero.execute(\n"
" wait_stream: persevero.exponential_backoff(50, 2),\n"
" allow: persevero.all_errors,\n"
" mode: persevero.MaxAttempts(3),\n"
" )\n"
"\n"
" httpc.send(request)\n"
" }\n"
"\n"
" case response {\n"
" Ok(response) if response.status == 200 ->\n"
" io.println(\"Give me #prawducks. 😃\")\n"
" _ -> io.println(\"Guess I'll dev on Linux. 😔\")\n"
" }\n"
" }\n"
" ```\n"
).
-type error(HAG) :: {retries_exhausted, list(HAG)} |
{time_exhausted, list(HAG)} |
{unallowed_error, HAG}.
-type retry_data(HAH, HAI) :: {retry_data,
{ok, HAH} | {error, error(HAI)},
list(integer()),
integer()}.
-type mode() :: {max_attempts, integer()} | {expiry, integer()}.
-file("src/persevero.gleam", 64).
?DOC(
" Convenience function that can supplied to `execute`'s `allow` parameter to\n"
" allow all errors.\n"
).
-spec all_errors(any()) -> boolean().
all_errors(_) ->
true.
-file("src/persevero.gleam", 69).
?DOC(" Produces a custom ms wait stream.\n").
-spec custom_backoff(integer(), fun((integer()) -> integer())) -> gleam@yielder:yielder(integer()).
custom_backoff(Wait_time, Next_wait_time) ->
gleam@yielder:iterate(Wait_time, Next_wait_time).
-file("src/persevero.gleam", 78).
?DOC(
" Produces a 0ms wait stream.\n"
" Ex: 0ms, 0ms, 0ms, ...\n"
).
-spec no_backoff() -> gleam@yielder:yielder(integer()).
no_backoff() ->
gleam@yielder:repeat(0).
-file("src/persevero.gleam", 84).
?DOC(
" Produces a ms wait stream with a constant wait time.\n"
" Ex: 500ms, 500ms, 500ms, ...\n"
).
-spec constant_backoff(integer()) -> gleam@yielder:yielder(integer()).
constant_backoff(Wait_time) ->
gleam@yielder:iterate(Wait_time, fun gleam@function:identity/1).
-file("src/persevero.gleam", 90).
?DOC(
" Produces a ms wait stream that increases linearly for each attempt.\n"
" Ex: 500ms, 1000ms, 1500ms, ...\n"
).
-spec linear_backoff(integer(), integer()) -> gleam@yielder:yielder(integer()).
linear_backoff(Wait_time, Step) ->
gleam@yielder:iterate(
Wait_time,
fun(_capture) -> gleam@int:add(_capture, Step) end
).
-file("src/persevero.gleam", 97).
?DOC(
" Produces a ms wait stream that increases exponentially for each attempt.\n"
" time:\n"
" Ex: 500ms, 1000ms, 2000ms, ...\n"
).
-spec exponential_backoff(integer(), integer()) -> gleam@yielder:yielder(integer()).
exponential_backoff(Wait_time, Factor) ->
gleam@yielder:iterate(
Wait_time,
fun(_capture) -> gleam@int:multiply(_capture, Factor) end
).
-file("src/persevero.gleam", 113).
?DOC(" Adds a constant integer to each wait time.\n").
-spec apply_constant(gleam@yielder:yielder(integer()), integer()) -> gleam@yielder:yielder(integer()).
apply_constant(Wait_stream, Adjustment) ->
_pipe = Wait_stream,
gleam@yielder:map(
_pipe,
fun(_capture) -> gleam@int:add(_capture, Adjustment) end
).
-file("src/persevero.gleam", 105).
?DOC(" Adds a random integer between [1, `upper_bound`] to each wait time.\n").
-spec apply_jitter(gleam@yielder:yielder(integer()), integer()) -> gleam@yielder:yielder(integer()).
apply_jitter(Wait_stream, Upper_bound) ->
apply_constant(Wait_stream, gleam@int:random(Upper_bound) + 1).
-file("src/persevero.gleam", 121).
?DOC(" Multiplies each wait time by a constant factor.\n").
-spec apply_multiplier(gleam@yielder:yielder(integer()), integer()) -> gleam@yielder:yielder(integer()).
apply_multiplier(Wait_stream, Factor) ->
_pipe = Wait_stream,
gleam@yielder:map(
_pipe,
fun(_capture) -> gleam@int:multiply(_capture, Factor) end
).
-file("src/persevero.gleam", 129).
?DOC(" Caps each wait time at a maximum value.\n").
-spec apply_cap(gleam@yielder:yielder(integer()), integer()) -> gleam@yielder:yielder(integer()).
apply_cap(Wait_stream, Max_wait_time) ->
_pipe = Wait_stream,
gleam@yielder:map(
_pipe,
fun(_capture) -> gleam@int:min(_capture, Max_wait_time) end
).
-file("src/persevero.gleam", 199).
-spec configure_wait_stream(gleam@yielder:yielder(integer()), mode()) -> gleam@yielder:yielder(integer()).
configure_wait_stream(Wait_stream, Mode) ->
Wait_stream@1 = begin
_pipe = Wait_stream,
_pipe@1 = gleam@yielder:prepend(_pipe, 0),
gleam@yielder:map(
_pipe@1,
fun(_capture) -> gleam@int:max(_capture, 0) end
)
end,
case Mode of
{max_attempts, Max_attempts} ->
_pipe@2 = Wait_stream@1,
gleam@yielder:take(_pipe@2, Max_attempts);
{expiry, _} ->
Wait_stream@1
end.
-file("src/persevero.gleam", 214).
-spec do_execute(
gleam@yielder:yielder(integer()),
fun((HBT) -> boolean()),
mode(),
fun((integer()) -> {ok, HBU} | {error, HBT}),
fun((integer()) -> nil),
list(integer()),
bigben@clock:clock(),
list(HBT),
integer(),
gleam@time@timestamp:timestamp(),
integer()
) -> retry_data(HBU, HBT).
do_execute(
Wait_stream,
Allow,
Mode,
Operation,
Wait_function,
Wait_time_acc,
Clock,
Errors_acc,
Attempt,
Start_time,
Duration
) ->
Should_execute = case Mode of
{max_attempts, Max_attempts} ->
Max_attempts > 0;
{expiry, Expiry} ->
(Expiry > 0) andalso (Duration < Expiry)
end,
case {Should_execute,
begin
_pipe = Wait_stream,
gleam@yielder:step(_pipe)
end} of
{true, {next, Wait_time, Wait_stream@1}} ->
Wait_function(Wait_time),
Wait_time_acc@1 = [Wait_time | Wait_time_acc],
Duration@1 = begin
_pipe@1 = Start_time,
_pipe@2 = gleam@time@timestamp:difference(
_pipe@1,
bigben@clock:now(Clock)
),
_pipe@3 = gleam@time@duration:to_seconds_and_nanoseconds(
_pipe@2
),
(fun(Seconds_nanoseconds) ->
{Seconds, Nanoseconds} = Seconds_nanoseconds,
(Seconds * 1000) + (Nanoseconds div 1000000)
end)(_pipe@3)
end,
case Operation(Attempt) of
{ok, Result} ->
{retry_data,
{ok, Result},
begin
_pipe@4 = Wait_time_acc@1,
lists:reverse(_pipe@4)
end,
Duration@1};
{error, Error} ->
case Allow(Error) of
true ->
do_execute(
Wait_stream@1,
Allow,
Mode,
Operation,
Wait_function,
Wait_time_acc@1,
Clock,
[Error | Errors_acc],
Attempt + 1,
Start_time,
Duration@1
);
false ->
{retry_data,
{error, {unallowed_error, Error}},
begin
_pipe@5 = Wait_time_acc@1,
lists:reverse(_pipe@5)
end,
Duration@1}
end
end;
{_, _} ->
Error@1 = case Mode of
{max_attempts, _} ->
{retries_exhausted,
begin
_pipe@6 = Errors_acc,
lists:reverse(_pipe@6)
end};
{expiry, _} ->
{time_exhausted,
begin
_pipe@7 = Errors_acc,
lists:reverse(_pipe@7)
end}
end,
{retry_data,
{error, Error@1},
begin
_pipe@8 = Wait_time_acc,
lists:reverse(_pipe@8)
end,
Duration}
end.
-file("src/persevero.gleam", 176).
?DOC(false).
-spec execute_with_options(
gleam@yielder:yielder(integer()),
fun((HBK) -> boolean()),
mode(),
fun((integer()) -> {ok, HBL} | {error, HBK}),
fun((integer()) -> nil),
bigben@clock:clock()
) -> retry_data(HBL, HBK).
execute_with_options(Wait_stream, Allow, Mode, Operation, Wait_function, Clock) ->
do_execute(
begin
_pipe = Wait_stream,
configure_wait_stream(_pipe, Mode)
end,
Allow,
Mode,
Operation,
Wait_function,
[],
Clock,
[],
0,
begin
_pipe@1 = Clock,
bigben@clock:now(_pipe@1)
end,
0
).
-file("src/persevero.gleam", 159).
?DOC(
" Initiates the execution process with the specified operation.\n"
"\n"
" `allow` sets the logic for determining whether an error should trigger\n"
" another attempt. Expects a function that takes an error and returns a\n"
" boolean. Use this function to match on the encountered error and return\n"
" `True` for errors that should trigger another attempt, and `False` for\n"
" errors that should not. To allow all errors, use `all_errors`.\n"
).
-spec execute(
gleam@yielder:yielder(integer()),
fun((HBD) -> boolean()),
mode(),
fun(() -> {ok, HBE} | {error, HBD})
) -> {ok, HBE} | {error, error(HBD)}.
execute(Wait_stream, Allow, Mode, Operation) ->
erlang:element(
2,
execute_with_options(
Wait_stream,
Allow,
Mode,
fun(_) -> Operation() end,
fun gleam_erlang_ffi:sleep/1,
bigben@clock:new()
)
).