Skip to content

Latest commit

 

History

History
232 lines (174 loc) · 8.61 KB

task_supervisor.livemd

File metadata and controls

232 lines (174 loc) · 8.61 KB

Task Supervisor

Mix.install([
  {:jason, "~> 1.4"},
  {:kino, "~> 0.9", override: true},
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"}
])

Navigation

Review Questions

  • Why might we want to supervise a task?
  • How do we supervise tasks?

Task Supervisor

We can start Task processes under a Task.Supervisor which can dynamically supervise tasks. The supervisor will automatically restart tasks if they encounter an error.

Generally speaking, we should start Task processes under a supervisor.

We encourage developers to rely on supervised tasks as much as possible. Supervised tasks improves the visibility of how many tasks are running at a given moment and enable a huge variety of patterns that gives you explicit control on how to handle the results, errors, and timeouts. Here is a summary:

Using Task.Supervisor.start_child/2 allows you to start a fire-and-forget task that you don't care about its results or if it completes successfully or not.

Using Task.Supervisor.async/2 + Task.await/2 allows you to execute tasks concurrently and retrieve its result. If the task fails, the caller will also fail.

Using Task.Supervisor.async_nolink/2 + Task.yield/2 + Task.shutdown/2 allows you to execute tasks concurrently and retrieve their results or the reason they failed within a given time frame. If the task fails, the caller won't fail. You will receive the error reason either on yield or shutdown.

The Task.Supervisor is started as a child under a normal supervisor. We start the Task.Supervisor as a named process using an atom or a module name.

children = [
  {Task.Supervisor, name: MyTaskSupervisor}
]

{:ok, supervisor_pid} =
  Supervisor.start_link(children, strategy: :one_for_one, name: :parent_supervisor)

We don't need to define a MyTaskSupervisor module. The supervisor uses this name to start a Task.Supervisor process. We can see that MyTaskSupervisor is a child process of our supervisor.

Supervisor.which_children(supervisor_pid)

We can also see that demonstrated in the following diagram.

Kino.Process.sup_tree(supervisor_pid)

Async Tasks

Now we can spawn supervised Task processes under MyTaskSupervisor using Task.Supervisor.async/3.

task =
  Task.Supervisor.async(MyTaskSupervisor, fn ->
    IO.puts("Task Started")
    Process.sleep(1000)
    IO.puts("Task Finished")
    "response!"
  end)

Task.await(task)

We can spawn many tasks under the supervisor.

tasks =
  Enum.map(1..5, fn int ->
    task =
      Task.Supervisor.async(MyTaskSupervisor, fn ->
        IO.puts("Task Started")
        Process.sleep(30000)
        IO.puts("Task Finished")
        int * 2
      end)
  end)

Evaluate the diagram below to see the tasks spawned under the MyTaskSupervisor process.

Kino.Process.sup_tree(supervisor_pid)

We can then await the response from all of the tasks.

Task.await_many(tasks)

Fire-and-Forget Tasks

Task.Supervisor.start_child/2 allows us to start a fire-and-forget task that will perform some work without returning a response.

Task.Supervisor.start_child(MyTaskSupervisor, fn ->
  IO.puts("Fire-and-forget task started")
  Process.sleep(60000)
  IO.puts("Fire-and-forget task finished")
end)

Re-evaluate the cell above a few times, and you'll see several tasks under the MyTaskSupervisor.

children = Supervisor.which_children(MyTaskSupervisor)

We can provide a :restart strategy when we start a process. By default, Task.Supervisor.start_child/2 uses the :temporary :restart strategy. These Task processes will never be restarted.

{:ok, pid} =
  Task.Supervisor.start_child(MyTaskSupervisor, fn ->
    Process.sleep(60000)
  end)

Supervisor.which_children(MyTaskSupervisor)
|> IO.inspect(label: "Started children")

Process.exit(pid, :kill)

Process.sleep(1000)

Supervisor.which_children(MyTaskSupervisor)
|> IO.inspect(label: "Children after exit")

Instead we can use the :permanent process to always restart a Task or :transient to restart a Task if it's exit reason is not :normal, :shutdown, or {:shutdown, reason}.

See Task.Supervisor.start_child/3#options for more.

Now when we kill a Task started with the :transient strategy, notice that a new process with a different pid is started under MyTaskSupervisor.

{:ok, pid} =
  Task.Supervisor.start_child(
    MyTaskSupervisor,
    fn ->
      Process.sleep(60000)
    end,
    restart: :transient
  )

Supervisor.which_children(MyTaskSupervisor)
|> IO.inspect(label: "Started children")

Process.exit(pid, :kill)

Process.sleep(1000)

Supervisor.which_children(MyTaskSupervisor)
|> IO.inspect(label: "Children after exit")

Further Reading

Consider the following resource(s) to deepen your understanding of the topic.

Commit Your Progress

DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.

Run git status to ensure there are no undesirable changes. Then run the following in your command line from the curriculum folder to commit your progress.

$ git add .
$ git commit -m "finish Task Supervisor reading"
$ git push

We're proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.

We also offer a paid course where you can learn from an instructor alongside a cohort of your peers. We will accept applications for the June-August 2023 cohort soon.

Navigation