Skip to content

Commit 488d3a9

Browse files
authored
feat: epmdless clustering (#205)
* feat: epmdless clustering * docs: update epmd/distribution docs * fix: check for epmd on expert startup
1 parent d80afb1 commit 488d3a9

File tree

23 files changed

+267
-39
lines changed

23 files changed

+267
-39
lines changed

apps/engine/test/test_helper.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
Application.ensure_all_started(:snowflake)
22
Application.ensure_all_started(:refactorex)
3-
{"", 0} = System.cmd("epmd", ~w(-daemon))
3+
44
random_number = :rand.uniform(500)
55

66
with :nonode@nohost <- Node.self() do
77
{:ok, _pid} =
8-
:net_kernel.start(:"testing-#{random_number}@127.0.0.1", %{name_domain: :longnames})
8+
Node.start(:"expert-manager-testing-#{random_number}@127.0.0.1", :longnames)
99
end
1010

1111
Engine.Module.Loader.start_link(nil)

apps/expert/lib/expert/application.ex

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ defmodule Expert.Application do
6464
System.halt(1)
6565
end
6666

67+
ensure_epmd_module!()
68+
6769
children = [
70+
{Forge.NodePortMapper, []},
6871
document_store_child_spec(),
6972
{DynamicSupervisor, Expert.Project.DynamicSupervisor.options()},
7073
{DynamicSupervisor, name: Expert.DynamicSupervisor},
@@ -89,4 +92,23 @@ defmodule Expert.Application do
8992
def document_store_child_spec do
9093
{Document.Store, derive: [analysis: &Forge.Ast.analyze/1]}
9194
end
95+
96+
def ensure_epmd_module! do
97+
epmd_module = to_charlist(Forge.EPMD)
98+
99+
case :init.get_argument(:epmd_module) do
100+
{:ok, [[^epmd_module]]} ->
101+
:ok
102+
103+
_ ->
104+
Application.put_env(:kernel, :epmd_module, Forge.EPMD, persistent: true)
105+
106+
# Note: this is a private API
107+
if :net_kernel.epmd_module() != Forge.EPMD do
108+
raise("""
109+
you must set the environment variable ELIXIR_ERL_OPTIONS="-epmd_module #{Forge.EPMD}"
110+
""")
111+
end
112+
end
113+
end
92114
end

apps/expert/lib/expert/engine_node.ex

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,39 @@ defmodule Expert.EngineNode do
2828
@dialyzer {:nowarn_function, start: 3}
2929

3030
def start(%__MODULE__{} = state, paths, from) do
31-
this_node = inspect(Node.self())
32-
33-
args = [
34-
"--name",
35-
Project.node_name(state.project),
36-
"--cookie",
37-
state.cookie,
38-
"--no-halt",
39-
"-e",
40-
"Node.connect(#{this_node})"
41-
| path_append_arguments(paths)
42-
]
43-
44-
case Expert.Port.open_elixir(state.project, args: args) do
31+
this_node = to_string(Node.self())
32+
dist_port = Forge.EPMD.dist_port()
33+
34+
args =
35+
[
36+
"--erl",
37+
"-start_epmd false -epmd_module #{Forge.EPMD}",
38+
"--cookie",
39+
state.cookie,
40+
"--no-halt",
41+
"-e",
42+
# We manually start distribution here instead of using --sname/--name
43+
# because those options are not really compatible with `-epmd_module`.
44+
# Apparently, passing the --name/-sname options causes the Erlang VM
45+
# to start distribution right away before the modules in the code path
46+
# are loaded, and it will crash because Forge.EPMD doesn't exist yet.
47+
# If we start distribution manually after all the code is loaded,
48+
# everything works fine.
49+
"""
50+
{:ok, _} = Node.start(:"#{Project.node_name(state.project)}", :longnames)
51+
#{Forge.NodePortMapper}.register()
52+
IO.puts(\"ok\")
53+
"""
54+
| path_append_arguments(paths)
55+
]
56+
57+
env =
58+
[
59+
{"EXPERT_PARENT_NODE", this_node},
60+
{"EXPERT_PARENT_PORT", to_string(dist_port)}
61+
]
62+
63+
case Expert.Port.open_elixir(state.project, args: args, env: env) do
4564
{:error, :no_elixir, message} ->
4665
GenLSP.error(Expert.get_lsp(), message)
4766
Expert.terminate("Failed to find an elixir executable, shutting down", 1)
@@ -64,7 +83,7 @@ defmodule Expert.EngineNode do
6483
end
6584

6685
def on_nodeup(%__MODULE__{} = state, node_name) do
67-
if node_name == Project.node_name(state.project) do
86+
if String.starts_with?(to_string(node_name), to_string(Project.node_name(state.project))) do
6887
{pid, _ref} = state.started_by
6988
Process.monitor(pid)
7089
GenServer.reply(state.started_by, :ok)
@@ -117,7 +136,6 @@ defmodule Expert.EngineNode do
117136
use GenServer
118137

119138
def start(project) do
120-
:ok = ensure_epmd_started()
121139
start_net_kernel(project)
122140

123141
node_name = Project.node_name(project)
@@ -134,23 +152,13 @@ defmodule Expert.EngineNode do
134152

135153
defp start_net_kernel(%Project{} = project) do
136154
manager = Project.manager_node_name(project)
137-
:net_kernel.start(manager, %{name_domain: :longnames})
155+
Node.start(manager, :longnames)
138156
end
139157

140158
defp ensure_apps_started(node) do
141159
:rpc.call(node, Engine, :ensure_apps_started, [])
142160
end
143161

144-
defp ensure_epmd_started do
145-
case System.cmd("epmd", ~w(-daemon)) do
146-
{"", 0} ->
147-
:ok
148-
149-
_ ->
150-
{:error, :epmd_failed}
151-
end
152-
end
153-
154162
if Mix.env() == :test do
155163
# In test environment, Expert depends on the Engine app, so we look for it
156164
# in the expert build path.
@@ -294,7 +302,7 @@ defmodule Expert.EngineNode do
294302

295303
@impl true
296304
def handle_call({:start, paths}, from, %State{} = state) do
297-
:ok = :net_kernel.monitor_nodes(true, node_type: :visible)
305+
:ok = :net_kernel.monitor_nodes(true, node_type: :all)
298306
Process.send_after(self(), :maybe_start_timeout, @start_timeout)
299307

300308
case State.start(state, paths, from) do
@@ -365,7 +373,8 @@ defmodule Expert.EngineNode do
365373
end
366374

367375
@impl true
368-
def handle_info({_port, {:data, _message}}, %State{} = state) do
376+
def handle_info({_port, {:data, message}}, %State{} = state) do
377+
Logger.debug("Node port message: #{to_string(message)}")
369378
{:noreply, state}
370379
end
371380

apps/expert/lib/expert/port.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ defmodule Expert.Port do
106106
opts
107107
end
108108

109-
Port.open({:spawn_executable, launcher}, opts)
109+
Port.open({:spawn_executable, launcher}, [:stderr_to_stdout | opts])
110110
end
111111

112112
@doc """

apps/expert/rel/remote.vm.args.eex

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html
2+
## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here
3+
4+
## Increase number of concurrent ports/sockets
5+
##+Q 65536
6+
7+
## Tweak GC to run more often
8+
##-env ERL_FULLSWEEP_AFTER 10
9+
10+
## Enable deployment without epmd
11+
## (requires changing both vm.args and remote.vm.args)
12+
-epmd_module Elixir.XPForge.EPMD
13+
-start_epmd false

apps/expert/rel/vm.args.eex

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## Customize flags given to the VM: https://www.erlang.org/doc/man/erl.html
2+
## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here
3+
4+
## Increase number of concurrent ports/sockets
5+
##+Q 65536
6+
7+
## Tweak GC to run more often
8+
##-env ERL_FULLSWEEP_AFTER 10
9+
10+
## Enable deployment without epmd
11+
## (requires changing both vm.args and remote.vm.args)
12+
-start_epmd false -epmd_module Elixir.XPForge.EPMD

apps/expert/test/engine/build_test.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ defmodule Engine.BuildTest do
4242
|> Project.workspace_path()
4343
|> File.rm_rf()
4444

45+
{:ok, _} = start_supervised(Forge.NodePortMapper)
4546
{:ok, _} = start_supervised({EngineSupervisor, project})
4647
{:ok, _, _} = EngineNode.start(project)
4748
EngineApi.register_listener(project, self(), [:all])
@@ -650,6 +651,7 @@ defmodule Engine.BuildTest do
650651

651652
describe ".exs files" do
652653
setup do
654+
start_supervised!({Forge.NodePortMapper, []})
653655
start_supervised!(Engine.Dispatch)
654656
start_supervised!(Engine.ModuleMappings)
655657
start_supervised!(Build.CaptureServer)

apps/expert/test/engine/code_intelligence/definition_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ defmodule Expert.Engine.CodeIntelligence.DefinitionTest do
4343
end
4444

4545
setup_all do
46+
{:ok, _} = start_supervised({Forge.NodePortMapper, []})
4647
project = project(:navigations)
4748
start_supervised!({Document.Store, derive: [analysis: &Forge.Ast.analyze/1]})
4849
{:ok, _} = start_supervised({EngineSupervisor, project})

apps/expert/test/engine/engine_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule EngineTest do
99
import Forge.Test.Fixtures
1010

1111
def start_project(%Project{} = project) do
12+
start_supervised!({Forge.NodePortMapper, []})
1213
start_supervised!({Expert.EngineSupervisor, project})
1314
assert {:ok, _, _} = EngineNode.start(project)
1415
:ok

apps/expert/test/expert/engine_node_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ defmodule Expert.EngineNodeTest do
1010

1111
setup do
1212
project = project()
13+
start_supervised!({Forge.NodePortMapper, []})
1314
start_supervised!({EngineSupervisor, project})
1415
{:ok, %{project: project}}
1516
end

0 commit comments

Comments
 (0)