peer(3erl) | Erlang Module Definition | peer(3erl) |
peer - Start and control linked Erlang nodes.
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 -init_debug). File I/O is not redirected, contrary to slave(3erl) behaviour.
The peer node can start on the same or a different host (via ssh) or in a separate container (for example Docker). When the peer starts on the same host as the origin, it inherits the current directory and environment variables from the origin.
A peer node started without alternative connection behaves similarly to slave(3erl). When an alternative connection is requested, the behaviour is similar to test_server:start_node(Name, peer, Args).
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 key-based authentication set up, allowing "another_host" connection without password prompt.
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).
server_ref() = pid()
Identifies the controlling process of a peer node.
start_options() =
#{name => atom() | string(),
longnames => boolean(),
host => string(),
peer_down => stop | continue | crash,
exec => exec(),
connection => connection(),
args => [string()],
env => [{string(), string()}],
wait_boot => wait_boot(),
shutdown =>
close | halt |
{halt, disconnect_timeout()} |
disconnect_timeout()}
Options that can be used when starting a peer node through start/1 and start_link/0,1.
peer_state() = booting | running | {down, Reason :: term()}
Peer node state.
connection() =
0..65535 | {inet:ip_address(), 0..65535} | standard_io
Alternative connection between the origin and the peer. When the connection closes, the peer node terminates automatically. If the peer_down startup flag is set to crash, the controlling process on the origin node exits with corresponding reason, effectively providing a two-way link.
When connection is set to a port number, the origin starts listening on the requested TCP port, and the peer node connects to the port. When it is set to an {IP, Port} tuple, the origin listens only on the specified IP. The port number can be set to 0 for automatic selection.
Using the standard_io alternative connection starts the peer attached to the origin (other connections use -detached flag to erl). In this mode peer and origin communicate via stdin/stdout.
exec() = file:name() | {file:name(), [string()]}
Overrides executable to start peer nodes with. By default it is the path to "erl", taken from init:get_argument(progname). If progname is not known, peer makes best guess given the current ERTS version.
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 PATH.
wait_boot() = timeout() | {pid(), Tag :: term()} | false
Specifies start/start_link timeout in milliseconds. Can be set to false, allowing the peer to start asynchronously. If {Pid, Tag} is specified instead of a timeout, the peer will send Tag to the requested process.
disconnect_timeout() = 1000..4294967295 | infinity
Disconnect timeout. See stop().
call(Dest :: server_ref(),
Module :: module(),
Function :: atom(),
Args :: [term()]) ->
Result :: term()
call(Dest :: server_ref(),
Module :: module(),
Function :: atom(),
Args :: [term()],
Timeout :: timeout()) ->
Result :: term()
Uses the alternative connection to evaluate apply(Module, Function, Args) on the peer node and returns the corresponding value Result. Timeout is an integer representing the timeout in milliseconds or the atom infinity which prevents the operation from ever timing out.
When an alternative connection is not requested, this function will raise exit signal with the noconnection reason. Use erpc module to communicate over Erlang distribution.
cast(Dest :: server_ref(),
Module :: module(),
Function :: atom(),
Args :: [term()]) ->
ok
Uses the alternative connection to evaluate apply(Module, Function, Args) on the peer node. No response is delivered to the calling process.
peer:cast/4 fails silently when the alternative connection is not configured. Use erpc module to communicate over Erlang distribution.
send(Dest :: server_ref(),
To :: pid() | atom(),
Message :: term()) ->
ok
Uses the alternative connection to send Message to a process on the the peer node. Silently fails if no alternative connection is configured. The process can be referenced by process ID or registered name.
get_state(Dest :: server_ref()) -> peer_state()
Returns the peer node state. Th initial state is booting; the node stays in that state until then boot script is complete, and then the node progresses to running. If the node stops (gracefully or not), the state changes to down.
random_name() -> string()
The same as random_name(peer).
random_name(Prefix :: string() | atom()) -> string()
Creates a sufficiently unique node name for the current host, combining a prefix, a unique number, and the current OS process ID.
start(Options :: start_options()) ->
{ok, pid()} | {ok, pid(), node()} | {error, Reason}
Types:
Starts a peer node with the specified start_options(). Returns the controlling process and the full peer node name, unless wait_boot is not requested and the host name is not known in advance.
start_link() -> {ok, pid(), node()} | {error, Reason :: term()}
The same as start_link(#{name => random_name()}).
start_link(Options :: start_options()) ->
{ok, pid()} | {ok, pid(), node()} | {error, Reason}
Types:
Starts a peer node in the same way as start/1, except that the peer node is linked to the currently executing process. If that process terminates, the peer node also terminates.
Accepts start_options(). Returns the controlling process and the full peer node name, unless wait_boot is not requested and host name is not known in advance.
When the standard_io alternative connection is requested, and wait_boot is not set to false, a failed peer boot sequence causes the caller to exit with the {boot_failed, {exit_status, ExitCode}} reason.
stop(Dest :: server_ref()) -> ok
Types:
disconnect_timeout() = 1000..4294967295 | infinity
Stops a peer node. How the node is stopped depends on the shutdown option passed when starting the peer node. Currently the following shutdown options are supported:
Note that if the Erlang distribution connection is not used as control connection it might not have been taken down when peer:stop/1 returns. Also note that the warning below applies when the Erlang distribution connection is used as control connection.
stdlib 4.2 | Maxim Fedorov, WhatsApp Inc. |