Difference between Task.Supervisor.start_child vs Task.start

I had assume that the difference between the two is that Task.start spawns a child process from process where its called while Task.Supervisor.start_child spawns the child process off a supervisor.

Running from iex -S mix. I am expecting for this i will see the child process printout “exiting child process…”

defmodule Foo do
  def foo do
    Task.Supervisor.start_child(
      MyApp.TaskSupervisor,
      fn ->
        Process.sleep(:timer.seconds(10))
        IO.puts("exiting child process...")
      end
    )

    IO.puts("exiting...")
  end
end

And for this i am not expecting to see the output because when foo runs to completion, the child process gets killed with it.

defmodule Foo do
  def foo do
    Task.start(fn ->
      Process.sleep(:timer.seconds(10))
      IO.puts("exiting child process...")
    end)

    IO.puts("exiting...")
  end
end

However i am seeing printout for both cases. What am i understanding wrong here?

start is for spawning an unlinked Task.

Try Task.start_link/1

Ah right. Got it. So i assume my understanding of Task.Supervisor.start_child is correct. The benefit of using this according to the documentation is we can control the shutdown behaviour if we use this instead of Task.start

When i run this using mix run -e Foo.foo the shutdown behaviour is not respected ie. i do not see the child process printout. But if i use iex -S mix i see the expected behaviour.
I thought mix run -e Foo.foo basically does the same thing. Start our app and run the function. Much like what i’m doing in iex. Am i misunderstanding something.

defmodule Foo do
  def foo do
    Task.Supervisor.start_child(
      MyApp.TaskSupervisor,
      fn ->
        Process.flag(:trap_exit, true)

        Process.sleep(:timer.seconds(10))
        IO.puts("exiting child process...")
      end,
      shutdown: 20_000
    )

    IO.puts("exiting...")
  end

I think the confusion results from understanding what a “shutdown” is.

Here’s one line where that confusion resides in the docs of Task.start/1:

If the current node is shutdown, the node will terminate even if the task was not completed

What is “the current node”? What makes the current node shutdown? What qualifies as a shutdown or not? How do you start a shutdown?

When you run iex -S mix you are starting and sustaining a vm node[0] that the Task child process resides in. So the spawned Task will continue to completion even after Foo.foo terminates. Compared to mix run -e Foo.foo which halts the vm node.

If you look at the docs of mix run you’ll see the --no-halt option. That’s because, by default, mix run halts the running system/node, which is different from a shutdown.

More details on what halt does from System.halt:

Terminates the Erlang runtime system without properly shutting down applications and ports. Please see stop/1 for a careful shutdown of the system.

So, I think the confusion here is understanding how a System/node starts and how a System/node stops.
Here’s a thread that may be of interest to you, to simulate a shutdown. In particular:

if you want to politely shutdown your system, you should invoke :init.stop, which will recursively shutdown the supervision tree causing terminate callbacks to be invoked

This is why in the docs it’s recommended to use Task.Supervisor as, I imagine, if you do a shutdown, it won’t take into account any outstanding Task.start processes.

[0] https://elixir-lang.org/getting-started/mix-otp/distributed-tasks.html

2 Likes

Super awesome answer. Thanks for taking time to enlighten me :heart:

1 Like

Would someone know if i can somehow trigger this graceful shutdown when using eval ?
https://hexdocs.pm/mix/1.12/Mix.Tasks.Release.html#module-one-off-commands-eval-and-rpc

Sure, call the System.stop/1 System — Elixir v1.14.3 function.

This is what i am doing but doesn’t seem to work.

defmodule Foo do
  def foo do
    Task.Supervisor.start_child(
      Myapp.TaskSupervisor,
      fn ->
        Process.flag(:trap_exit, true)

        Process.sleep(:timer.seconds(10))
        IO.puts("exiting child process...")
      end,
      shutdown: 20_000
    )

    IO.puts("exiting...")
  end
end

defmodule Adhoc do
  @app :myapp

  @spec run(:atom, :atom, list(any())) :: any()
  def run(module, fun, args) do
    {:ok, _app} = Application.ensure_all_started(@app)
    apply(module, fun, args)
    System.stop()
  end
end

➜ myapp _build/dev/rel/myapp/bin/myapp eval “Adhoc.run(Foo, :foo, [])”
exiting…

Working as expected on iex but not eval. On iex it shutsdown the iex process gracefully. But on eval it seems to shutdown instantly

Hi @laiboonh is that your full example or a truncated example? If that’s your actual code, I think part of the issue is that the release has no idea that your supervisor exists at all. You haven’t defined an app or returned the root supervisor pid as part of any app start callback.

Wow i’m pretty new to this. Is there any documentation that you can point me to regarding “defined an app or returned the root supervisor pid as part of any app start callback.”

This is the full app by the way. I thought things are correct because i tried mix run --no-halt -e "Adhoc.run(Foo, :foo, [])" and it gave me the desired behaviour

Sure, Introduction to Mix - The Elixir programming language walks you through creating an application with mix, defining supervisor children, and so forth. You can adapt the example it provides to instead create a Task.Supervisor child.

Yup thats exactly what i did and eval is not working

Sharing mix.exs

defmodule Myapp.MixProject do
  use Mix.Project

  def project do
    [
      app: :myapp,
      version: "0.1.0",
      elixir: "~> 1.14",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger],
      mod: {Myapp.Application, []}
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
    ]
  end
end

application.ex

defmodule Myapp.Application do
  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Starts a worker by calling: Myapp.Worker.start_link(arg)
      # {Myapp.Worker, arg}
      {Task.Supervisor, name: Myapp.TaskSupervisor}
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Myapp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

1 Like

I would expect it to work given a simple project with minimal code but eval still exits ignoring instructions for graceful shutdown exiting instantly. Would anyone know whats going on?

➜ myapp _build/dev/rel/myapp/bin/myapp eval “Adhoc.run(Foo, :foo, [])”
exiting…

To further strip down the code. I am expecting System.stop to do graceful shudown and in this case show the child process printout

defmodule Foo do
  def foo do
    {:ok, _app} = Application.ensure_all_started(:myapp)

    Task.Supervisor.start_child(
      Myapp.TaskSupervisor,
      fn ->
        Process.flag(:trap_exit, true)

        Process.sleep(:timer.seconds(10))
        IO.puts("exiting child process...")
      end,
      shutdown: 20_000
    )

    IO.puts("exiting...")
    System.stop()
  end
end

➜ myapp _build/dev/rel/myapp/bin/myapp eval “Foo.foo”
exiting…
➜ myapp

Found out how to get this to work.
Similar to how mix run --no-halt -e "Adhoc.run(Foo, :foo, [])" works. We can do no-halt in code

defmodule Foo do
  def foo do
    System.no_halt(true)
    {:ok, _app} = Application.ensure_all_started(:myapp)

    Task.Supervisor.start_child(
      Myapp.TaskSupervisor,
      fn ->
        Process.flag(:trap_exit, true)

        Process.sleep(:timer.seconds(10))
        IO.puts("exiting child process...")
      end,
      shutdown: 20_000
    )

    IO.puts("exiting...")
    System.stop()
  end
end

System.stop is not blocking. So the system then proceeds to halt. If you sleep for infinity after System.stop, then it should shutdown cleanly.

So the reason it works on IEx is because IEx always blocks waiting for input.

There is a lot of great knowledge in this thread. Please feel free to submit pull requests to the relevant parts of the documentation you believe could be improved. :slight_smile:

1 Like

Ah yeah I think the root misunderstanding here has to do with what eval does and doesn’t do. eval only runs for as long as your commands block. If you want to simulate normal release shutdown behavior, you need to actually start your release, and then use rpc to run your function inside the running release.

2 Likes