There are many instances where we will have multiple things to do that aren’t
dependent on each other. When we run into these cases, we would like to start
separate processes that are not linked to the caller. This allows the caller to
continue running if the new process crashes. To do this, we use
Task.Supervisor.
In lib/hello.ex
, where our app is started, we can see that we have
HelloWeb.Endpoint
as a supervisor, which is handling our web requests. If
we want to hand off async tasks to from our HelloWeb.Endpoint
supervisor
to a Task.Supervisor
, we need to start one here. Inside of the children
list, add:
supervisor(Task.Supervisor, [[name: Hello.TaskSupervisor]])
Which gives us:
defmodule Hello do
use Application
# See https://hexdocs.pm/elixir/1.5.1/Application.html
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
# Start the endpoint when the application starts
supervisor(HelloWeb.Endpoint, []),
supervisor(Task.Supervisor, [[name: Hello.TaskSupervisor]]),
# Start the Ecto repository
worker(Hello.Repo, []),
# Here you could define other workers and supervisors as children
# worker(Hello.Worker, [arg1, arg2, arg3]),
]
# See https://hexdocs.pm/elixir/1.5.1/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Hello.Supervisor]
Supervisor.start_link(children, opts)
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
def config_change(changed, _new, removed) do
HelloWeb.Endpoint.config_change(changed, removed)
:ok
end
end
Now we have a Task.Supervisor
that we can refer to as
Hello.TaskSupervisor
, waiting to receive any tasks that we’d like to
offload from another process. Let’s see what this does for us.
Often we’d like to “fire and forget” certain types of work. A common example here would be to send an email to an end user in a controller, then sending some sort of notification or redirect in the response without waiting for the email to send. Our email needs to be handled in an async task so we don’t block our calling process (where the controller response will be sent). Additionally, unlike some async tasks, we don’t care about the result of the task, so we don’t need to await the result. We like to say we can “fire and forget” these types of tasks.
Since the result of the new async process and the calling process are
independent of each other, we need to make sure if something goes wrong in
our async task, it doesn’t crash the calling process. To do this, we need a
separate supervisor that can supervise our async process. We can use the
Hello.TaskSupervisor
that we created earlier for this, as we will see
below.
Let’s prove that we can send a task to our supervisor that is completely independent of the calling process, allowing the caller to continue running if the task blows up.
In the new processes, we will crash it with 1/0
(which will raise
(ArithmeticError) bad argument in arithmetic expression), to make sure our
request still finishes despite the error. We will also sleep for 2 seconds, so
we can see that our calling process is not blocked by the async task runs.
defmodule Hello.PageController do
use Hello.Web, :controller
def index(conn, _params) do
Task.Supervisor.async_nolink(Hello.TaskSupervisor, fn ->
:timer.sleep(2000)
1 / 0
end)
render conn, "index.html"
end
end
async_nolink/2
accepts the name of a supervisor as the first argument, we
passed in the name of the supervisor we specified in lib/hello.ex
.
The next argument is an anonymous function that will become a task supervised
by the passed in supervisor. As the name suggested, this task will not be
linked to the calling process, allowing our request to finish when our task
fails.
When we spin up our server and visit our "/"
route at localhost:4000
, the
request finishes and responds as normal - followed by an arithmetic error after
a few seconds. Since the tasks aren’t linked, we can execute any task using the
other supervisor, and our controller is able to send a response regardless of
the error.
It’s worth noting that if these tasks fail, they won’t be retried. Consider building a solution using GenStage