Create an application

View Source

This tutorial builds a minimal OTP application on barrel_p2p from scratch: a worker that registers itself in the cluster-wide service registry, and a small API that discovers and calls a worker on any node. It is the setup companion to Distributed chat, which builds something larger on the same primitives.

The full source is under examples/quickstart; this page explains each piece. If you have not read Getting started, skim it first: it covers the boot flags and the credential layout in more depth.

What you get

A normal OTP app where Pid ! Msg, gen_server:call, global, links and monitors all work as usual, but distribution rides barrel_p2p's QUIC carrier and HyParView membership, and you get a built-in service registry.

Prerequisites

  • Erlang/OTP 27 or later, and rebar3.
  • No EPMD: barrel_p2p does not use it.
  • One UDP port per node (the default 9100 in this example).

Step 1: scaffold the app

rebar3 new app myapp
cd myapp

Step 2: add the dependency

In rebar.config:

{deps, [
    {barrel_p2p, "0.1.0"}
]}.
rebar3 get-deps && rebar3 compile

Barrel P2P pulls in the QUIC transport, hlc, and instrument.

Step 3: declare barrel_p2p as a runtime dependency

List it in src/myapp.app.src so the release boots it before your supervisor starts (your services register against a running barrel_p2p):

{applications, [kernel, stdlib, barrel_p2p]}

Step 4: configure the node

config/sys.config (barrel_p2p projects the underlying quic_dist wiring itself; you only set barrel_p2p env):

[
 {barrel_p2p, [
    {active_size, 5}, {passive_size, 30},   %% HyParView views
    {listen_port, 9100},                    %% pin in prod; 0 = OS-assigned
    {contact_nodes, []},                    %% seeds to auto-join at boot
    {dist_cookie, quickstart},              %% set as the node cookie at boot
    {auth_enabled, true},                   %% Ed25519 mutual auth (default)
    {auth_trust_mode, tofu}                 %% tofu | strict
 ]}
].

config/vm.args (the three flags switch Erlang's distribution to barrel_p2p):

-name myapp@127.0.0.1
-setcookie quickstart
-proto_dist barrel_p2p
-epmd_module barrel_p2p_epmd
-start_epmd false

Step 5: write a worker that registers a service

register_service/2 registers the calling process, so a gen_server that calls it in init/1 publishes itself under a cluster-wide name:

-module(myapp_worker).
-behaviour(gen_server).
-export([start_link/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).

start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

init([]) ->
    process_flag(trap_exit, true),
    ok = barrel_p2p:register_service(myapp_worker, #{node => node()}),
    ok = barrel_p2p:register_service({worker, node()}, #{}),
    {ok, #{}}.

handle_call({work, X}, _From, State) ->
    {reply, {worked_on, node(), X}, State};
handle_call(_Request, _From, State) ->
    {reply, {error, unknown_request}, State}.

handle_cast(_Msg, State) -> {noreply, State}.
handle_info(_Info, State) -> {noreply, State}.

terminate(_Reason, _State) ->
    barrel_p2p:unregister_service(myapp_worker),
    barrel_p2p:unregister_service({worker, node()}),
    ok.

It registers two names: a generic myapp_worker (local-preferred by whereis_service/1) and a per-node {worker, node()} so a specific node can be targeted. The registry monitors the pid, so the entries also disappear automatically if the worker dies.

The discover-and-call API. whereis_service/1 returns {ok, Pid} for a local service and {ok, Node, Pid} for a remote one; handle both, then send over standard distribution:

-module(myapp).
-export([work/1, work_on/2]).

work(X)          -> call(myapp_worker, {work, X}).
work_on(Node, X) -> call({worker, Node}, {work, X}).

call(Name, Msg) ->
    case barrel_p2p:whereis_service(Name) of
        {ok, Pid}        -> gen_server:call(Pid, Msg);
        {ok, _Node, Pid} -> gen_server:call(Pid, Msg);
        {error, not_found} -> {error, no_worker}
    end.

Step 6: supervise it

Add the worker to your supervisor's children (one_for_one), and start the supervisor from your application callback. (See quickstart_sup.erl and quickstart_app.erl in the example.)

Step 7: run one node

ERL_AFLAGS="-proto_dist barrel_p2p -epmd_module barrel_p2p_epmd -start_epmd false" \
rebar3 shell --config config/sys.config --sname q1
1> myapp:work(hello).
{worked_on, q1@yourhost, hello}
2> barrel_p2p:lookup(myapp_worker).
{ok, [{service_entry, myapp_worker, <0.123.0>, q1@yourhost, #{node => q1@yourhost}}]}

The node generated its TLS cert and Ed25519 identity under data/ on first boot. lookup/1 returns #service_entry{} records ({service_entry, name, pid, node, meta}).

Step 8: two nodes

The example ships scripts/run-local.sh, which gives each node its own keys and cert under data/node<N>/ while sharing data/discovery so they find each other on one host:

./scripts/run-local.sh 1     # seed
./scripts/run-local.sh 2     # joins node1

In node2's shell:

1> myapp:work_on('node1@yourhost', hi).
{worked_on, node1@yourhost, hi}

The reply is tagged by node1: node2 discovered node1's worker through the registry and called the real pid over standard distribution. Across hosts you would instead list seeds in contact_nodes and let nodes auto-join at boot (see Getting started).

Step 9: keys

Each node has an Ed25519 identity. In tofu mode the first handshake pins peers automatically, so the two-node demo needs no key setup. For strict mode, or to verify a node out of band, see Manage node keys: create keys, read a fingerprint, and share public keys with peers.

Step 10: production

For a real deployment, build a release (rebar3 as prod release), pin listen_port, set a real dist_cookie, list seeds in contact_nodes, and persist data/. See Run in production for ports, secrets, sizing, and the graceful shutdown order.

Where to go next