Skip to content

Instantly share code, notes, and snippets.

@moklett
Last active May 7, 2024 09:59
Show Gist options
  • Star 43 You must be signed in to star a gist
  • Fork 13 You must be signed in to fork a gist
  • Save moklett/d30fc2dbaf71f3b978da115f8a5f8387 to your computer and use it in GitHub Desktop.
Save moklett/d30fc2dbaf71f3b978da115f8a5f8387 to your computer and use it in GitHub Desktop.
Elixir Task - Crash Handling
# This demonstrates that, when using async/await, a crash in the task will crash the caller
defmodule Tasker do
def good(message) do
IO.puts message
end
def bad(message) do
IO.puts message
raise "I'm BAD!"
end
end
IO.puts "Starting Async GOOD"
a = Task.async(Tasker, :good, ["-> Async GOOD"])
IO.puts "Starting Async BAD"
b = Task.async(Tasker, :bad, ["-> Async BAD"])
:timer.sleep 500
IO.puts "Awaiting both"
Task.await(a)
Task.await(b)
# % elixir task1.exs
# Starting Async GOOD
# Starting Async BAD
# -> Async GOOD
# -> Async BAD
# ** (EXIT from #PID<0.47.0>) an exception was raised:
# ** (RuntimeError) I'm BAD!
# task1.exs:8: Tasker.bad/1
# (elixir) lib/task/supervised.ex:89: Task.Supervised.do_apply/2
# (elixir) lib/task/supervised.ex:40: Task.Supervised.reply/5
# (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
#
# 09:06:08.269 [error] Task #PID<0.54.0> started from #PID<0.47.0> terminating
# ** (RuntimeError) I'm BAD!
# task1.exs:8: Tasker.bad/1
# (elixir) lib/task/supervised.ex:89: Task.Supervised.do_apply/2
# (elixir) lib/task/supervised.ex:40: Task.Supervised.reply/5
# (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
# Function: &Tasker.bad/1
# Args: ["-> Async BAD"]
# This demonstrates that, when using async/yield, a crash in the task will crash the caller.
# i.e. `async` always links the processes! Note that "I'm still alive" is not printed
defmodule Tasker do
def good(message) do
IO.puts message
end
def bad(message) do
IO.puts message
raise "I'm BAD!"
end
end
IO.puts "Starting Async GOOD"
a = Task.async(Tasker, :good, ["-> Async GOOD"])
IO.puts "Starting Async BAD"
b = Task.async(Tasker, :bad, ["-> Async BAD"])
:timer.sleep 500
IO.puts "Awaiting both"
Task.yield(a)
Task.yield(b)
IO.puts "I'm still alive"
# % elixir task2.exs
# Starting Async GOOD
# Starting Async BAD
# -> Async GOOD
# -> Async BAD
# ** (EXIT from #PID<0.47.0>) an exception was raised:
# ** (RuntimeError) I'm BAD!
# task2.exs:9: Tasker.bad/1
# (elixir) lib/task/supervised.ex:89: Task.Supervised.do_apply/2
# (elixir) lib/task/supervised.ex:40: Task.Supervised.reply/5
# (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
#
# 09:08:23.912 [error] Task #PID<0.54.0> started from #PID<0.47.0> terminating
# ** (RuntimeError) I'm BAD!
# task2.exs:9: Tasker.bad/1
# (elixir) lib/task/supervised.ex:89: Task.Supervised.do_apply/2
# (elixir) lib/task/supervised.ex:40: Task.Supervised.reply/5
# (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
# Function: &Tasker.bad/1
# Args: ["-> Async BAD"]
#
# This demonstrates that, when using start_link, the caller will not wait around
# on the task to finish - the caller will finish regardless of what happens in the
# task.
defmodule Tasker do
def good(message) do
:timer.sleep 500
IO.puts message
end
def bad(message) do
:timer.sleep 500
IO.puts message
raise "I'm BAD!"
IO.puts "After CRASH"
end
end
IO.puts "Starting start_link GOOD"
{:ok, pid1} = Task.start_link(Tasker, :good, ["-> Start Link GOOD"])
IO.puts "Starting start_link BAD"
{:ok, pid2} = Task.start_link(Tasker, :bad, ["-> Start Link BAD"])
IO.puts "PIDS:"
IO.inspect pid1
IO.inspect pid2
# There is no way to wait!
# % elixir task3.exs
# Starting start_link GOOD
# Starting start_link BAD
# PIDS:
# #PID<0.53.0>
# #PID<0.54.0>
# This demonstrates that, when using start_link, the task can crash the caller
# if the caller is still running.
defmodule Tasker do
def good(message) do
IO.puts message
end
def bad(message) do
IO.puts message
raise "I'm BAD!"
IO.puts "After CRASH"
end
end
IO.puts "Starting start_link GOOD"
{:ok, pid1} = Task.start_link(Tasker, :good, ["-> Start Link GOOD"])
IO.puts "Starting start_link BAD"
{:ok, pid2} = Task.start_link(Tasker, :bad, ["-> Start Link BAD"])
:timer.sleep(1000)
IO.puts "PIDS:"
IO.inspect pid1
IO.inspect pid2
# % elixir task4.exs
# Starting start_link GOOD
# Starting start_link BAD
# -> Start Link GOOD
# -> Start Link BAD
# ** (EXIT from #PID<0.47.0>) an exception was raised:
# ** (RuntimeError) I'm BAD!
# task4.exs:10: Tasker.bad/1
# (elixir) lib/task/supervised.ex:89: Task.Supervised.do_apply/2
# (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
#
# 09:19:30.200 [error] Task #PID<0.54.0> started from #PID<0.47.0> terminating
# ** (RuntimeError) I'm BAD!
# task4.exs:10: Tasker.bad/1
# (elixir) lib/task/supervised.ex:89: Task.Supervised.do_apply/2
# (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
# Function: &Tasker.bad/1
# Args: ["-> Start Link BAD"]
# This demonstrates that, when using start, the task does not crash the caller
# (but the exception status is printed to the screen)
defmodule Tasker do
def good(message) do
IO.puts message
end
def bad(message) do
IO.puts message
raise "I'm BAD!"
IO.puts "After CRASH"
end
end
IO.puts "Starting start GOOD"
{:ok, pid1} = Task.start(Tasker, :good, ["-> Start GOOD"])
IO.puts "Starting start BAD"
{:ok, pid2} = Task.start(Tasker, :bad, ["-> Start BAD"])
:timer.sleep(1000)
IO.puts "PIDS:"
IO.inspect pid1
IO.inspect pid2
# % elixir task5.exs
# Starting start GOOD
# Starting start BAD
# -> Start GOOD
# -> Start BAD
#
# 09:21:51.944 [error] Task #PID<0.54.0> started from #PID<0.47.0> terminating
# ** (RuntimeError) I'm BAD!
# task5.exs:9: Tasker.bad/1
# (elixir) lib/task/supervised.ex:89: Task.Supervised.do_apply/2
# (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
# Function: &Tasker.bad/1
# Args: ["-> Start BAD"]
# PIDS:
# #PID<0.53.0>
# #PID<0.54.0>
# This demonstrates that, when using start, the task does not crash the caller,
# but the tasks do not live past the life of the caller. I've switched from
# `IO.puts` in the tasks to `File.write` to show that the side-effect never
# happens.
defmodule Tasker do
def good(message) do
:timer.sleep(1000)
File.write("./task_good.txt", "#{message} - #{inspect(make_ref)}")
end
def bad(message) do
:timer.sleep(1000)
File.write("./task_bad.txt", "#{message} - #{inspect(make_ref)}")
raise "I'm BAD!"
end
end
IO.puts "Starting start GOOD"
{:ok, pid1} = Task.start(Tasker, :good, ["-> Start GOOD"])
IO.puts "Starting start BAD"
{:ok, pid2} = Task.start(Tasker, :bad, ["-> Start BAD"])
IO.puts "PIDS:"
IO.inspect pid1
IO.inspect pid2
# % elixir task6.exs
# Starting start GOOD
# Starting start BAD
# PIDS:
# #PID<0.53.0>
# #PID<0.54.0>
# % ls task_*.txt
# ls: task_*.txt: No such file or directory
# This demonstrates that, when using async_nolink, the task only crashes
# the caller when we call await. Note that "I'm still alive" is not
# printed
defmodule Tasker do
def good(message) do
:timer.sleep(1000)
IO.puts message
end
def bad(message) do
:timer.sleep(1000)
IO.puts message
raise "I'm BAD!"
end
end
{:ok, pid} = Task.Supervisor.start_link()
IO.puts "Starting async_nolink GOOD"
a = Task.Supervisor.async_nolink(pid, Tasker, :good, ["-> Start GOOD"])
IO.puts "Starting async_nolink BAD"
b = Task.Supervisor.async_nolink(pid, Tasker, :bad, ["-> Start BAD"])
IO.puts "Awaiting both"
Task.await(a)
Task.await(b)
IO.puts "I'm still alive"
# % elixir task7.exs
# Starting async_nolink GOOD
# Starting async_nolink BAD
# Awaiting both
# -> Start GOOD
# -> Start BAD
#
# 09:55:05.130 [error] Task #PID<0.55.0> started from #PID<0.47.0> terminating
# ** (RuntimeError) I'm BAD!
# task7.exs:13: Tasker.bad/1
# (elixir) lib/task/supervised.ex:89: Task.Supervised.do_apply/2
# (elixir) lib/task/supervised.ex:40: Task.Supervised.reply/5
# (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
# Function: &Tasker.bad/1
# Args: ["-> Start BAD"]
# ** (exit) exited in: Task.await(%Task{owner: #PID<0.47.0>, pid: #PID<0.55.0>, ref: #Reference<0.0.1.14>}, 5000)
# ** (EXIT) an exception was raised:
# ** (RuntimeError) I'm BAD!
# task7.exs:13: Tasker.bad/1
# (elixir) lib/task/supervised.ex:89: Task.Supervised.do_apply/2
# (elixir) lib/task/supervised.ex:40: Task.Supervised.reply/5
# (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
# (elixir) lib/task.ex:332: Task.await/2
# task7.exs:28: (file)
# (elixir) lib/code.ex:363: Code.require_file/2
#
#
# 09:55:05.157 [error] GenServer #PID<0.53.0> terminating
# ** (stop) exited in: Task.await(%Task{owner: #PID<0.47.0>, pid: #PID<0.55.0>, ref: #Reference<0.0.1.14>}, 5000)
# ** (EXIT) an exception was raised:
# ** (RuntimeError) I'm BAD!
# task7.exs:13: Tasker.bad/1
# (elixir) lib/task/supervised.ex:89: Task.Supervised.do_apply/2
# (elixir) lib/task/supervised.ex:40: Task.Supervised.reply/5
# (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
# Last message: {:EXIT, #PID<0.47.0>, {{%RuntimeError{message: "I'm BAD!"}, [{Tasker, :bad, 1, [file: 'task7.exs', line: 13]}, {Task.Supervised, :do_apply, 2, [file: 'lib/task/supervised.ex', line: 89]}, {Task.Supervised, :reply, 5, [file: 'lib/task/supervised.ex', line: 40]}, {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 240]}]}, {Task, :await, [%Task{owner: #PID<0.47.0>, pid: #PID<0.55.0>, ref: #Reference<0.0.1.14>}, 5000]}}}
# State: {:state, {#PID<0.53.0>, Supervisor.Default}, :simple_one_for_one, [{:child, :undefined, Task.Supervised, {Task.Supervised, :start_link, []}, :temporary, 5000, :worker, [Task.Supervised]}], {:set, 0, 16, 16, 8, 80, 48, {[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []}, {{[], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []}}}, 3, 5, [], Supervisor.Default, {:ok, {{:simple_one_for_one, 3, 5}, [{Task.Supervised, {Task.Supervised, :start_link, []}, :temporary, 5000, :worker, [Task.Supervised]}]}}}
# This demonstrates that, when using async_nolink, the task does not crash
# if we use `yield` to capture results
# Note that "I'm still alive" IS printed, and we get results from the tasks
defmodule Tasker do
def good(message) do
:timer.sleep(1000)
IO.puts message
end
def bad(message) do
:timer.sleep(1000)
IO.puts message
raise "I'm BAD!"
end
end
{:ok, pid} = Task.Supervisor.start_link()
IO.puts "Starting async_nolink GOOD"
a = Task.Supervisor.async_nolink(pid, Tasker, :good, ["-> Start GOOD"])
IO.puts "Starting async_nolink BAD"
b = Task.Supervisor.async_nolink(pid, Tasker, :bad, ["-> Start BAD"])
IO.puts "Awaiting both"
res_a = Task.yield(a)
res_b = Task.yield(b)
IO.puts "I'm still alive"
IO.puts "Result A: #{inspect(res_a)}"
IO.puts "Result B: #{inspect(res_b)}"
# % elixir task8.exs
# Starting async_nolink GOOD
# Starting async_nolink BAD
# Awaiting both
# -> Start GOOD
# -> Start BAD
# I'm still alive
# Result A: {:ok, :ok}
#
# 09:57:48.659 [error] Task #PID<0.55.0> started from #PID<0.47.0> terminating
# ** (RuntimeError) I'm BAD!
# task8.exs:13: Tasker.bad/1
# (elixir) lib/task/supervised.ex:89: Task.Supervised.do_apply/2
# (elixir) lib/task/supervised.ex:40: Task.Supervised.reply/5
# (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
# Function: &Tasker.bad/1
# Args: ["-> Start BAD"]
# Result B: {:exit, {%RuntimeError{message: "I'm BAD!"}, [{Tasker, :bad, 1, [file: 'task8.exs', line: 13]}, {Task.Supervised, :do_apply, 2, [file: 'lib/task/supervised.ex', line: 89]}, {Task.Supervised, :reply, 5, [file: 'lib/task/supervised.ex', line: 40]}, {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 240]}]}}
@jbosse
Copy link

jbosse commented Nov 22, 2021

Thanks! This helped me track down an issue I was having.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment