Current section

Files

Jump to
retry lib retry.ex
Raw

lib/retry.ex

defmodule Retry do
@moduledoc """
Retry functions.
"""
@doc false
defmacro __using__(_opts) do
quote do
import Retry
end
end
@doc """
Retry block of code a maximum number of times with a fixed delay between
attempts.
Example
```elixir
retry 5 in 500 do
# interact with external service
end
```
Runs the block up to 5 times with a half second sleep between each
attempt. Execution is deemed a failure if the block returns `{:error, _}` or
raises a runtime error.
"""
defmacro retry({:in, _, [retries, sleep]}, do: block) do
quote do
do_retry(
fixed_delays(unquote(retries), unquote(sleep)),
unquote(block_runner(block))
)
end
end
@doc """
Retry block of code with a exponential backoff delay between attempts.
Example
```elixir
backoff 1000, delay_cap: 100 do
# interact the external service
end
```
Runs the block repeated until it succeeds or 1 second elapses with an
exponentially increasing delay between attempts. Execution is deemed a failure
if the block returns `{:error, _}` or raises a runtime error.
The `delay_cap` is optional. If specified it will be the max duration of any
delay. In the example this is saying never delay more than 100ms between
attempts. Omitting `delay_cap` is the same as setting it to `:infinity`.
"""
defmacro backoff(time_budget, do: block) do
quote do
do_retry(
exp_backoff_delays(unquote(time_budget), :infinity),
unquote(block_runner(block))
)
end
end
defmacro backoff(time_budget, delay_cap: delay_cap, do: block) do
quote do
do_retry(
exp_backoff_delays(unquote(time_budget), unquote(delay_cap)),
unquote(block_runner(block))
)
end
end
@doc """
Executes fun until it succeeds or we have run out of retry_delays. Each retry
is preceded by a sleep of the specified retry delay.
"""
def do_retry(retry_delays, fun) do
delays = [0] |> Stream.concat(retry_delays)
final_result = delays |> Enum.reduce_while(nil, fn(delay, _last_result) ->
:timer.sleep(delay)
fun.()
end)
case final_result do
{:exception, e} -> raise e
result -> result
end
end
defp block_runner(block) do
quote do
fn ->
try do
case unquote(block) do
{:error, _} = result -> {:cont, result}
:error = result -> {:cont, result}
result -> {:halt, result}
end
rescue
e in RuntimeError -> {:cont, {:exception, e}}
end
end
end
end
@doc """
Returns stream of delays that are exponentially increasing. Stream halts once
the specified budget of milliseconds has elapsed.
"""
def exp_backoff_delays(budget, delay_cap) do
Stream.unfold({1, :os.system_time(:milli_seconds) + budget}, fn {failures, end_t} ->
next_delay = figure_exp_delay(failures, delay_cap)
now_t = :os.system_time(:milli_seconds)
cond do
now_t > end_t ->
nil # out of time
(now_t + next_delay) > end_t ->
{end_t - now_t, {failures + 1, end_t}} # one last try
true ->
{next_delay, {failures + 1, end_t}}
end
end)
end
@doc """
Returns stream that returns specified number of the specified delay.
"""
def fixed_delays(count, delay) do
[delay]
|> Stream.cycle
|> Stream.take(count)
end
defp figure_exp_delay(failures, :infinity) do
:erlang.round((1 + :random.uniform) * 10 * :math.pow(2, failures))
end
defp figure_exp_delay(failures, delay_cap) do
Enum.min([
figure_exp_delay(failures, :infinity),
delay_cap
])
end
end