Настройка репликации. Финал #14

This commit is contained in:
2026-05-03 21:02:29 +03:00
parent 3d61830f1c
commit 83ce92afa4
5 changed files with 148 additions and 93 deletions
+37 -50
View File
@@ -1,64 +1,51 @@
%% ===================================================================
%% EventHub Cluster Discovery (с репликацией, задача #14)
%% ===================================================================
-module(cluster_discovery).
-export([discover_and_replicate/0]).
%% ------------------------------------------------------------------
%% @doc Основная точка входа: запускает процесс обнаружения и репликации
%% ------------------------------------------------------------------
discover_and_replicate() ->
spawn(fun() ->
io:format("Cluster discovery started (mode: ~s)~n", [os:getenv("CLUSTER_MODE", "local")]),
discover_loop()
end).
-define(DNS_ALIAS, get_dns_name()).
-define(RETRY_INTERVAL, 5000).
%% ------------------------------------------------------------------
%% Внутренние функции
%% ------------------------------------------------------------------
discover_and_replicate() ->
io:format("Starting cluster DNS discovery via epmd (~s)...~n", [?DNS_ALIAS]),
discover_loop().
discover_loop() ->
% Получаем список IP-адресов через DNS
case inet_res:getbyname("eventhub-node", in, a) of
{ok, {hostent, _, _, in, 4, Ips}} ->
Nodes = [list_to_atom("eventhub@" ++ inet:ntoa(Ip)) || Ip <- Ips],
io:format("Discovered nodes: ~p~n", [Nodes]),
lists:foreach(fun(Node) ->
case net_kernel:connect_node(Node) of
true ->
io:format("Connected to ~p, joining Mnesia cluster...~n", [Node]),
join_and_replicate(Node);
false ->
io:format("Failed to connect to ~p~n", [Node])
case inet:getaddrs(?DNS_ALIAS, inet) of
{ok, IPs} when is_list(IPs) ->
lists:foreach(fun(IP) ->
IPStr = inet:ntoa(IP),
io:format("Checking epmd on ~s...~n", [IPStr]),
case erl_epmd:names(IP) of
{ok, List} ->
lists:foreach(fun({Name, _Port}) ->
Node = list_to_atom(Name ++ "@" ++ Name),
io:format(" Trying net_kernel:connect_node(~s)...~n", [Node]),
case net_kernel:connect_node(Node) of
true -> io:format(" *** Connected to ~s ***~n", [Node]), join_and_replicate(Node); %io:format(" *** Connected to ~s ***~n", [Node]);
false -> io:format(" *** Failed to connect to ~s ***~n", [Node]);
ignored -> ok
end
end, List);
{error, Reason} ->
io:format(" epmd error on ~s: ~p~n", [IPStr, Reason])
end
end, Nodes);
end, IPs);
{error, Reason} ->
io:format("DNS lookup failed: ~p, retrying in 5s...~n", [Reason]),
timer:sleep(5000),
discover_loop()
end.
io:format("DNS lookup failed (~p), retrying...~n", [Reason])
end,
timer:sleep(?RETRY_INTERVAL),
discover_loop().
%% ------------------------------------------------------------------
%% @doc Добавляет удалённый узел в Mnesia и реплицирует данные
%% ------------------------------------------------------------------
join_and_replicate(Node) ->
% Добавляем узел в список extra_db_nodes, чтобы Mnesia знала о нём
ExtraNodes = application:get_env(eventhub, extra_db_nodes, []),
application:set_env(eventhub, extra_db_nodes, [Node | ExtraNodes]),
case lists:member(Node, mnesia:system_info(db_nodes)) of
true ->
io:format("Node ~p already in Mnesia cluster, skipping~n", [Node]);
false ->
io:format("Adding node ~p to Mnesia cluster...~n", [Node]),
infra_mnesia:add_cluster_nodes([Node])
end.
% Подключаем узел к кластеру Mnesia
mnesia:change_config(extra_db_nodes, [Node]),
% Реплицируем все пользовательские таблицы на себя
Tables = mnesia:system_info(tables) -- [schema],
lists:foreach(fun(Tab) ->
case lists:member(node(), mnesia:table_info(Tab, disc_copies)) of
false ->
io:format("Adding local disc copy of table ~p...~n", [Tab]),
mnesia:add_table_copy(Tab, node(), disc_copies);
true ->
ok
end
end, Tables),
ok.
get_dns_name() ->
os:getenv("DNS_NAME", "eventhub-node"). % значение по умолчанию оставим
+59 -21
View File
@@ -1,5 +1,5 @@
%% ===================================================================
%% EventHub infra_mnesia (финальная рабочая версия, задача #14)
%% EventHub infra_mnesia (финальная версия с автоочисткой кластера)
%% ===================================================================
-module(infra_mnesia).
-behaviour(gen_server).
@@ -7,6 +7,7 @@
-include("records.hrl").
-export([start_link/0, init_tables/0, wait_for_tables/0]).
-export([add_cluster_nodes/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
@@ -20,10 +21,9 @@
admin_audit, notification
]).
%% Таблицы, которые должны иметь дисковые копии на каждом узле
-define(DISC_TABLES, ?TABLES -- [session, admin_session]).
-define(TABLE_WAIT_TIMEOUT, 5000).
-define(CLEANUP_INTERVAL, 30000). % 30 секунд
%% ===================================================================
%% API
@@ -38,6 +38,9 @@ init_tables() ->
wait_for_tables() ->
gen_server:call(?MODULE, wait_for_tables).
add_cluster_nodes(Nodes) ->
gen_server:call(?MODULE, {add_nodes, Nodes}).
%% ===================================================================
%% gen_server callbacks
%% ===================================================================
@@ -55,6 +58,11 @@ handle_call(init_tables, _From, State) ->
end,
lists:foreach(fun create_table/1, ?TABLES),
ok = create_indices(),
ok = start_cleanup_timer(),
{reply, ok, State};
handle_call({add_nodes, Nodes}, _From, State) ->
ok = do_add_nodes(Nodes),
{reply, ok, State};
handle_call(wait_for_tables, _From, State) ->
@@ -62,12 +70,17 @@ handle_call(wait_for_tables, _From, State) ->
{reply, ok, State}.
handle_cast(_Msg, State) -> {noreply, State}.
handle_info({cleanup}, State) ->
prune_dead_nodes(),
erlang:send_after(?CLEANUP_INTERVAL, self(), {cleanup}),
{noreply, State};
handle_info(_Info, State) -> {noreply, State}.
terminate(_Reason, _State) -> ok.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
%% ===================================================================
%% Локальное создание схемы (только если нет extra_db_nodes)
%% Управление схемой
%% ===================================================================
maybe_recreate_schema() ->
@@ -88,10 +101,6 @@ maybe_recreate_schema() ->
end
end.
%% ===================================================================
%% Присоединение к кластеру – добавляет локальные дисковые копии таблиц
%% ===================================================================
join_cluster(Nodes) ->
case mnesia:system_info(is_running) of
yes -> mnesia:stop();
@@ -100,22 +109,16 @@ join_cluster(Nodes) ->
application:set_env(mnesia, extra_db_nodes, Nodes),
mnesia:start(),
ensure_schema_disc(),
% Дожидаемся появления всех таблиц в локальной схеме
wait_for_tables_available(),
lists:foreach(fun add_local_disc_copy/1, ?DISC_TABLES).
wait_for_tables_available() ->
lists:foreach(fun(Tab) ->
wait_for_table(Tab)
end, ?DISC_TABLES).
wait_for_table(Tab) ->
case lists:member(Tab, mnesia:system_info(tables)) of
true -> ok;
false ->
timer:sleep(100),
wait_for_table(Tab)
end.
do_add_nodes(Nodes) ->
ExtraNodes = application:get_env(eventhub, extra_db_nodes, []),
application:set_env(eventhub, extra_db_nodes, Nodes ++ ExtraNodes),
{ok, _} = mnesia:change_config(extra_db_nodes, Nodes),
ensure_schema_disc(),
wait_for_tables_available(),
lists:foreach(fun add_local_disc_copy/1, ?DISC_TABLES).
ensure_schema_disc() ->
case lists:member(node(), mnesia:table_info(schema, disc_copies)) of
@@ -142,6 +145,41 @@ add_local_disc_copy(Tab) ->
true -> ok
end.
wait_for_tables_available() ->
lists:foreach(fun(Tab) -> wait_for_table(Tab) end, ?DISC_TABLES).
wait_for_table(Tab) ->
case lists:member(Tab, mnesia:system_info(tables)) of
true -> ok;
false ->
timer:sleep(100),
wait_for_table(Tab)
end.
%% ===================================================================
%% Автоматическая очистка мёртвых узлов
%% ===================================================================
start_cleanup_timer() ->
erlang:send_after(?CLEANUP_INTERVAL, self(), {cleanup}),
ok.
prune_dead_nodes() ->
AliveNodes = lists:filter(fun(Node) ->
Node =/= node() andalso net_adm:ping(Node) =:= pong
end, mnesia:system_info(db_nodes)),
DeadNodes = mnesia:system_info(db_nodes) -- [node() | AliveNodes],
lists:foreach(fun(Node) ->
io:format("Removing dead node ~p from Mnesia schema...~n", [Node]),
lists:foreach(fun(Tab) ->
case lists:member(Node, mnesia:table_info(Tab, disc_copies)) of
true -> catch mnesia:del_table_copy(Tab, Node);
false -> ok
end
end, ?DISC_TABLES),
catch mnesia:del_table_copy(schema, Node)
end, DeadNodes).
%% ===================================================================
%% Создание / открытие таблиц
%% ===================================================================