Current section
Files
Jump to
Current section
Files
lib/liveness.ex
defmodule Liveness do
@moduledoc """
`Liveness` offers the `eventually` higher-order function, which can be used
to specify liveness properties, or to busy-wait for a particular condition.
"""
defexception [:message]
@doc """
Runs `f` repeatedly until `f` succeeds or the number of `tries` is
reached.
Particular runs are separated in time by an interval of `interval`
milliseconds. The interval period begins when the function begins
execution. This means that if execution takes longer than `interval`
milliseconds, the next try will be attempted immediately after `f` returns.
A function is deemed to have failed if it returns `false` or `nil` (a falsy
value), or if it crashes (exits, raises, or `:erlang.error`s out).
If the function returns successfully, its return value becomes the value of
the call to `eventually`.
If the function returns a falsy value (`false` or `nil`) upon the last try,
then the `Liveness` exception is raised.
If the function raises an exception upon the last try, this exception is
re-raised by `eventually` with the *original* stacktrace.
"""
def eventually(f, tries \\ 250, interval \\ 20) do
eventually(f, tries, interval, %RuntimeError{}, nil)
end
defp eventually(_, 0, _, last_exception, last_stacktrace) do
case last_stacktrace do
nil -> raise(last_exception)
stacktrace -> reraise_or_exit(last_exception, stacktrace)
end
end
defp eventually(f, tries, interval, _, _) do
started_at = System.os_time(:millisecond)
try do
case f.() do
x when is_nil(x) or false == x ->
sleep_remaining(started_at, interval)
exception = %__MODULE__{message: "function returned #{inspect(x)}"}
eventually(f, tries - 1, interval, exception, nil)
other ->
other
end
rescue
e in __MODULE__ ->
reraise e, __STACKTRACE__
exception ->
sleep_remaining(started_at, interval)
eventually(f, tries - 1, interval, exception, __STACKTRACE__)
catch
class, reason ->
sleep_remaining(started_at, interval)
eventually(f, tries - 1, interval, {class, reason}, __STACKTRACE__)
end
end
defp sleep_remaining(started_at, interval) do
case interval - (System.os_time(:millisecond) - started_at) do
remaining when remaining > 0 -> Process.sleep(remaining)
_other -> :noop
end
end
defp reraise_or_exit({class, reason}, stacktrace) do
:erlang.raise(class, reason, stacktrace)
end
defp reraise_or_exit(exception, stacktrace) do
reraise(exception, stacktrace)
end
end