Packages
A set of convenience functions for concurrent, asynchronous tasks, loosely inspired by JavaScript's Promises. Pure Elixir, no dependencies.
Current section
Files
Jump to
Current section
Files
lib/taskmaster.ex
defmodule Taskmaster do
@moduledoc """
A set of convenience functions for concurrent, asynchronous tasks, loosely inspired by JavaScript's Promises.
"""
use GenServer
@doc """
Creates a process, that runs `funs` concurrently and when the first one resolves, sends a `{:race_won, result}` message to the caller.
Function resolves either by:
- returning a value, which results in a `{:race_won, value}` message
- crashing or returning a `{:error, reason}` tuple, which results in a `{:race_interrupted, {:error | :exit, reason}}` message
- exceeding a `:timeout` options, which results in a `{:race_interrupted, :timeout}` message
The process created by `race/2` **isn't linked** to the caller process. It terminates after the race is won.
Options
* `:timeout` - a timeout for each function (defaults to 5000)
Example:
iex(1)> Taskmaster.race([
...(1)> fn ->
...(1)> :one
...(1)> end,
...(1)> fn ->
...(1)> :timer.sleep(200)
...(1)> :two
...(1)> end,
...(1)> fn ->
...(1)> :timer.sleep(300)
...(1)> :three
...(1)> end
...(1)> ])
{:ok, #PID<0.178.0>}
iex(2)> flush
{:race_won, :one}
:ok
The process created by `race/2` **isn't linked** to the caller process. It terminates after the race is won,
by one of the functions either returning a value or crashing.
"""
@spec race(funs :: [function(), ...], opts :: [timeout: integer()]) :: {:ok, pid}
def race(funs, opts \\ [])
def race([], _), do: raise(ArgumentError, message: "funs cannot be an empty list")
def race(funs, opts) when is_list(funs) do
GenServer.start(__MODULE__, %{op: {:race, funs, opts}, caller: self(), monitor: nil})
end
def init(%{op: op, caller: caller} = state) do
monitor = Process.monitor(caller)
GenServer.cast(self(), op)
{:ok, %{state | monitor: monitor}}
end
def handle_cast({:race, funs, opts}, %{caller: caller} = state) do
message =
Task.async_stream(
funs,
fn fun ->
try do
fun.()
catch
problem, reason ->
{problem, reason}
end
end,
ordered: false,
max_concurrency: length(funs),
on_timeout: :kill_task,
timeout: opts[:timeout] || 5000
)
|> Stream.take(1)
|> Enum.map(fn
{:ok, {problem, _reason} = res} when problem in [:error, :exit] ->
{:race_interrupted, res}
{:ok, value} ->
{:race_won, value}
{:exit, :timeout} ->
{:race_interrupted, :timeout}
end)
|> List.first()
send(caller, message)
{:stop, :normal, state}
end
def handle_cast(_, state), do: {:noreply, state}
def handle_info({:DOWN, monitor, :process, _, reason}, %{monitor: monitor} = state) do
{:stop, reason, %{state | monitor: nil}}
end
def handle_info(_, state), do: {:noreply, state}
end