Skip to main content

src/barrel_p2p_bridge.erl

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

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

-define(SERVER, ?MODULE).

-record(state, {
    pending = #{} :: #{node() => connecting}
}).

%%====================================================================
%% API
%%====================================================================

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

-spec request_connect(node()) -> ok.
request_connect(Node) ->
    gen_server:cast(?SERVER, {request_connect, Node}).

%%====================================================================
%% gen_server callbacks
%%====================================================================

init([]) ->
    net_kernel:monitor_nodes(true, [{node_type, visible}]),
    {ok, #state{}}.

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

handle_cast({request_connect, Node}, State) ->
    case maps:is_key(Node, State#state.pending) of
        true ->
            {noreply, State};
        false ->
            case lists:member(Node, nodes()) of
                true ->
                    %% Dist channel is already up (someone called
                    %% `net_kernel:connect_node/1' directly, or OTP
                    %% auto-connected via `Pid ! Msg'). No nodeup will
                    %% fire; resolve HyParView pending immediately.
                    barrel_p2p_hyparview:peer_connected(Node, undefined),
                    {noreply, State};
                false ->
                    Self = self(),
                    spawn_link(fun() ->
                        case net_kernel:connect_node(Node) of
                            true ->
                                %% nodeup will be received
                                ok;
                            false ->
                                Self ! {connect_failed, Node};
                            ignored ->
                                ok
                        end
                    end),
                    Pending = maps:put(Node, connecting, State#state.pending),
                    {noreply, State#state{pending = Pending}}
            end
    end;
handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info({nodeup, Node, _Info}, State) ->
    %% Only escalate to HyParView when WE asked for this connection via
    %% `request_connect/1' (i.e. it's part of the gossip topology).
    %% Other dist channels - opened by `net_kernel:connect_node/1' or by
    %% OTP's `Pid ! Msg' auto-connect - stay outside the active view; the
    %% active view tracks the bounded gossip topology, not raw dist.
    case maps:take(Node, State#state.pending) of
        {connecting, NewPending} ->
            barrel_p2p_hyparview:peer_connected(Node, undefined),
            {noreply, State#state{pending = NewPending}};
        error ->
            {noreply, State}
    end;
handle_info({nodedown, Node, _Info}, State) ->
    Pending = maps:remove(Node, State#state.pending),
    barrel_p2p_hyparview:peer_failed(Node, nodedown),
    {noreply, State#state{pending = Pending}};
handle_info({connect_failed, Node}, State) ->
    case maps:is_key(Node, State#state.pending) of
        true ->
            Pending = maps:remove(Node, State#state.pending),
            barrel_p2p_hyparview:peer_failed(Node, connect_failed),
            {noreply, State#state{pending = Pending}};
        false ->
            {noreply, State}
    end;
%% Handle HyParView protocol messages
handle_info({'$barrel_p2p_hyparview', From, Msg}, State) ->
    barrel_p2p_protocol:handle_message({'$barrel_p2p_hyparview', From, Msg}, self()),
    {noreply, State};
handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.