%%% -*- erlang -*-
%%% Copyright (c) 2026 Benoit Chesneau
%%% SPDX-License-Identifier: Apache-2.0
%%%
-module(barrel_p2p_discovery).
-behaviour(quic_discovery).
%% Composing discovery module for barrel_p2p.
%%
%% Implements the upstream `quic_discovery' behaviour so it can be
%% wired into `quic_dist' as `discovery_module'. Delegates to a chain
%% of backend modules, each itself a `quic_discovery' implementation.
%%
%% The chain is read at every call from the application env:
%% application:get_env(barrel_p2p, discovery_backends, [...]).
%%
%% Default chain:
%% [barrel_p2p_discovery_static, %% explicit {Node, {Addr, Port}} map
%% barrel_p2p_discovery_file, %% data/discovery/<node>.endpoint files
%% barrel_p2p_discovery_dns] %% DNS host-from-name fallback
%%
%% Semantics:
%% - register/3 fans out to every backend that exports register/3.
%% - lookup/2 tries each backend in order; first {ok, _} wins.
%% - list_nodes/1 unions every backend's output.
%%
%% State for the composing module is a map of `Module => BackendState'.
%% Backends without an `init/1' callback get the empty map as state.
-export([init/1, register/3, lookup/2, list_nodes/1]).
-define(DEFAULT_BACKENDS, [
barrel_p2p_discovery_static,
barrel_p2p_discovery_file,
barrel_p2p_discovery_dns
]).
%%====================================================================
%% quic_discovery callbacks
%%====================================================================
init(Opts) ->
Backends = configured_backends(Opts),
States = lists:foldl(
fun(Mod, Acc) ->
case init_backend(Mod, Opts) of
{ok, S} -> Acc#{Mod => S};
{error, _} -> Acc
end
end,
#{},
Backends
),
{ok, States}.
register(Node, Port, State) when is_map(State) ->
Backends = configured_backends(#{}),
NewState = lists:foldl(
fun(Mod, Acc) -> register_one(Mod, Node, Port, Acc) end,
State,
Backends
),
{ok, NewState}.
register_one(Mod, Node, Port, Acc) ->
_ = code:ensure_loaded(Mod),
case erlang:function_exported(Mod, register, 3) of
false ->
Acc;
true ->
BS = maps:get(Mod, Acc, undefined),
case Mod:register(Node, Port, BS) of
{ok, NS} ->
Acc#{Mod => NS};
{error, R} ->
logger:warning(
"[barrel_p2p_discovery] ~p register failed: ~p",
[Mod, R]
),
Acc
end
end.
lookup(Node, Host) ->
Backends = configured_backends(#{}),
try_lookup(Backends, Node, Host).
list_nodes(Host) ->
Backends = configured_backends(#{}),
All = lists:foldl(
fun(Mod, Acc) ->
case erlang:function_exported(Mod, list_nodes, 1) of
false ->
Acc;
true ->
case Mod:list_nodes(Host) of
{ok, L} -> L ++ Acc;
{error, _} -> Acc
end
end
end,
[],
Backends
),
{ok, lists:usort(All)}.
%%====================================================================
%% Helpers
%%====================================================================
configured_backends(Opts) when is_map(Opts) ->
case maps:get(backends, Opts, undefined) of
undefined ->
application:get_env(barrel_p2p, discovery_backends, ?DEFAULT_BACKENDS);
Backends when is_list(Backends) ->
Backends
end;
configured_backends(_) ->
application:get_env(barrel_p2p, discovery_backends, ?DEFAULT_BACKENDS).
init_backend(Mod, Opts) ->
case erlang:function_exported(Mod, init, 1) of
false -> {ok, undefined};
true -> Mod:init(Opts)
end.
try_lookup([], _Node, _Host) ->
{error, not_found};
try_lookup([Mod | Rest], Node, Host) ->
case Mod:lookup(Node, Host) of
{ok, _} = Hit -> Hit;
{error, _} -> try_lookup(Rest, Node, Host)
end.