Skip to content

Commit

Permalink
Supports :peer module (>= OTP 25)
Browse files Browse the repository at this point in the history
API change: LocalCluster.start_nodes/3 no longer returns node
names. See LocalCluster.nodes/1.
  • Loading branch information
JesseStimpson committed Aug 25, 2024
1 parent 9fc14f5 commit 510bab7
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 31 deletions.
35 changes: 19 additions & 16 deletions lib/local_cluster.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ defmodule LocalCluster do
when testing distributed applications.
"""

alias LocalCluster.Peer

@doc """
Starts the current node as a distributed node.
"""
Expand Down Expand Up @@ -41,7 +43,7 @@ defmodule LocalCluster do
Starts a number of namespaced child nodes.
This will start the current runtime environment on a set of child nodes
and return the names of the nodes to the user for further use. All child
and return a list of `%LocalCluster.Peer{}` structs for further use. All child
nodes are linked to the current process, and so will terminate when the
parent process does for automatic cleanup.
Expand All @@ -55,23 +57,21 @@ defmodule LocalCluster do
nodes, which are then compiled on the remote node. This is necessary
if you wish to spawn tasks from inside test code, as test code would
not typically be loaded automatically.
The caller should use `LocalCluster.node(peer)` and `LocalCluster.nodes(peers)`
to retrieve the node names.
"""
@spec start_nodes(binary, integer, Keyword.t()) :: [atom]
@spec start_nodes(binary, integer, Keyword.t()) :: [Peer.t()]
def start_nodes(prefix, amount, options \\ [])
when (is_binary(prefix) or is_atom(prefix)) and is_integer(amount) do
nodes =
peers =
Enum.map(1..amount, fn idx ->
{:ok, name} =
:slave.start_link(
~c"127.0.0.1",
:"#{prefix}#{idx}",
~c"-loader inet -hosts 127.0.0.1 -setcookie \"#{:erlang.get_cookie()}\""
)

name
{:ok, peer} = Peer.start_link(prefix, idx)

peer
end)

rpc = &({_, []} = :rpc.multicall(nodes, &1, &2, &3))
rpc = &({_, []} = :rpc.multicall(Peer.nodes(peers), &1, &2, &3))

rpc.(:code, :add_paths, [:code.get_path()])

Expand Down Expand Up @@ -108,15 +108,18 @@ defmodule LocalCluster do
rpc.(Code, :require_file, [file])
end

nodes
peers
end

@doc """
Stops a set of child nodes.
"""
@spec stop_nodes([atom]) :: :ok
def stop_nodes(nodes) when is_list(nodes),
do: Enum.each(nodes, &:slave.stop/1)
@spec stop_nodes([Peer.t()]) :: :ok
def stop_nodes(peers) when is_list(peers),
do: Enum.each(peers, &Peer.stop/1)

defdelegate nodes(peers), to: Peer
defdelegate node(peer), to: Peer

@doc """
Stops the current distributed node and turns it back into a local node.
Expand Down
67 changes: 67 additions & 0 deletions lib/local_cluster/peer.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
defmodule LocalCluster.Peer do
@moduledoc """
Contains metadata about a peer that has been started with `LocalCluster.start_nodes/2`
or `LocalCluster.start_nodes/3`.
"""
import Kernel, except: [node: 1]

@type t :: %__MODULE__{}

defstruct [:node, :pid]

@doc """
Given a list of `LocalCluster.Peer` structs, returns the
node names.
The node names can be used for `:rpc` calls.
"""
def nodes(peers) when is_list(peers) do
Enum.map(peers, &node/1)
end

@doc """
Given a `LocalCluster.Peer`, returns the node name.
The node name can be used for `:rpc` calls.
"""
def node(%__MODULE__{node: node}) do
node
end

def start_link(prefix, idx) do
args =
~w[-loader inet -hosts 127.0.0.1 -setcookie #{:erlang.get_cookie()}]
|> Enum.map(&String.to_charlist/1)

{:ok, pid, node} =
start_link_int(%{
host: ~c"127.0.0.1",
name: :"#{prefix}#{idx}",
args: args
})

{:ok, %__MODULE__{node: node, pid: pid}}
end

def stop(%__MODULE__{pid: pid, node: node}) do
stop_int(pid, node)
end

if Code.ensure_loaded?(:peer) and function_exported?(:peer, :start_link, 1) do
def start_link_int(opts), do: :peer.start_link(opts)
def stop_int(pid, _node), do: :peer.stop(pid)
else
# Support for OTP < 25
def start_link_int(%{host: host, name: name, args: args}) do
case :slave.start_link(host, name, :string.join(args, ~c" ")) do
{:ok, node} ->
{:ok, nil, node}

error ->
error
end
end

def stop_int(_pid, node), do: :slave.stop(node)
end
end
40 changes: 25 additions & 15 deletions test/local_cluster_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,30 @@ defmodule LocalClusterTest do
doctest LocalCluster

test "creates and stops child nodes" do
nodes = LocalCluster.start_nodes(:child, 3)
peers = LocalCluster.start_nodes(:child, 3)

[node1, node2, node3] = nodes
[peer1, peer2, peer3] = peers
[node1, node2, node3] = LocalCluster.nodes(peers)

assert Node.ping(node1) == :pong
assert Node.ping(node2) == :pong
assert Node.ping(node3) == :pong

:ok = LocalCluster.stop_nodes([node1])
:ok = LocalCluster.stop_nodes([peer1])

assert Node.ping(node1) == :pang
assert Node.ping(node2) == :pong
assert Node.ping(node3) == :pong

:ok = LocalCluster.stop_nodes([node2, node3])
:ok = LocalCluster.stop_nodes([peer2, peer3])

assert Node.ping(node1) == :pang
assert Node.ping(node2) == :pang
assert Node.ping(node3) == :pang
end

test "load selected applications" do
nodes =
peers =
LocalCluster.start_nodes(:child, 1,
applications: [
:local_cluster,
Expand All @@ -34,7 +35,7 @@ defmodule LocalClusterTest do
]
)

[node1] = nodes
[node1] = LocalCluster.nodes(peers)

node1_apps =
node1
Expand All @@ -45,18 +46,18 @@ defmodule LocalClusterTest do
assert :ex_unit in node1_apps
assert :no_real_app in node1_apps == false

:ok = LocalCluster.stop_nodes(nodes)
:ok = LocalCluster.stop_nodes(peers)
end

test "spawns tasks directly on child nodes" do
nodes =
peers =
LocalCluster.start_nodes(:spawn, 3,
files: [
__ENV__.file
]
)

[node1, node2, node3] = nodes
[node1, node2, node3] = LocalCluster.nodes(peers)

assert Node.ping(node1) == :pong
assert Node.ping(node2) == :pong
Expand All @@ -82,30 +83,39 @@ defmodule LocalClusterTest do
end

test "overriding environment variables on child nodes" do
[node1] =
[peer1] =
LocalCluster.start_nodes(:cluster_var_a, 1,
environment: [
local_cluster: [override: "test1"]
]
)

[node2] =
[peer2] =
LocalCluster.start_nodes(:cluster_var_b, 1,
environment: [
local_cluster: [override: "test2"]
]
)

[node3] = LocalCluster.start_nodes(:cluster_no_env, 1)
[peer3] = LocalCluster.start_nodes(:cluster_no_env, 1)

node1_env =
:rpc.call(node1, Application, :get_env, [:local_cluster, :override])
:rpc.call(LocalCluster.node(peer1), Application, :get_env, [
:local_cluster,
:override
])

node2_env =
:rpc.call(node2, Application, :get_env, [:local_cluster, :override])
:rpc.call(LocalCluster.node(peer2), Application, :get_env, [
:local_cluster,
:override
])

node3_env =
:rpc.call(node3, Application, :get_env, [:local_cluster, :override])
:rpc.call(LocalCluster.node(peer3), Application, :get_env, [
:local_cluster,
:override
])

assert node1_env == "test1"
assert node2_env == "test2"
Expand Down

0 comments on commit 510bab7

Please sign in to comment.