Skip to content

Commit 7f8e8b6

Browse files
committed
Lock mix compile.elixir to avoid concurrent usage, closes #12013
1 parent 7cc21ea commit 7f8e8b6

File tree

2 files changed

+105
-28
lines changed

2 files changed

+105
-28
lines changed

lib/mix/lib/mix/state.ex

Lines changed: 91 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,25 @@
11
defmodule Mix.State do
22
@moduledoc false
33
@name __MODULE__
4+
@timeout :infinity
45

56
use GenServer
67

78
def start_link(_opts) do
89
GenServer.start_link(__MODULE__, :ok, name: @name)
910
end
1011

11-
@impl true
12-
def init(:ok) do
13-
table = :ets.new(@name, [:public, :set, :named_table, read_concurrency: true])
14-
15-
:ets.insert(table,
16-
shell: Mix.Shell.IO,
17-
env: from_env("MIX_ENV", :dev),
18-
target: from_env("MIX_TARGET", :host),
19-
scm: [Mix.SCM.Git, Mix.SCM.Path]
20-
)
21-
22-
{:ok, table}
23-
end
24-
25-
defp from_env(varname, default) do
26-
case System.get_env(varname) do
27-
nil -> default
28-
"" -> default
29-
value -> String.to_atom(value)
12+
def lock(key, fun) do
13+
try do
14+
GenServer.call(@name, {:lock, key}, @timeout)
15+
fun.()
16+
after
17+
GenServer.call(@name, {:unlock, key}, @timeout)
3018
end
3119
end
3220

21+
## ETS state storage (mutable, not cleared ion tests)
22+
3323
def fetch(key) do
3424
case :ets.lookup(@name, key) do
3525
[{^key, value}] -> {:ok, value}
@@ -52,6 +42,8 @@ defmodule Mix.State do
5242
:ets.insert(@name, {key, fun.(:ets.lookup_element(@name, key, 2))})
5343
end
5444

45+
## Persistent term cache (persistent, cleared in tests)
46+
5547
def read_cache(key) do
5648
:persistent_term.get({__MODULE__, key}, nil)
5749
end
@@ -70,4 +62,84 @@ defmodule Mix.State do
7062
:persistent_term.erase(key)
7163
end
7264
end
65+
66+
## Callbacks
67+
68+
@impl true
69+
def init(:ok) do
70+
table = :ets.new(@name, [:public, :set, :named_table, read_concurrency: true])
71+
72+
:ets.insert(table,
73+
shell: Mix.Shell.IO,
74+
env: from_env("MIX_ENV", :dev),
75+
target: from_env("MIX_TARGET", :host),
76+
scm: [Mix.SCM.Git, Mix.SCM.Path]
77+
)
78+
79+
{:ok, {%{}, %{}}}
80+
end
81+
82+
defp from_env(varname, default) do
83+
case System.get_env(varname) do
84+
nil -> default
85+
"" -> default
86+
value -> String.to_atom(value)
87+
end
88+
end
89+
90+
@impl true
91+
def handle_call({:lock, key}, {pid, _} = from, {key_to_waiting, pid_to_key}) do
92+
key_to_waiting =
93+
case key_to_waiting do
94+
%{^key => {locked, waiting}} ->
95+
Map.put(key_to_waiting, key, {locked, :queue.in(from, waiting)})
96+
97+
%{} ->
98+
go!(from)
99+
Map.put(key_to_waiting, key, {pid, :queue.new()})
100+
end
101+
102+
ref = Process.monitor(pid)
103+
{:noreply, {key_to_waiting, Map.put(pid_to_key, pid, {key, ref})}}
104+
end
105+
106+
@impl true
107+
def handle_call({:unlock, key}, {pid, _}, {key_to_waiting, pid_to_key}) do
108+
{{^key, ref}, pid_to_key} = Map.pop(pid_to_key, pid)
109+
Process.demonitor(ref, [:flush])
110+
{:reply, :ok, {unlock(key_to_waiting, pid_to_key, key), pid_to_key}}
111+
end
112+
113+
@impl true
114+
def handle_info({:DOWN, ref, _type, pid, _reason}, {key_to_waiting, pid_to_key}) do
115+
{{key, ^ref}, pid_to_key} = Map.pop(pid_to_key, pid)
116+
117+
key_to_waiting =
118+
case key_to_waiting do
119+
%{^key => {^pid, _}} ->
120+
unlock(key_to_waiting, pid_to_key, key)
121+
122+
%{^key => {locked, waiting}} ->
123+
Map.put(key_to_waiting, key, {locked, List.keydelete(waiting, pid, 0)})
124+
end
125+
126+
{:noreply, {key_to_waiting, pid_to_key}}
127+
end
128+
129+
defp unlock(key_to_waiting, pid_to_key, key) do
130+
%{^key => {_locked, waiting}} = key_to_waiting
131+
132+
case :queue.out(waiting) do
133+
{{:value, {pid, _} = from}, waiting} ->
134+
# Assert that we still know this PID
135+
_ = Map.fetch!(pid_to_key, pid)
136+
go!(from)
137+
Map.put(key_to_waiting, key, {pid, waiting})
138+
139+
{:empty, _waiting} ->
140+
Map.delete(key_to_waiting, key)
141+
end
142+
end
143+
144+
defp go!(from), do: GenServer.reply(from, :ok)
73145
end

lib/mix/lib/mix/tasks/compile.elixir.ex

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,20 @@ defmodule Mix.Tasks.Compile.Elixir do
111111
|> tracers_opts(tracers)
112112
|> profile_opts()
113113

114-
Mix.Compilers.Elixir.compile(
115-
manifest,
116-
srcs,
117-
dest,
118-
cache_key,
119-
Mix.Tasks.Compile.Erlang.manifests(),
120-
Mix.Tasks.Compile.Erlang.modules(),
121-
opts
122-
)
114+
# The Elixir compiler relies on global state in the application tracer.
115+
# However, even without it, having compilations racing with other is most
116+
# likely undesired, so we wrap the compiler in a lock.
117+
Mix.State.lock(__MODULE__, fn ->
118+
Mix.Compilers.Elixir.compile(
119+
manifest,
120+
srcs,
121+
dest,
122+
cache_key,
123+
Mix.Tasks.Compile.Erlang.manifests(),
124+
Mix.Tasks.Compile.Erlang.modules(),
125+
opts
126+
)
127+
end)
123128
end
124129

125130
@impl true

0 commit comments

Comments
 (0)