Packages
moar
1.17.0
5.0.0
4.3.0
4.2.0
4.1.0
4.0.0
3.2.0
3.1.0
3.0.0
2.7.0
2.6.0
2.5.0
2.4.2
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.1
2.0.0
1.64.0
1.63.1
1.63.0
1.62.0
1.61.2
1.61.1
1.61.0
1.60.0
1.59.2
1.59.1
1.59.0
1.58.0
1.57.0
1.56.2
1.56.1
1.56.0
1.55.0
1.54.0
1.53.0
1.52.1
1.52.0
1.51.0
1.50.0
1.49.0
1.48.0
1.47.0
1.46.0
1.45.1
1.45.0
1.44.0
1.43.0
1.42.0
1.41.0
1.40.0
1.39.0
1.38.0
1.37.0
1.36.0
1.35.0
1.34.0
1.33.0
1.32.0
1.31.0
1.30.0
1.29.0
1.28.0
1.27.0
1.26.0
1.25.0
1.24.1
1.24.0
1.23.0
1.22.0
1.21.0
1.20.0
1.19.3
1.19.2
1.19.1
1.19.0
1.18.1
1.18.0
1.17.0
1.16.0
1.15.0
1.14.0
1.13.1
1.13.0
1.12.0
1.11.0
1.10.0
1.9.0
1.8.0
1.7.0
1.6.0
1.5.0
1.4.0
1.3.0
1.2.0
1.1.0
1.0.0
0.1.0
A dependency-free utility library containing 100+ useful functions.
Current section
Files
Jump to
Current section
Files
lib/retry.ex
defmodule Moar.Retry do
# @related [test](/test/retry_test.exs)
@moduledoc """
Retryable functions.
These functions are particularly useful in higher-level tests, though they can be used for any
kind of waiting.
One use of `rescue_for/3` in a test is to wait until an assertion succeeds:
```elixir
# wait up to 5 seconds for the element with ID "status" to be "finished"
Moar.Retry.rescue_for!(5000, fn ->
assert view |> element("#status") |> render() == "finished"
end)
```
One use of `retry_for/3` in a test is to wait until something changes before making an assertion:
```elixir
Moar.Retry.retry_for(5000, fn ->
view |> element("#status") |> render() == "finished"
end)
assert view |> element("#total") |> render() == "8,675,309"
```
"""
@default_interval 100
@doc """
Run `fun` every `interval_ms` until it doesn't raise an exception.
If `timeout` expires, the exception will be re-raised.
* `timeout` can be an integer in milliseconds or a `Moar.Duration` tuple
```elixir
iex> Moar.Retry.rescue_for!(20, fn -> raise "always fails" end, 2)
** (RuntimeError) always fails
iex> Moar.Retry.rescue_for!({20, :millisecond}, fn -> raise "always fails" end, 2)
** (RuntimeError) always fails
```
"""
@spec rescue_for!(pos_integer() | Moar.Duration.t(), (() -> any()), pos_integer()) :: any() | no_return
def rescue_for!(timeout, fun, interval_ms \\ @default_interval) do
expiry = Moar.DateTime.add(DateTime.utc_now(), duration(timeout))
rescue_until!(expiry, fun, interval_ms)
end
@doc """
Run `fun` every `interval_ms` until it doesn't raise an exception.
If the current time reaches `expiry`, the exception will be re-raised.
```elixir
iex> date_time = DateTime.add(DateTime.utc_now(), 20, :millisecond)
iex> Moar.Retry.rescue_until!(date_time, fn -> raise "always fails" end, 2)
** (RuntimeError) always fails
```
"""
@spec rescue_until!(DateTime.t(), (() -> any()), pos_integer()) :: any() | no_return
def rescue_until!(%DateTime{} = expiry, fun, interval_ms \\ @default_interval) do
fun.()
rescue
e ->
if DateTime.compare(expiry, DateTime.utc_now()) == :gt do
:timer.sleep(interval_ms)
rescue_until!(expiry, fun)
else
reraise e, __STACKTRACE__
end
end
@doc """
Run `fun` every `interval_ms` until it returns a truthy value, returning `{:ok, <value>}`.
If `timeout` expires, returns `{:error, :timeout}`.
* `timeout` can be an integer in milliseconds or a `Moar.Duration` tuple
```elixir
iex> Moar.Retry.retry_for(20, fn -> 10 end, 2)
{:ok, 10}
iex> Moar.Retry.retry_for(20, fn -> false end, 2)
{:error, :timeout}
```
"""
@spec retry_for(pos_integer() | Moar.Duration.t(), fun(), pos_integer()) :: {:ok, any()} | {:error, :timeout}
def retry_for(timeout, fun, interval_ms \\ @default_interval) do
expiry = Moar.DateTime.add(DateTime.utc_now(), duration(timeout))
retry_until(expiry, fun, interval_ms)
end
@doc """
Run `fun` every `interval_ms` until it returns a truthy value, returning `{:ok, <value>}`.
If the current time reaches `expiry`, returns `{:error, :timeout}`.
```elixir
iex> date_time = DateTime.add(DateTime.utc_now(), 20, :millisecond)
iex> Moar.Retry.retry_until(date_time, fn -> false end, 2)
{:error, :timeout}
```
"""
@spec retry_until(DateTime.t(), fun(), pos_integer()) :: {:ok, any()} | {:error, :timeout}
def retry_until(expiry, fun, interval_ms \\ @default_interval) do
cond do
result = fun.() ->
{:ok, result}
DateTime.compare(expiry, DateTime.utc_now()) == :gt ->
:timer.sleep(interval_ms)
retry_until(expiry, fun, interval_ms)
true ->
{:error, :timeout}
end
end
# # #
defp duration({_time, _unit} = duration), do: duration
defp duration(time) when is_integer(time), do: {time, :millisecond}
end