diff --git a/packages/bridge/lib/pi/target/attached.ex b/packages/bridge/lib/pi/target/attached.ex index 380b77e4..995db10d 100644 --- a/packages/bridge/lib/pi/target/attached.ex +++ b/packages/bridge/lib/pi/target/attached.ex @@ -53,7 +53,7 @@ defmodule Pi.Target.Attached do def handle_call(:reset, _from, state) do Enum.each(state, fn {{node, _session_id}, evaluator} -> - _ = remote_call(node, Process, :exit, [evaluator, :kill], 5_000) + :erpc.cast(node, Process, :exit, [evaluator, :kill]) end) {:reply, :ok, %{}} @@ -114,7 +114,7 @@ defmodule Pi.Target.Attached do {:ok, value} valid_node_name?(value) -> - bounded_atom({__MODULE__, :configured_node}, value) + bounded_atom({__MODULE__, :configured_node}, value, 64) true -> {:error, @@ -197,7 +197,7 @@ defmodule Pi.Target.Attached do {:ok, value} value when is_binary(value) and byte_size(value) <= 255 -> - bounded_atom({__MODULE__, :configured_cookie}, value) + bounded_atom({__MODULE__, :configured_cookie}, value, 1) _other -> {:error, %{kind: :invalid_cookie, message: "Distribution cookie is invalid"}} @@ -213,19 +213,21 @@ defmodule Pi.Target.Attached do end end - # Erlang distribution requires atom node/cookie identifiers. Each key is immutable after - # its first explicit configuration, bounding this VM to at most one new atom per key. - defp bounded_atom(key, value) do - case :persistent_term.get(key, :unset) do - :unset -> - atom = :erlang.binary_to_atom(value) - :persistent_term.put(key, {value, atom}) + # Distribution needs atoms. Cap the distinct values so changing targets cannot + # exhaust the atom table. + defp bounded_atom(key, value, limit) do + atoms = :persistent_term.get(key, %{}) + + cond do + atom = Map.get(atoms, value) -> {:ok, atom} - {^value, atom} -> + map_size(atoms) < limit -> + atom = :erlang.binary_to_atom(value) + :persistent_term.put(key, Map.put(atoms, value, atom)) {:ok, atom} - {_configured, _atom} -> + true -> {:error, %{ kind: :distribution_configuration_changed, diff --git a/packages/bridge/test/pi/target/attached_test.exs b/packages/bridge/test/pi/target/attached_test.exs index d4602be2..d0909101 100644 --- a/packages/bridge/test/pi/target/attached_test.exs +++ b/packages/bridge/test/pi/target/attached_test.exs @@ -58,6 +58,43 @@ defmodule Pi.Target.AttachedTest do refute message =~ "{:ok," end + test "switches runtime nodes without restarting the bridge" do + first = peer() |> Atom.to_string() + second = peer() |> Atom.to_string() + + assert {:ok, _} = Attached.run_structured("marker = :first", node: first) + assert {:ok, result} = Attached.run_structured("binding()[:marker]", node: second) + assert result.result == "nil" + assert {:ok, result} = Attached.run_structured("marker", node: first) + assert result.result == ":first" + end + + test "reset tolerates disconnected nodes" do + node = peer() + assert {:ok, _} = Attached.run_structured("1 + 1", node: node) + Node.monitor(node, true) + :erpc.cast(node, :init, :stop, []) + assert_receive {:nodedown, ^node}, 5_000 + assert :ok = Attached.reset() + end + + defp peer do + name = :"pi_attached_peer_#{System.unique_integer([:positive])}" + cookie = Atom.to_charlist(Node.get_cookie()) + + {:ok, pid, node} = + :peer.start(%{ + name: name, + connection: :standard_io, + args: [~c"-hidden", ~c"-setcookie", cookie] + }) + + on_exit(fn -> if Process.alive?(pid), do: :peer.stop(pid) end) + :ok = :peer.call(pid, :code, :add_paths, [:code.get_path()]) + {:ok, _} = :peer.call(pid, :application, :ensure_all_started, [:elixir]) + node + end + test "requires an explicit node", %{node: _node} do previous = System.get_env("PI_ELIXIR_NODE") System.delete_env("PI_ELIXIR_NODE")