Skip to content

Commit 8cada72

Browse files
authored
fix(engine): keep ModuleStore alive across builds (#889)
We have been attempting to improve our project compilation performance, and in particular how that interacts with Expert. One weird issue we ran into when testing rapid "save" calls on a file is that this could result in a crash in Expert. # Cause / Resolution Engine.ModuleStore exited :normal after every project_compiled event. As a permanent child of Engine.Supervisor that is one restart per build, so the fourth build to finish within five seconds exceeded the default restart intensity and shut the engine application down; the server then stopped reading stdin once its main process was restarted. Four quick saves that produce no-op builds are enough to trigger it since the edit window dropped to 100 ms (#872). Stay alive and keep rebuilding the store after each compile. Full disclosure: The test was written with the assistance of Fable, I'm pretty sure the stubs make sense, but if it doesn't seem right feel free to just tell me to go away and do it properly :)
1 parent 728cf1d commit 8cada72

2 files changed

Lines changed: 57 additions & 1 deletion

File tree

‎apps/engine/lib/engine/module_store.ex‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ defmodule Engine.ModuleStore do
2424
{:done, token}
2525
end)
2626

27-
{:stop, :normal, state}
27+
# This is a permanent child of Engine.Supervisor. Exiting here, even with
28+
# :normal, means one restart per build, and the fourth build to finish
29+
# within five seconds exceeds the supervisor's restart intensity and stops
30+
# the whole engine application.
31+
{:noreply, state}
2832
end
2933
end
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
defmodule Engine.ModuleStoreTest do
2+
use ExUnit.Case
3+
use Patch
4+
5+
import Forge.EngineApi.Messages
6+
7+
alias ElixirSense.Providers.Plugins.ModuleStore, as: ElixirSenseModuleStore
8+
alias Engine.Dispatch
9+
alias Engine.ModuleStore
10+
11+
setup do
12+
test_pid = self()
13+
start_supervised!(Dispatch)
14+
15+
# Progress reports go to the manager node over erpc; keep them local.
16+
patch(Dispatch, :erpc_call, fn
17+
Expert.Progress, :begin, [_title, _opts] -> {:ok, System.unique_integer([:positive])}
18+
Expert.Progress, :report, _args -> :ok
19+
end)
20+
21+
patch(Dispatch, :erpc_cast, fn Expert.Progress, _function, _args -> true end)
22+
23+
patch(ElixirSenseModuleStore, :build, fn ->
24+
send(test_pid, :module_store_built)
25+
:ok
26+
end)
27+
28+
pid = start_supervised!(ModuleStore)
29+
{:ok, pid: pid}
30+
end
31+
32+
test "rebuilds the module store when the project compiles, and stays up", %{pid: pid} do
33+
ref = Process.monitor(pid)
34+
Dispatch.broadcast(project_compiled(status: :success))
35+
36+
assert_receive :module_store_built
37+
refute_receive {:DOWN, ^ref, :process, ^pid, _}
38+
end
39+
40+
test "survives many builds in quick succession", %{pid: pid} do
41+
# It used to exit :normal after each build. As a permanent child that meant
42+
# one restart per build, so the fourth build within five seconds exceeded
43+
# the supervisor's restart intensity and took the engine application down.
44+
for _ <- 1..10 do
45+
Dispatch.broadcast(project_compiled(status: :success))
46+
assert_receive :module_store_built
47+
end
48+
49+
assert Process.alive?(pid)
50+
assert Dispatch.registered?(pid)
51+
end
52+
end

0 commit comments

Comments
 (0)