Powered by AppSignal & Oban Pro

Shipping code from one node to another

sending_code_to_another_node.livemd

Shipping code from one node to another

random_id = fn -> :crypto.strong_rand_bytes(4) |> Base.encode16(case: :lower) end

wait_until = fn check ->
  Stream.repeatedly(fn ->
    Process.sleep(100)
    check.()
  end)
  |> Stream.take(30)
  |> Enum.find(& &1)
end

The idea

You can take a module compiled on one node and load it on another at runtime.

The plumbing

Create a second node (node_b@remote.host) by spawning a plain Elixir node locally.

Cluster the node of this notebook with that second node.

id = random_id.()
cookie = "cookie_#{id}"

remote_port =
  Port.open(
    {:spawn_executable, System.find_executable("elixir")},
    [:binary, :stderr_to_stdout,
     args: ["--name", "node_b_#{id}@127.0.0.1", "--cookie", cookie,
            "-e", "spawn(fn -> Process.sleep(3_600_000); System.halt(0) end); IO.read(:stdio, :eof)"]]
  )

wait_until.(fn ->
  {:ok, names} = :net_adm.names()
  Enum.any?(names, fn {name, _} -> to_string(name) =~ "node_b_#{id}" end)
end) || raise("Remote node failed to start within 3s")

other_node = :"node_b_#{id}@127.0.0.1"
Node.set_cookie(other_node, :"#{cookie}")
true = Node.connect(other_node)

IO.puts("Connected to #{other_node}")

The technique

Define MyModule on this node:

defmodule MyModule do
  def say_hello do
    "Hello from #{node()}!"
  end
end

It doesn’t exist on node_b yet — calling it fails:

:rpc.call(other_node, MyModule, :say_hello, [])

Grab the compiled bytecode:

{_mod, binary, file} = :code.get_object_code(MyModule)
IO.puts("#{file}#{byte_size(binary)} bytes")

Ship it: load the bytecode into node_b‘s code server:

:rpc.call(other_node, :code, :load_binary, [MyModule, file, binary])

Now the call works: runs on node_b:

:rpc.call(other_node, MyModule, :say_hello, [])

The clean up

:code.delete/1 marks the current code as “old” (no new calls allowed); :code.purge/1 removes it from memory.

:rpc.call(other_node, :code, :delete, [MyModule])
:rpc.call(other_node, :code, :purge, [MyModule])

# Verify it's gone
IO.puts("MyModule still loaded: #{:rpc.call(other_node, Code, :ensure_loaded?, [MyModule])}")

# Stop the remote node
Port.close(remote_port)