Resiliency.FirstOk (Resiliency v0.6.0)

Copy Markdown View Source

Try functions sequentially and return the first successful result.

When to use

  • Implementing a fallback chain across cache, database, and remote API — each layer is tried sequentially, stopping at the first success.
  • Trying multiple parsing strategies or data sources in priority order.

How it works

Functions are tried sequentially (not concurrently). The first function that returns a non-error value wins. Exceptions, exits, throws, and {:error, _} tuples are treated as failures, and the next function is tried. If all fail, {:error, :all_failed} is returned. A total :timeout can be set — elapsed time is subtracted after each attempt.

Algorithm Complexity

TimeSpace
O(n) sequential calls in the worst caseO(1) — only one function executes at a time

Examples

iex> Resiliency.FirstOk.run([fn -> :hello end])
{:ok, :hello}

iex> Resiliency.FirstOk.run([
...>   fn -> {:error, :miss} end,
...>   fn -> {:ok, "found"} end
...> ])
{:ok, "found"}

iex> Resiliency.FirstOk.run([fn -> raise "boom" end])
{:error, :all_failed}

iex> Resiliency.FirstOk.run([])
{:error, :empty}

Typical usage — cache / DB / API fallback:

Resiliency.FirstOk.run([
  fn -> fetch_from_cache(key) end,
  fn -> fetch_from_db(key) end,
  fn -> fetch_from_api(key) end
])

Telemetry

All events are emitted in the caller's process via :telemetry.span/3. See Resiliency.Telemetry for the complete event catalogue.

[:resiliency, :first_ok, :run, :start]

Emitted before any task is attempted.

Measurements

KeyTypeDescription
system_timeintegerSystem.system_time() at emission time

Metadata

KeyTypeDescription
countintegerNumber of functions submitted

[:resiliency, :first_ok, :run, :stop]

Emitted after the first success, or after all functions have been tried.

Measurements

KeyTypeDescription
durationintegerElapsed native time units (System.monotonic_time/0 delta)

Metadata

KeyTypeDescription
countintegerTotal number of functions

| result | :ok | :error | :ok if any function succeeded, :error if all failed | | attempts | integer | Number of functions actually tried before stopping |

[:resiliency, :first_ok, :run, :exception]

Emitted if run/2 raises or exits unexpectedly.

Measurements

KeyTypeDescription
durationintegerElapsed native time units

Metadata

KeyTypeDescription
countintegerNumber of functions submitted
kindatomException kind (:error, :exit, or :throw)
reasontermThe exception or exit reason
stacktracelistStack at the point of the exception

Summary

Functions

Try functions sequentially. Return the first successful result.

Types

task_fun()

@type task_fun() :: (-> any())

Functions

run(funs, opts \\ [])

@spec run(
  [task_fun()],
  keyword()
) :: {:ok, any()} | {:error, :all_failed | :empty}

Try functions sequentially. Return the first successful result.

Functions are tried one at a time, in order. A function "succeeds" if it returns any value other than {:error, _} without raising, exiting, or throwing. A function "fails" if it raises, exits, throws, or returns {:error, _}.

Successful results are wrapped in {:ok, result}. If the function already returns {:ok, value}, it is passed through unchanged. Functions after the first success are never called.

Returns {:error, :all_failed} if all functions fail. Returns {:error, :empty} for an empty list.

Parameters

  • funs -- a list of zero-arity functions to try sequentially.
  • opts -- keyword list of options. Defaults to [].
    • :timeout -- total timeout across all attempts, in milliseconds or :infinity. Defaults to :infinity.

Returns

{:ok, result} from the first function that succeeds, {:error, :all_failed} if all functions fail or the timeout expires, or {:error, :empty} if the input list is empty.

Examples

iex> Resiliency.FirstOk.run([fn -> :hello end])
{:ok, :hello}

iex> Resiliency.FirstOk.run([
...>   fn -> {:error, :miss} end,
...>   fn -> {:ok, "found"} end
...> ])
{:ok, "found"}

iex> Resiliency.FirstOk.run([fn -> raise "boom" end])
{:error, :all_failed}

iex> Resiliency.FirstOk.run([])
{:error, :empty}