How do I make a Task.Supervisor wait for its children to finish on shutdown?

I’m sure I’m missing something obvious here – hopefully someone can point out what it is!

I’ve got an application (Phoenix, but I don’t think that’s relevant) that uses a Task.Supervisor to supervise a number of long-running scheduled tasks in the background.

This all works fine, but when I shut the application down the supervisor and its child tasks are killed immediately, whereas I’d like the supervisor to wait for the tasks to complete (or until it reaches the :shutdown value in its child spec) before terminating. My assumption was that if I put the scheduler that triggers the tasks last in the main application’s child list, preceded by the task supervisor, then when shutdown was requested (eg by :init.stop()), no new tasks would start, running ones would complete, then the rest of the application would shutdown.

I’ve reproduced this in a simple (mix new --sup) application:

defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      {Task.Supervisor, name: MyApp.TaskSupervisor}
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
iex(1)> Task.Supervisor.start_child(MyApp.TaskSupervisor, fn ->
...(1)>     Process.sleep(:timer.seconds(5))
...(1)>     IO.puts("Finished!")
...(1)>   end)
{:ok, #PID<0.151.0>}
iex(2)> :init.stop()
:ok
iex(3)>
$

The IEX shell terminates more-or-less instantly on running :init.stop(), and the task never completes.

2 Likes

IIRC correctly, the :shutdown for a Task is :brutal_kill by default. If you want a different behaviour you need to specify a timeout value for the :shutdown.

Supervisors usually have an infinite timeout and wait for all of their children to shut down properly.

When a supervisor shuts down, it sends an exit signal to all of its children. The default is signal is :shutdown and it waits for 5 seconds before a forced termination. However, unless you are trapping exits, your process will terminate immediately once it receives :shutdown. Therefore, to fix this, put Process.flag(:trap_exit, true) at the top of your task.

4 Likes

Fantastic, thank you! I knew it would be something simple – I was looking so hard at the supervisor that it never occurred to me that it was waiting for its children to stop, and the problem was that the tasks themselves were terminating immediately.