Skip to content

Commit 3e54d33

Browse files
author
José Valim
committed
Do not catch failures on tasks
Closes #2345
1 parent 0fe1e68 commit 3e54d33

File tree

5 files changed

+40
-52
lines changed

5 files changed

+40
-52
lines changed

lib/elixir/lib/task.ex

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ defmodule Task do
22
@moduledoc """
33
Conveniences for spawning and awaiting for tasks.
44
5-
Tasks are processes that meant to execute one particular
6-
action throughout their life-cycle, often with little
7-
explicit communication with other processes. The most common
8-
use case for tasks is to compute a value asynchronously:
5+
Tasks are processes meant to execute one particular
6+
action throughout their life-cycle, often with little or no
7+
communication with other processes. The most common use case
8+
for tasks is to compute a value asynchronously:
99
1010
task = Task.async(fn -> do_some_work() end)
1111
res = do_some_other_work()
@@ -16,28 +16,21 @@ defmodule Task do
1616
They are implemented by spawning a process that sends a message
1717
to the caller once the given computation is performed.
1818
19-
Besides `async/1` and `await/1`, tasks can also be used as part
20-
of supervision trees and dynamically spawned in remote nodes.
21-
We will explore all three scenarios next.
19+
Besides `async/1` and `await/1`, tasks can also be used be
20+
started as part of supervision trees and dynamically spawned
21+
in remote nodes. We will explore all three scenarios next.
2222
2323
## async and await
2424
2525
The most common way to spawn a task is with `Task.async/1`. A new
26-
process will be created and this process is linked and monitored
27-
by the caller. However, the processes are unlinked right before
28-
the task finishes, allowing the proper error to be triggered only
29-
on `await/1`.
26+
process will be created, linked and monitored by the caller. Once
27+
the task action finishes, a message will be sent to the caller
28+
with its result.
3029
31-
This implies three things:
32-
33-
1) In case the caller crashes, the task will be killed and its
34-
computation will abort;
35-
36-
2) In case the task crashes due to an error, the parent will
37-
crash only on `await/1`;
38-
39-
3) In case the task crashes because a linked process caused
40-
it to crash, the parent will crash immediately;
30+
`Task.await/1` is used to read the message sent by the task. On
31+
await, Elixir will also setup a monitor to verify if the process
32+
exited with any abnormal reason (or in case exits are being
33+
trapped by the caller).
4134
4235
## Supervised tasks
4336
@@ -55,14 +48,9 @@ defmodule Task do
5548
]
5649
5750
Since such tasks are supervised and not directly linked to
58-
the caller, they cannot be awaited on. For such reason,
59-
differently from `async/1`, `start_link/1` returns `{:ok, pid}`
60-
(which is the result expected by supervision trees).
61-
62-
Such tasks are useful as workers that run during your application
63-
life-cycle and rarely communicate with other workers. For example,
64-
a worker that pushes data to another server or a worker that consumes
65-
events from an event manager and writes it to a log file.
51+
the caller, they cannot be awaited on. Note `start_link/1`,
52+
differently from `async/1`, returns `{:ok, pid}` (which is
53+
the result expected by supervision trees).
6654
6755
## Supervision trees
6856
@@ -78,7 +66,7 @@ defmodule Task do
7866
# In the remote node
7967
Task.Supervisor.start_link(name: :tasks_sup)
8068
81-
# On the client
69+
# In the client
8270
Task.Supervisor.async({:tasks_sup, :remote@local}, fn -> do_work() end)
8371
8472
`Task.Supervisor` is more often started in your supervision tree as:
@@ -118,7 +106,7 @@ defmodule Task do
118106
"""
119107
@spec start_link(module, atom, [term]) :: {:ok, pid}
120108
def start_link(mod, fun, args) do
121-
Task.Supervised.start_link(:undefined, {mod, fun, args})
109+
Task.Supervised.start_link({mod, fun, args})
122110
end
123111

124112
@doc """

lib/elixir/lib/task/supervised.ex

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
defmodule Task.Supervised do
22
@moduledoc false
33

4-
def start_link(:undefined, fun) do
4+
def start_link(fun) do
55
:proc_lib.start_link(__MODULE__, :noreply, [fun])
66
end
77

88
def start_link(caller, fun) do
99
:proc_lib.start_link(__MODULE__, :reply, [caller, fun])
1010
end
1111

12-
def async(caller, {module, fun, args}) do
12+
def async(caller, mfa) do
13+
ref = receive do: ({^caller, ref} -> ref)
14+
send caller, {ref, apply(mfa)}
15+
end
16+
17+
def reply(caller, mfa) do
18+
:erlang.link(caller)
19+
:proc_lib.init_ack({:ok, self()})
20+
1321
ref =
1422
# There is a race condition on this operation when working accross
1523
# node that manifests if a `Task.Supervisor.async/1` call is made
@@ -33,29 +41,15 @@ defmodule Task.Supervised do
3341
5000 -> exit(:timeout)
3442
end
3543

36-
try do
37-
apply(module, fun, args)
38-
else
39-
result ->
40-
send caller, {ref, result}
41-
catch
42-
:error, reason ->
43-
exit({reason, System.stacktrace()})
44-
:throw, value ->
45-
exit({{:nocatch, value}, System.stacktrace()})
46-
after
47-
:erlang.unlink(caller)
48-
end
44+
send caller, {ref, apply(mfa)}
4945
end
5046

51-
def reply(caller, mfa) do
52-
:erlang.link(caller)
47+
def noreply(mfa) do
5348
:proc_lib.init_ack({:ok, self()})
54-
async(caller, mfa)
49+
apply(mfa)
5550
end
5651

57-
def noreply({module, fun, args}) do
58-
:proc_lib.init_ack({:ok, self()})
52+
def apply({module, fun, args}) do
5953
try do
6054
apply(module, fun, args)
6155
catch

lib/elixir/lib/task/supervisor.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,6 @@ defmodule Task.Supervisor do
9898
"""
9999
@spec start_child(Supervisor.supervisor, module, atom, [term]) :: {:ok, pid}
100100
def start_child(supervisor, module, fun, args) do
101-
Supervisor.start_child(supervisor, [:undefined, {module, fun, args}])
101+
Supervisor.start_child(supervisor, [{module, fun, args}])
102102
end
103103
end

lib/elixir/test/elixir/task/supervisor_test.exs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,21 @@ defmodule Task.SupervisorTest do
8585
end
8686

8787
test "await/1 exits on task throw", config do
88+
Process.flag(:trap_exit, true)
8889
task = Task.Supervisor.async(config[:supervisor], fn -> throw :unknown end)
8990
assert {{{:nocatch, :unknown}, _}, {Task, :await, [^task, 5000]}} =
9091
catch_exit(Task.await(task))
9192
end
9293

9394
test "await/1 exits on task error", config do
95+
Process.flag(:trap_exit, true)
9496
task = Task.Supervisor.async(config[:supervisor], fn -> raise "oops" end)
9597
assert {{%RuntimeError{}, _}, {Task, :await, [^task, 5000]}} =
9698
catch_exit(Task.await(task))
9799
end
98100

99101
test "await/1 exits on task exit", config do
102+
Process.flag(:trap_exit, true)
100103
task = Task.Supervisor.async(config[:supervisor], fn -> exit :unknown end)
101104
assert {:unknown, {Task, :await, [^task, 5000]}} =
102105
catch_exit(Task.await(task))

lib/elixir/test/elixir/task_test.exs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,21 @@ defmodule TaskTest do
7070
end
7171

7272
test "await/1 exits on task throw" do
73+
Process.flag(:trap_exit, true)
7374
task = Task.async(fn -> throw :unknown end)
7475
assert {{{:nocatch, :unknown}, _}, {Task, :await, [^task, 5000]}} =
7576
catch_exit(Task.await(task))
7677
end
7778

7879
test "await/1 exits on task error" do
80+
Process.flag(:trap_exit, true)
7981
task = Task.async(fn -> raise "oops" end)
8082
assert {{%RuntimeError{}, _}, {Task, :await, [^task, 5000]}} =
8183
catch_exit(Task.await(task))
8284
end
8385

8486
test "await/1 exits on task exit" do
87+
Process.flag(:trap_exit, true)
8588
task = Task.async(fn -> exit :unknown end)
8689
assert {:unknown, {Task, :await, [^task, 5000]}} =
8790
catch_exit(Task.await(task))

0 commit comments

Comments
 (0)