Can or should Task.async() be used in a controller#action?

Viewed 140

Because Task.async() is linked to the current process, does this mean that if I use it in an action of a controller, it'll get killed automatically, or will die, once an action has been executed?

defmodule MyAppWeb.PageController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    
    # ????
    Task.async(fn -> some_10_second_long_stuff() end)
    Task.async(fn -> another_10_second_long_stuff() end)
    # will they both get killed or die?

    render(conn, "index.html")
  end

If so, what should be used instead? Task.start() ?


Those 2 some_10_second_long_stuff functions are "send emails"

2 Answers

The sole purpose of Task.async/1 would be to create a task that can easily be awaited.

Unless blown up, or killed (read: if it returns normally,) it just silently dies and surely it won’t do anything with the calling process. If the task dies abnormally, it would also result in a crash of the calling process.

It is all well described in the docs I linked above.

That said, if you want to spawn a task for side effects only, use Task.start/1. If you want the task to be monitored, and restarted, if crashed, use Task.start_link/1. If you need the task to be awaited by the controller process, use Task.async/1.

When a process terminates, it will terminate with an exit reason. This exit reason is emitted in an exit signal to all linked processes.

The default behavior when a process receives an exit signal with an exit reason other than normal, is to terminate and in turn emit exit signals with the same exit reason to its linked processes. An exit signal with reason normal is ignored.

So, just because a process ends, does not necessarily mean that its linked processes will terminate. Rather, a process has to crash to cause its linked processes to crash. Even then, a linked process can "trap exits" to prevent it from crashing.

In the following example, a process starts a new process and links to it, then the original process ends because it has nothing more to do, i.e. the function has no more statements to execute, yet the linked process keeps executing:

defmodule A do

  def start do
    spawn(A, :do_stuff, []) 
  end

  def do_stuff do    #This is the "do_stuff() process"...
    spawn_link(A, :ticker, [10])   #...which spawns a second process, the "ticker()" process, and links to it.
    IO.puts "exiting do_stuff()"   #The "do_stuff()" process ends.  Does the "ticker()" process also end now?
  end

  def ticker(0), do: IO.puts "exiting ticker()" 
  def ticker(num_ticks) do
    :timer.sleep(1000)  #sleep for 1 second
    IO.write "."
    ticker(num_ticks - 1)
  end

end

In iex:

iex(1)> A.start
exiting do_stuff
:ok
..........exiting ticker()
iex(2)> 

The output shows that the ticker() process continued executing for 10 seconds after the do_stuff() process terminated--see the ten dots?

Compare the output above to the output produced when you change do_stuff() to this:

 def do_stuff do
    spawn_link(A, :ticker, [10]) 

    :timer.sleep(2000)
    raise "I'm crashing"

    IO.puts "exiting do_stuff() normally"
  end

In iex:

iex(1)> A.start
:ok

..iex(2)> 
23:25:25.772 [error] Process #PID<0.116.0> raised an exception
** (RuntimeError) I'm crashing
    a.ex:20: A.do_stuff/0

This time there are only two dots.

========= Response to comment:

Here is an example that calls Task.async to run ticker():

defmodule A do

  def start do
    spawn(A, :do_stuff, [])
    :ok
  end

  def do_stuff do
    Task.async(A, :ticker, [10])

    #spawn_link(A, :ticker, [10])  #links the process executing do_stuff() and
    #:timer.sleep(2000)
    #raise "I'm crashing"

    IO.puts "exiting do_stuff() normally"
  end

  def ticker(0), do: IO.puts "exiting ticker()"
  def ticker(num_ticks) do
    :timer.sleep(1000)  #sleep for 1 second
    IO.write "."
    ticker(num_ticks - 1)
  end

end

In iex:

iex(1)> A.start
exiting do_stuff() normally
:ok
..........exiting ticker()

Notice the 10 dots, which means the "ticker() process" executed for 10 seconds after the "do_stuff() process" exited normally.

The Task docs say that you must call Task.await() if you call Task.async(), but there doesn't seem to be any penalty for not calling await(). I guess you get a message in the calling process's mailbox when Task.async() finishes executing, and if you launch a trillion Tasks, then you can overflow the mailbox and cause the process to crash.

Related