Packages

Elixir standard libraries instrumented with PULSE.

Current section

Files

Jump to
pulse_libs lib task.ex
Raw

lib/task.ex

defmodule EQC.Pulse.Task do
@compile {:parse_transform, :pulse_instrument}
@compile {:pulse_replace_module, [{Task.Supervised, EQC.Pulse.Task.Supervised}, {:proc_lib, :pulse_proc_lib}]}
@moduledoc """
Conveniences for spawning and awaiting for tasks.
Tasks are processes meant to execute one particular
action throughout their life-cycle, often with little or no
communication with other processes. The most common use case
for tasks is to compute a value asynchronously:
task = Task.async(fn -> do_some_work() end)
res = do_some_other_work()
res + Task.await(task)
Tasks spawned with `async` can be awaited on by its caller
process (and only its caller) as shown in the example above.
They are implemented by spawning a process that sends a message
to the caller once the given computation is performed.
Besides `async/1` and `await/2`, tasks can also be
started as part of supervision trees and dynamically spawned
in remote nodes. We will explore all three scenarios next.
## async and await
The most common way to spawn a task is with `Task.async/1`. A new
process will be created, linked and monitored by the caller. Once
the task action finishes, a message will be sent to the caller
with the result.
`Task.await/2` is used to read the message sent by the task. On
`await`, Elixir will also setup a monitor to verify if the process
exited for any abnormal reason (or in case exits are being
trapped by the caller).
## Supervised tasks
It is also possible to spawn a task inside a supervision tree
with `start_link/1` and `start_link/3`:
Task.start_link(fn -> IO.puts "ok" end)
Such tasks can be mounted in your supervision tree as:
import Supervisor.Spec
children = [
worker(Task, [fn -> IO.puts "ok" end])
]
Since these tasks are supervised and not directly linked to
the caller, they cannot be awaited on. Note `start_link/1`,
unlike `async/1`, returns `{:ok, pid}` (which is
the result expected by supervision trees).
## Supervision trees
The `Task.Supervisor` module allows developers to start supervisors
that dynamically supervise tasks:
{:ok, pid} = Task.Supervisor.start_link()
Task.Supervisor.async(pid, MyMod, :my_fun, [arg1, arg2, arg3])
`Task.Supervisor` also makes it possible to spawn tasks in remote nodes as
long as the supervisor is registered locally or globally:
# In the remote node
Task.Supervisor.start_link(name: :tasks_sup)
# In the client
Task.Supervisor.async({:tasks_sup, :remote@local}, MyMod, :my_fun, [arg1, arg2, arg3])
`Task.Supervisor` is more often started in your supervision tree as:
import Supervisor.Spec
children = [
supervisor(Task.Supervisor, [[name: :tasks_sup]])
]
Note that, when working with distributed tasks, one should use the `async/3` API,
that expects explicit module, function and arguments, instead of `async/1` that
works with anonymous functions. That's because the anonymous function API expects
the same module version to exist on all involved nodes. Check the `Agent` module
documentation for more information on distributed processes, as the limitations
described in the agents documentation apply to the whole ecosystem.
Finally, check `Task.Supervisor` for other operations supported by the Task
supervisor.
"""
@doc """
The Task struct.
It contains two fields:
* `:pid` - the process reference of the task process; it may be a pid
or a tuple containing the process and node names
* `:ref` - the task monitor reference
"""
defstruct pid: nil, ref: nil
@type t :: %__MODULE__{}
@doc """
Starts a task as part of a supervision tree.
"""
@spec start_link(fun) :: {:ok, pid}
def start_link(fun) do
start_link(:erlang, :apply, [fun, []])
end
@doc """
Starts a task as part of a supervision tree.
"""
@spec start_link(module, atom, [term]) :: {:ok, pid}
def start_link(mod, fun, args) do
Task.Supervised.start_link(get_info(self), {mod, fun, args})
end
@doc """
Starts a task.
This is only used when the task is used for side-effects
(i.e. no interest in its return result) and it should not
be linked to the current process.
"""
@spec start(fun) :: {:ok, pid}
def start(fun) do
start(:erlang, :apply, [fun, []])
end
@doc """
Starts a task.
This is only used when the task is used for side-effects
(i.e. no interest in its return result) and it should not
be linked to the current process.
"""
@spec start(module, atom, [term]) :: {:ok, pid}
def start(mod, fun, args) do
Task.Supervised.start(get_info(self), {mod, fun, args})
end
@doc """
Starts a task that can be awaited on.
This function spawns a process that is linked to and monitored
by the caller process. A `Task` struct is returned containing
the relevant information.
## Task's message format
The reply sent by the task will be in the format `{ref, msg}`,
where `ref` is the monitoring reference held by the task.
"""
@spec async(fun) :: t
def async(fun) do
async(:erlang, :apply, [fun, []])
end
@doc """
Starts a task that can be awaited on.
Similar to `async/1`, but the task is specified by the given
module, function and arguments.
"""
@spec async(module, atom, [term]) :: t
def async(mod, fun, args) do
mfa = {mod, fun, args}
pid = :proc_lib.spawn_link(EQC.Pulse.Task.Supervised, :async, [self, get_info(self), mfa])
ref = Process.monitor(pid)
send(pid, {self(), ref})
%Task{pid: pid, ref: ref}
end
defp get_info(self) do
{node(),
case Process.info(self, :registered_name) do
{:registered_name, []} -> self()
{:registered_name, name} -> name
end}
end
@doc """
Awaits a task reply.
A timeout, in milliseconds, can be given with default value
of `5000`. In case the task process dies, this function will
exit with the same reason as the task.
"""
@spec await(t, timeout) :: term | no_return
def await(%Task{ref: ref}=task, timeout \\ 5000) do
receive do
{^ref, reply} ->
Process.demonitor(ref, [:flush])
reply
{:DOWN, ^ref, _, _, :noconnection} ->
mfa = {__MODULE__, :await, [task, timeout]}
exit({{:nodedown, node(task.pid)}, mfa})
{:DOWN, ^ref, _, _, reason} ->
exit({reason, {__MODULE__, :await, [task, timeout]}})
after
timeout ->
Process.demonitor(ref, [:flush])
exit({:timeout, {__MODULE__, :await, [task, timeout]}})
end
end
@doc """
Receives a group of tasks and a message and finds
a task that matches the given message.
This function returns a tuple with the task and the
returned value in case the message matches a task that
exited with success, it raises in case the found task
failed or `nil` if no task was found.
This function is useful in situations where multiple
tasks are spawned and their results are collected
later on. For example, a `GenServer` can spawn tasks,
store the tasks in a list and later use `Task.find/2`
to see if incoming messages are from any of the tasks.
"""
@spec find([t], any) :: {term, t} | nil | no_return
def find(tasks, msg)
def find(tasks, {ref, reply}) when is_reference(ref) do
Enum.find_value tasks, fn
%Task{ref: task_ref} = t when ref == task_ref ->
Process.demonitor(ref, [:flush])
{reply, t}
%Task{} ->
nil
end
end
def find(tasks, {:DOWN, ref, _, _, reason} = msg) when is_reference(ref) do
find = fn(%Task{ref: task_ref}) -> task_ref == ref end
case Enum.find(tasks, find) do
%Task{pid: pid} when reason == :noconnection ->
exit({{:nodedown, node(pid)}, {__MODULE__, :find, [tasks, msg]}})
%Task{} ->
exit({reason, {__MODULE__, :find, [tasks, msg]}})
nil ->
nil
end
end
def find(_tasks, _msg) do
nil
end
end