-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() ) ).