Skip to main content

src/barrel_p2p_bootstrap.erl

%%% -*- erlang -*-
%%% Copyright (c) 2026 Benoit Chesneau
%%% SPDX-License-Identifier: Apache-2.0
%%%
%%% Seed bootstrap: auto-joins the cluster from the configured
%%% `contact_nodes' (the seeds) at boot, so a node forms a cluster from
%%% config without a manual `barrel_p2p:join/1'. The seed itself (empty
%%% `contact_nodes') needs no bootstrap, so this worker idles there.
%%%
%%% While the node has an empty active view it periodically asks each
%%% contact to let it in (`barrel_p2p:join/1' is a non-blocking request to
%%% connect; the seed's address is resolved through the discovery chain).
%%% It keeps checking every `contact_retry_ms' so a seed that comes up
%%% late, or a node that loses all peers, re-joins on its own. Once the
%%% active view is non-empty the check is a cheap no-op.
-module(barrel_p2p_bootstrap).
-behaviour(gen_server).

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

-define(SERVER, ?MODULE).
-define(DEFAULT_RETRY_MS, 5000).
%% Defer the first attempt so the listener and discovery are up.
-define(INITIAL_DELAY_MS, 1000).

-record(state, {
    contacts :: [node()],
    retry_ms :: pos_integer()
}).

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

init([]) ->
    case contacts() of
        [] ->
            %% A seed (or a node configured to be joined to) has nothing to
            %% bootstrap from; do not run.
            ignore;
        Contacts ->
            Retry = application:get_env(barrel_p2p, contact_retry_ms, ?DEFAULT_RETRY_MS),
            erlang:send_after(min(?INITIAL_DELAY_MS, Retry), self(), join_contacts),
            {ok, #state{contacts = Contacts, retry_ms = Retry}}
    end.

handle_call(_Request, _From, State) ->
    {reply, ok, State}.

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

handle_info(join_contacts, #state{contacts = Contacts, retry_ms = Retry} = State) ->
    case barrel_p2p:active_view() of
        [_ | _] ->
            %% Already in a cluster; nothing to do this tick.
            ok;
        [] ->
            _ = [barrel_p2p:join(C) || C <- Contacts]
    end,
    erlang:send_after(Retry, self(), join_contacts),
    {noreply, State};
handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

%% Configured seeds, minus ourselves (a node may ship a cluster-wide
%% contact list that includes its own name).
contacts() ->
    [N || N <- application:get_env(barrel_p2p, contact_nodes, []), N =/= node()].