%%% -*- 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()].