Skip to main content

src/barrel_p2p_app.erl

%%% -*- erlang -*-
%%% Copyright (c) 2026 Benoit Chesneau
%%% SPDX-License-Identifier: Apache-2.0
%%%
-module(barrel_p2p_app).
-behaviour(application).

-export([start/2, stop/1]).

start(_StartType, _StartArgs) ->
    %% Set distribution cookie automatically
    init_dist_cookie(),
    %% Disable global's partition prevention - barrel_p2p manages topology
    ok = application:set_env(kernel, prevent_overlapping_partitions, false),
    %% HyParView owns the bounded gossip topology (active view); OTP
    %% owns demand-driven dist channels (`Pid ! Msg' to any cluster
    %% node auto-connects through the barrel_p2p discovery chain). The
    %% two are decoupled: `barrel_p2p_dist_gc' reaps idle non-gossip
    %% channels so the natural fan-out stays bounded.
    ok = application:set_env(kernel, dist_auto_connect, once),
    %% Publish ourselves through the discovery chain. quic_dist's own
    %% registration path runs before sys.config envs apply, so we
    %% redo it here now that barrel_p2p's discovery_backends env is set.
    publish_self(),
    barrel_p2p_sup:start_link().

%% @private Register this node with the discovery chain after sys.config
%% has applied, so backends configured in `discovery_backends' actually
%% see the call. Best-effort; logs and moves on if anything fails.
publish_self() ->
    case node() of
        nonode@nohost ->
            ok;
        Node ->
            case find_listen_port() of
                {ok, Port} ->
                    DistOpts = application:get_env(quic, dist, []),
                    {ok, S0} = barrel_p2p_discovery:init(#{}),
                    _ = barrel_p2p_discovery:register(Node, Port, S0),
                    _ = DistOpts,
                    ok;
                error ->
                    logger:debug(
                        "[barrel_p2p_app] no listen port found; "
                        "skipping discovery publish"
                    ),
                    ok
            end
    end.

%% @private Find the live quic_dist listener port via persistent_term.
%% Upstream `quic_dist' stashes `{quic_dist_early_listener, _} ->
%% #{port => P, ...}` during early boot, and the same entry sticks
%% around once the quic app adopts the listener.
find_listen_port() ->
    Hits = [
        V
     || {{quic_dist_early_listener, _}, V} <- persistent_term:get(),
        is_map(V)
    ],
    case Hits of
        [#{port := Port} | _] when is_integer(Port) -> {ok, Port};
        _ -> error
    end.

%% @doc Set the distribution cookie automatically.
%% Uses the configured dist_cookie or defaults to 'barrel_p2p'.
%% This removes the need for users to set -setcookie on the command line.
%% Only sets cookie when running as a distributed node.
init_dist_cookie() ->
    case node() of
        nonode@nohost ->
            %% Not a distributed node, skip cookie setup
            ok;
        Node ->
            Cookie = application:get_env(barrel_p2p, dist_cookie, barrel_p2p),
            check_cookie_safety(Cookie),
            erlang:set_cookie(Node, Cookie)
    end.

%% @private The default cookie is a public constant. With Ed25519 auth on
%% it is only defense-in-depth, so we warn. But for cookie_only_nodes
%% peers the cookie is the sole barrier - refuse to boot in that
%% combination rather than ship a cluster gated by a known cookie.
check_cookie_safety(Cookie) ->
    CookieOnly = application:get_env(barrel_p2p, cookie_only_nodes, []),
    case Cookie =:= barrel_p2p of
        true when CookieOnly =/= [] ->
            erlang:error({barrel_p2p, cookie_only_nodes_with_default_cookie});
        true ->
            logger:warning(
                "barrel_p2p: using the default distribution cookie 'barrel_p2p'. "
                "Set {barrel_p2p, [{dist_cookie, <secret>}]} for production."
            );
        false ->
            ok
    end.

stop(_State) ->
    ok.