%%% -*- erlang -*-
%%% Copyright (c) 2026 Benoit Chesneau
%%% SPDX-License-Identifier: Apache-2.0
%%%
%%% Dynamic supervisor for public replicated maps (`barrel_p2p_map'). One
%%% per-map instance supervisor child per named map. Holds an ETS registry
%%% (map name -> instance-sup pid) for idempotent `start_map' and lookup.
%%%
%%% At boot it starts every map declared in the `replicated_maps' app env,
%%% so a map declared in config exists on every node without a per-node
%%% call. Placed after Plumtree + HyParView in the tree (the replica
%%% subscribes to both).
-module(barrel_p2p_map_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([start_map/2, stop_map/1, which_maps/0]).
-export([init/1]).
-define(SERVER, ?MODULE).
-define(TAB, barrel_p2p_maps).
%%====================================================================
%% API
%%====================================================================
start_link() ->
case supervisor:start_link({local, ?SERVER}, ?MODULE, []) of
{ok, Pid} ->
%% The supervisor is up now, so start_child works. Best-effort:
%% a bad declared map is logged, not fatal to boot.
start_declared_maps(),
{ok, Pid};
Other ->
Other
end.
%% @doc Start (or return the existing) named map. Idempotent.
-spec start_map(atom(), barrel_p2p_map:opts()) -> {ok, pid()} | {error, term()}.
start_map(Name, Opts) when is_atom(Name) ->
case lookup(Name) of
{ok, Pid} ->
{ok, Pid};
not_found ->
case supervisor:start_child(?SERVER, [Name, Opts]) of
{ok, Pid} ->
ets:insert(?TAB, {Name, Pid}),
{ok, Pid};
{error, _} = Error ->
Error
end
end;
start_map(_Name, _Opts) ->
{error, invalid_map_name}.
%% @doc Stop the named map on this node.
-spec stop_map(atom()) -> ok.
stop_map(Name) ->
case lookup(Name) of
{ok, Pid} ->
ets:delete(?TAB, Name),
_ = supervisor:terminate_child(?SERVER, Pid),
ok;
not_found ->
ok
end.
%% @doc Names of the maps running on this node.
-spec which_maps() -> [atom()].
which_maps() ->
[N || {N, _Pid} <- ets:tab2list(?TAB)].
%%====================================================================
%% supervisor callback
%%====================================================================
init([]) ->
?TAB = ets:new(?TAB, [named_table, public, set, {read_concurrency, true}]),
SupFlags = #{strategy => simple_one_for_one, intensity => 10, period => 10},
Child = #{
id => map_instance,
start => {barrel_p2p_map_instance_sup, start_link, []},
restart => transient,
shutdown => infinity,
type => supervisor,
modules => [barrel_p2p_map_instance_sup]
},
{ok, {SupFlags, [Child]}}.
%%====================================================================
%% Internal
%%====================================================================
lookup(Name) ->
case ets:lookup(?TAB, Name) of
[{_, Pid}] ->
case is_process_alive(Pid) of
true ->
{ok, Pid};
false ->
ets:delete(?TAB, Name),
not_found
end;
[] ->
not_found
end.
start_declared_maps() ->
Declared = application:get_env(barrel_p2p, replicated_maps, []),
lists:foreach(
fun
({Name, Opts}) when is_atom(Name), is_map(Opts) ->
case start_map(Name, Opts) of
{ok, _} ->
ok;
{error, Reason} ->
logger:error(
"barrel_p2p_map_sup: declared map ~p "
"failed to start: ~p",
[Name, Reason]
)
end;
(Bad) ->
logger:error(
"barrel_p2p_map_sup: invalid replicated_maps "
"entry (want {atom(), map()}): ~p",
[Bad]
)
end,
Declared
).