Resiliency.Race (Resiliency v0.6.0)

Copy Markdown View Source

Run all functions concurrently and return the first successful result.

When to use

  • Querying multiple replicas or services in parallel and using whichever responds first — e.g., hitting a primary and a read-replica simultaneously for a latency-sensitive endpoint.
  • Sending the same request to multiple regions or providers and taking the fastest response.

How it works

All functions are spawned concurrently as monitored processes. The caller enters a receive loop: the first task to send a successful result wins, all remaining tasks are killed via Process.exit(pid, :kill), and {:ok, result} is returned. If a task crashes (:DOWN message), it is removed from the active set and the race continues with the survivors. If all tasks crash, {:error, :all_failed} is returned.

Algorithm Complexity

TimeSpace
O(n) spawns + O(n) monitor cleanup where n = number of functionsO(n) — one monitored process per function

Examples

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

iex> Resiliency.Race.run([
...>   fn -> Process.sleep(100); :slow end,
...>   fn -> :fast end
...> ])
{:ok, :fast}

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

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

Crashed tasks are skipped — the race continues:

iex> Resiliency.Race.run([
...>   fn -> raise "primary down" end,
...>   fn -> :backup end
...> ])
{:ok, :backup}

With a timeout:

Resiliency.Race.run([
  fn -> fetch_from_slow_service() end,
  fn -> fetch_from_another_service() end
], timeout: 5_000)

Telemetry

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

[:resiliency, :race, :run, :start]

Emitted before tasks are spawned.

Measurements

KeyTypeDescription
system_timeintegerSystem.system_time() at emission time

Metadata

KeyTypeDescription
countintegerNumber of functions in the race

[:resiliency, :race, :run, :stop]

Emitted after the first result is received.

Measurements

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

Metadata

KeyTypeDescription
countintegerNumber of functions in the race

| result | :ok | :error | Outcome of the winning result |

[:resiliency, :race, :run, :exception]

Emitted if run/2 raises or exits (e.g., an unexpected error in the collection logic).

Measurements

KeyTypeDescription
durationintegerElapsed native time units

Metadata

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

Summary

Functions

Run all functions concurrently. Return the first successful result and cancel the rest.

Types

task_fun()

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

Functions

run(funs, opts \\ [])

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

Run all functions concurrently. Return the first successful result and cancel the rest.

Spawns all functions as concurrent tasks. The first task to complete successfully wins — its result is returned and all remaining tasks are shut down. If a task crashes (raise, exit, or throw), it is skipped and the race continues with the remaining tasks.

Returns {:ok, result} from the first function that completes successfully. If all functions fail, returns {:error, :all_failed}. If no function succeeds within the timeout, returns {:error, :timeout}. An empty list returns {:error, :empty}.

Parameters

  • funs -- a list of zero-arity functions to race concurrently.
  • opts -- keyword list of options. Defaults to [].
    • :timeout -- milliseconds or :infinity. Defaults to :infinity.

Returns

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

Examples

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

iex> Resiliency.Race.run([
...>   fn -> Process.sleep(100); :slow end,
...>   fn -> :fast end
...> ])
{:ok, :fast}

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

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