Skip to content

Commit eb80bc2

Browse files
committed
fix(forge): monitor beam rewriting worker process
Replace bare spawn with spawn_monitor in Beams.apply_to_all/2. If the worker process crashed, the parent would hang forever waiting for :progress messages that would never arrive. Propagate the monitor ref through block_until_done and raise on {:DOWN, ...} so crashes surface immediately. Demonitor on normal completion.
1 parent 59489c7 commit eb80bc2

File tree

2 files changed

+52
-15
lines changed

2 files changed

+52
-15
lines changed

apps/forge/lib/forge/namespace/transform/beams.ex

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,18 @@ defmodule Forge.Namespace.Transform.Beams do
1919

2020
me = self()
2121

22-
spawn(fn ->
23-
all_beams
24-
|> Task.async_stream(
25-
&apply_and_update_progress(&1, me),
26-
ordered: false,
27-
timeout: :infinity
28-
)
29-
|> Stream.run()
30-
end)
31-
32-
block_until_done(0, total_files, opts)
22+
{_pid, monitor_ref} =
23+
spawn_monitor(fn ->
24+
all_beams
25+
|> Task.async_stream(
26+
&apply_and_update_progress(&1, me),
27+
ordered: false,
28+
timeout: :infinity
29+
)
30+
|> Stream.run()
31+
end)
32+
33+
block_until_done(0, total_files, monitor_ref, opts)
3334
end
3435

3536
def apply(path) do
@@ -46,17 +47,23 @@ defmodule Forge.Namespace.Transform.Beams do
4647
defp changed?(same, same), do: false
4748
defp changed?(_, _), do: true
4849

49-
defp block_until_done(same, same, opts) do
50+
defp block_until_done(same, same, monitor_ref, opts) do
51+
Process.demonitor(monitor_ref, [:flush])
52+
5053
if !opts[:no_progress] do
5154
IO.write("\n")
5255
end
5356

5457
Mix.Shell.IO.info("Finished namespacing .beam files")
5558
end
5659

57-
defp block_until_done(current, max, opts) do
60+
defp block_until_done(current, max, monitor_ref, opts) do
5861
receive do
59-
:progress -> :ok
62+
:progress ->
63+
:ok
64+
65+
{:DOWN, ^monitor_ref, :process, _pid, reason} ->
66+
raise "Beam rewriting worker crashed: #{inspect(reason)}"
6067
end
6168

6269
current = current + 1
@@ -68,7 +75,7 @@ defmodule Forge.Namespace.Transform.Beams do
6875
IO.write(" Applying namespace: #{percent_complete} complete")
6976
end
7077

71-
block_until_done(current, max, opts)
78+
block_until_done(current, max, monitor_ref, opts)
7279
end
7380

7481
defp apply_and_update_progress(beam_file, caller) do
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
defmodule Forge.Namespace.Transform.BeamsTest do
2+
use ExUnit.Case, async: false
3+
use Patch
4+
5+
alias Forge.Namespace.Transform.Beams
6+
7+
@moduletag tmp_dir: true
8+
9+
describe "apply_to_all/2 crash handling" do
10+
test "raises when worker process crashes", %{tmp_dir: tmp_dir} do
11+
# We need total_files to be > 0 in block_until_done to enter the
12+
# receive loop
13+
File.mkdir_p!(Path.join([tmp_dir, "lib", "fake"]))
14+
File.write!(Path.join([tmp_dir, "lib", "fake", "Elixir.Fake.beam"]), "")
15+
16+
patch(Mix.Tasks.Namespace, :app_names, [:fake])
17+
18+
# Force a crash inside the worker. :beam_lib.chunks is the first
19+
# remote call in apply/1, so patching it sidesteps the with clause that
20+
# would otherwise handle a graceful error return.
21+
patch(:beam_lib, :chunks, fn _path, _chunks ->
22+
raise "simulated beam_lib crash"
23+
end)
24+
25+
assert_raise RuntimeError, ~r/Beam rewriting worker crashed/, fn ->
26+
Beams.apply_to_all(tmp_dir, no_progress: true)
27+
end
28+
end
29+
end
30+
end

0 commit comments

Comments
 (0)