This module provides functions for starting linked Erlang nodes. The node spawning new nodes is called origin, and newly started nodes are peer nodes, or peers. A peer node automatically terminates when it loses the control connection to the origin. This connection could be an Erlang distribution connection, or an alternative - TCP or standard I/O. The alternative connection provides a way to execute remote procedure calls even when Erlang Distribution is not available, allowing to test the distribution itself.
Peer node terminal input/output is relayed through the origin.
If a standard I/O alternative connection is requested, console output
also goes via the origin, allowing debugging of node startup and boot
script execution (see
The peer node can start on the same or a different host (via
This module is designed to facilitate multi-node testing with Common Test.
Use the
A peer node started without alternative connection behaves similarly
to
The following example implements a test suite starting extra Erlang nodes. It employs a number of techniques to speed up testing and reliably shut down peer nodes:
-module(my_SUITE).
-behaviour(ct_suite).
-export([all/0, groups/0]).
-export([basic/1, args/1, named/1, restart_node/1, multi_node/1]).
-include_lib("common_test/include/ct.hrl").
groups() ->
[{quick, [parallel],
[basic, args, named, restart_node, multi_node]}].
all() ->
[{group, quick}].
basic(Config) when is_list(Config) ->
{ok, Peer, _Node} = ?CT_PEER(),
peer:stop(Peer).
args(Config) when is_list(Config) ->
%% specify additional arguments to the new node
{ok, Peer, _Node} = ?CT_PEER(["-emu_flavor", "smp"]),
peer:stop(Peer).
named(Config) when is_list(Config) ->
%% pass test case name down to function starting nodes
Peer = start_node_impl(named_test),
peer:stop(Peer).
start_node_impl(ActualTestCase) ->
{ok, Peer, Node} = ?CT_PEER(#{name => ?CT_PEER_NAME(ActualTestCase)}),
%% extra setup needed for multiple test cases
ok = rpc:call(Node, application, set_env, [kernel, key, value]),
Peer.
restart_node(Config) when is_list(Config) ->
Name = ?CT_PEER_NAME(),
{ok, Peer, Node} = ?CT_PEER(#{name => Name}),
peer:stop(Peer),
%% restart the node with the same name as before
{ok, Peer2, Node} = ?CT_PEER(#{name => Name, args => ["+fnl"]}),
peer:stop(Peer2).
The next example demonstrates how to start multiple nodes concurrently:
multi_node(Config) when is_list(Config) ->
Peers = [?CT_PEER(#{wait_boot => {self(), tag}})
|| _ <- lists:seq(1, 4)],
%% wait for all nodes to complete boot process, get their names:
_Nodes = [receive {tag, {started, Node, Peer}} -> Node end
|| {ok, Peer} <- Peers],
[peer:stop(Peer) || {ok, Peer} <- Peers].
Start a peer on a different host. Requires
Ssh = os:find_executable("ssh"),
peer:start_link(#{exec => {Ssh, ["another_host", "erl"]},
connection => standard_io}),
The following Common Test case demonstrates Docker integration, starting two containers with hostnames "one" and "two". In this example Erlang nodes running inside containers form an Erlang cluster.
docker(Config) when is_list(Config) ->
Docker = os:find_executable("docker"),
PrivDir = proplists:get_value(priv_dir, Config),
build_release(PrivDir),
build_image(PrivDir),
%% start two Docker containers
{ok, Peer, Node} = peer:start_link(#{name => lambda,
connection => standard_io,
exec => {Docker, ["run", "-h", "one", "-i", "lambda"]}}),
{ok, Peer2, Node2} = peer:start_link(#{name => lambda,
connection => standard_io,
exec => {Docker, ["run", "-h", "two", "-i", "lambda"]}}),
%% find IP address of the second node using alternative connection RPC
{ok, Ips} = peer:call(Peer2, inet, getifaddrs, []),
{"eth0", Eth0} = lists:keyfind("eth0", 1, Ips),
{addr, Ip} = lists:keyfind(addr, 1, Eth0),
%% make first node to discover second one
ok = peer:call(Peer, inet_db, set_lookup, [[file]]),
ok = peer:call(Peer, inet_db, add_host, [Ip, ["two"]]),
%% join a cluster
true = peer:call(Peer, net_kernel, connect_node, [Node2]),
%% verify that second peer node has only the first node visible
[Node] = peer:call(Peer2, erlang, nodes, []),
%% stop peers, causing containers to also stop
peer:stop(Peer2),
peer:stop(Peer).
build_release(Dir) ->
%% load sasl.app file, otherwise application:get_key will fail
application:load(sasl),
%% create *.rel - release file
RelFile = filename:join(Dir, "lambda.rel"),
Release = {release, {"lambda", "1.0.0"},
{erts, erlang:system_info(version)},
[{App, begin {ok, Vsn} = application:get_key(App, vsn), Vsn end}
|| App <- [kernel, stdlib, sasl]]},
ok = file:write_file(RelFile, list_to_binary(lists:flatten(
io_lib:format("~tp.", [Release])))),
RelFileNoExt = filename:join(Dir, "lambda"),
%% create boot script
{ok, systools_make, []} = systools:make_script(RelFileNoExt,
[silent, {outdir, Dir}]),
%% package release into *.tar.gz
ok = systools:make_tar(RelFileNoExt, [{erts, code:root_dir()}]).
build_image(Dir) ->
%% Create Dockerfile example, working only for Ubuntu 20.04
%% Expose port 4445, and make Erlang distribution to listen
%% on this port, and connect to it without EPMD
%% Set cookie on both nodes to be the same.
BuildScript = filename:join(Dir, "Dockerfile"),
Dockerfile =
"FROM ubuntu:20.04 as runner\n"
"EXPOSE 4445\n"
"WORKDIR /opt/lambda\n"
"COPY lambda.tar.gz /tmp\n"
"RUN tar -zxvf /tmp/lambda.tar.gz -C /opt/lambda\n"
"ENTRYPOINT [\"/opt/lambda/erts-" ++ erlang:system_info(version) ++
"/bin/dyn_erl\", \"-boot\", \"/opt/lambda/releases/1.0.0/start\","
" \"-kernel\", \"inet_dist_listen_min\", \"4445\","
" \"-erl_epmd_port\", \"4445\","
" \"-setcookie\", \"secret\"]\n",
ok = file:write_file(BuildScript, Dockerfile),
os:cmd("docker build -t lambda " ++ Dir).
Identifies the controlling process of a peer node.
Options that can be used when starting
a
Node name (the part before "@"). When
Use long names to start a node. Default is taken from the origin
using
Enforces a specific host name. Can be used to override the default behaviour and start "node@localhost" instead of "node@realhostname".
Defines the peer control process behaviour when the control connection is
closed from the peer node side (for example when the peer crashes or dumps core).
When set to
Alternative connection specification. See the
Alternative mechanism to start peer nodes with, for example, ssh instead of the default bash.
Defines whether to pass the
Extra command line arguments to append to the "erl" command. Arguments are passed as is, no escaping or quoting is needed or accepted.
Allows the user to change the arguments passed to
peer:start(#{ name => peer:random_name(),
exec => {os:find_executable("bash"),["-c","erl"]},
post_process_args =>
fun(["-c"|Args]) -> ["-c", lists:flatten(lists:join($\s, Args))] end
}).
List of environment variables with their values. This list is applied
to a locally started executable. If you need to change the environment of
the remote peer, adjust
Specifies the start/start_link timeout.
See
Specifies the peer node stopping behaviour. See
Peer node state.
Alternative connection between the origin and the peer. When the
connection closes, the peer node terminates automatically. If
the
When
Using the
Overrides executable to start peer nodes with. By default it is
the path to "erl", taken from
When a tuple is passed, the first element is the path to executable, and the second element is prepended to the final command line. This can be used to start peers on a remote host or in a Docker container. See the examples above.
This option is useful for testing backwards compatibility with previous releases,
installed at specific paths, or when the Erlang installation location
is missing from the
Specifies start/start_link timeout in milliseconds. Can be set to
Disconnect timeout. See
Uses the alternative connection to
evaluate
When an alternative connection is not requested, this
function will raise
Uses the alternative connection to
evaluate
Uses the alternative connection to send
Returns the peer node state. Th initial state is
The same as
Creates a sufficiently unique node name for the current host, combining a prefix, a unique number, and the current OS process ID.
Use the
Starts a peer node with the specified
The same as
Starts a peer node in the same way as
Accepts
When the
Stops a peer node. How the node is stopped depends on the
This is the default shutdown behavior. It behaves as
Triggers a call to
Triggers a call to
Close the control connection to the peer node and
return. This is the fastest way for the caller of
Note that if the Erlang distribution connection is not used as
control connection it might not have been taken down when
In the cases where the Erlang distribution connection is taken
down by