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
| Time | Space |
|---|---|
| O(n) spawns + O(n) monitor cleanup where n = number of functions | O(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
| Key | Type | Description |
|---|---|---|
system_time | integer | System.system_time() at emission time |
Metadata
| Key | Type | Description |
|---|---|---|
count | integer | Number of functions in the race |
[:resiliency, :race, :run, :stop]
Emitted after the first result is received.
Measurements
| Key | Type | Description |
|---|---|---|
duration | integer | Elapsed native time units (System.monotonic_time/0 delta) |
Metadata
| Key | Type | Description |
|---|---|---|
count | integer | Number 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
| Key | Type | Description |
|---|---|---|
duration | integer | Elapsed native time units |
Metadata
| Key | Type | Description |
|---|---|---|
count | integer | Number of functions in the race |
kind | atom | Exception kind (:error, :exit, or :throw) |
reason | term | The exception or exit reason |
stacktrace | list | Stack at the point of the exception |
Summary
Functions
Run all functions concurrently. Return the first successful result and cancel the rest.
Types
@type task_fun() :: (-> any())
Functions
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}