Mnesia migrations with global lock on startup. Refs EventHub/EventHubBack#24
This commit is contained in:
+270
-272
@@ -1,273 +1,271 @@
|
||||
%% ===================================================================
|
||||
%% EventHub – infra_mnesia (финальная версия с автоочисткой кластера)
|
||||
%% ===================================================================
|
||||
-module(infra_mnesia).
|
||||
-behaviour(gen_server).
|
||||
|
||||
-include("records.hrl").
|
||||
|
||||
-export([start_link/0, init_tables/0, wait_for_tables/0, wait_for_table/1]).
|
||||
-export([add_cluster_nodes/1]).
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||
terminate/2, code_change/3]).
|
||||
|
||||
-define(TABLES, [
|
||||
user, session, verification, admin, admin_session, auth_session,
|
||||
calendar, calendar_share, calendar_specialist,
|
||||
event, recurrence_exception,
|
||||
booking,
|
||||
review, report, banned_word,
|
||||
ticket, subscription,
|
||||
admin_audit, notification,
|
||||
stats, node_metric, schema_migration
|
||||
]).
|
||||
|
||||
-define(DISC_TABLES, ?TABLES -- [session, verification, admin_session, node_metric]).
|
||||
-define(TABLE_WAIT_TIMEOUT, 5000).
|
||||
-define(CLEANUP_INTERVAL, 30000). % 30 секунд
|
||||
|
||||
%% ===================================================================
|
||||
%% API
|
||||
%% ===================================================================
|
||||
|
||||
start_link() ->
|
||||
% Счётчики для метрик (сессии, ws-соединения)
|
||||
ets:new(eventhub_counters, [named_table, public, set, {write_concurrency, true}]),
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
init_tables() ->
|
||||
gen_server:call(?MODULE, 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
|
||||
%% ===================================================================
|
||||
|
||||
init([]) ->
|
||||
{ok, #{}}.
|
||||
|
||||
handle_call(init_tables, _From, State) ->
|
||||
ExtraNodes = application:get_env(eventhub, extra_db_nodes, []),
|
||||
case ExtraNodes of
|
||||
[] ->
|
||||
ok = maybe_recreate_schema();
|
||||
%% ok = migration_engine:init_migrations_table(),
|
||||
%% _ = migration_engine:apply_pending(); //todo выключил - обваливает кластер, нужно разбираться
|
||||
_ ->
|
||||
ok = join_cluster(ExtraNodes)
|
||||
end,
|
||||
lists:foreach(fun create_table/1, ?TABLES),
|
||||
% Принудительное создание node_metric на каждом узле
|
||||
case mnesia:create_table(node_metric, [
|
||||
{disc_copies, [node()]}, % хранить на диске
|
||||
{local_content, true}, % не реплицировать
|
||||
{attributes, record_info(fields, node_metric)}
|
||||
]) of
|
||||
{atomic, ok} -> ok;
|
||||
{aborted, {already_exists, node_metric}} -> ok;
|
||||
_ -> ok
|
||||
end,
|
||||
% ГАРАНТИРУЕМ, что узел имеет локальную копию node_metric (критично для присоединяющихся узлов)
|
||||
case lists:member(node(), mnesia:table_info(node_metric, disc_copies) ++
|
||||
mnesia:table_info(node_metric, ram_copies)) of
|
||||
false -> mnesia:add_table_copy(node_metric, node(), disc_copies);
|
||||
true -> ok
|
||||
end,
|
||||
ok = create_indices(),
|
||||
ok = stats_collector:subscribe(),
|
||||
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) ->
|
||||
mnesia:wait_for_tables(?TABLES, ?TABLE_WAIT_TIMEOUT),
|
||||
{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}.
|
||||
|
||||
%% ===================================================================
|
||||
%% Управление схемой
|
||||
%% ===================================================================
|
||||
|
||||
maybe_recreate_schema() ->
|
||||
MnesiaDir = mnesia:system_info(directory),
|
||||
case filelib:is_dir(MnesiaDir) of
|
||||
false ->
|
||||
io:format("Mnesia directory (~s) not found. Creating fresh schema...~n", [MnesiaDir]),
|
||||
mnesia:stop(),
|
||||
mnesia:delete_schema([node()]),
|
||||
mnesia:create_schema([node()]),
|
||||
mnesia:start(),
|
||||
ok;
|
||||
true ->
|
||||
io:format("Mnesia directory exists (~s). Reusing existing schema.~n", [MnesiaDir]),
|
||||
case mnesia:system_info(is_running) of
|
||||
yes -> ok;
|
||||
_ -> mnesia:start()
|
||||
end
|
||||
end.
|
||||
|
||||
join_cluster(Nodes) ->
|
||||
case mnesia:system_info(is_running) of
|
||||
yes -> mnesia:stop();
|
||||
no -> ok
|
||||
end,
|
||||
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).
|
||||
|
||||
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
|
||||
false ->
|
||||
io:format("Changing schema copy to disc...~n"),
|
||||
case mnesia:change_table_copy_type(schema, node(), disc_copies) of
|
||||
{atomic, ok} -> ok;
|
||||
{aborted, {already_exists, _, _}} -> ok;
|
||||
{aborted, Reason} -> error({failed_schema_disc, Reason})
|
||||
end;
|
||||
true -> ok
|
||||
end.
|
||||
|
||||
add_local_disc_copy(Tab) ->
|
||||
case lists:member(node(), mnesia:table_info(Tab, disc_copies)) of
|
||||
false ->
|
||||
io:format("Adding local disc copy of table ~p...~n", [Tab]),
|
||||
case mnesia:add_table_copy(Tab, node(), disc_copies) of
|
||||
{atomic, ok} -> ok;
|
||||
{aborted, {already_exists, _}} -> ok;
|
||||
{aborted, Reason} ->
|
||||
io:format("Could not add disc copy for ~p: ~p~n", [Tab, Reason])
|
||||
end;
|
||||
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).
|
||||
|
||||
%% ===================================================================
|
||||
%% Создание / открытие таблиц
|
||||
%% ===================================================================
|
||||
|
||||
create_table(Table) ->
|
||||
Opts = table_opts(Table),
|
||||
case mnesia:create_table(Table, Opts) of
|
||||
{atomic, ok} -> ok;
|
||||
{aborted, {already_exists, _}} -> ok;
|
||||
{aborted, Reason} ->
|
||||
error({table_creation_failed, Table, Reason})
|
||||
end.
|
||||
|
||||
%% ===================================================================
|
||||
%% Опции хранения таблиц
|
||||
%% ===================================================================
|
||||
|
||||
table_opts(user) -> [{disc_copies, [node()]}, {attributes, record_info(fields, user)}];
|
||||
table_opts(admin) -> [{disc_copies, [node()]}, {attributes, record_info(fields, admin)}];
|
||||
table_opts(calendar) -> [{disc_copies, [node()]}, {attributes, record_info(fields, calendar)}];
|
||||
table_opts(calendar_share) -> [{disc_copies, [node()]}, {attributes, record_info(fields, calendar_share)}];
|
||||
table_opts(calendar_specialist) -> [{disc_copies, [node()]}, {attributes, record_info(fields, calendar_specialist)}];
|
||||
table_opts(event) -> [{disc_copies, [node()]}, {attributes, record_info(fields, event)}];
|
||||
table_opts(recurrence_exception) -> [{disc_copies, [node()]}, {attributes, record_info(fields, recurrence_exception)}];
|
||||
table_opts(booking) -> [{disc_copies, [node()]}, {attributes, record_info(fields, booking)}];
|
||||
table_opts(review) -> [{disc_copies, [node()]}, {attributes, record_info(fields, review)}];
|
||||
table_opts(report) -> [{disc_copies, [node()]}, {attributes, record_info(fields, report)}];
|
||||
table_opts(banned_word) -> [{disc_copies, [node()]}, {attributes, record_info(fields, banned_word)}];
|
||||
table_opts(ticket) -> [{disc_copies, [node()]}, {attributes, record_info(fields, ticket)}];
|
||||
table_opts(subscription) -> [{disc_copies, [node()]}, {attributes, record_info(fields, subscription)}];
|
||||
table_opts(admin_audit) -> [{disc_copies, [node()]}, {attributes, record_info(fields, admin_audit)}];
|
||||
table_opts(notification) -> [{disc_copies, [node()]}, {attributes, record_info(fields, notification)}];
|
||||
table_opts(stats) -> [{disc_copies, [node()]}, {attributes, record_info(fields, stats)}];
|
||||
table_opts(schema_migration) -> [{disc_copies, [node()]}, {attributes, record_info(fields, schema_migration)}];
|
||||
table_opts(session) -> [{ram_copies, [node()]}, {attributes, record_info(fields, session)}];
|
||||
table_opts(verification) -> [{ram_copies, [node()]}, {attributes, record_info(fields, verification)}];
|
||||
table_opts(admin_session) -> [{ram_copies, [node()]}, {attributes, record_info(fields, admin_session)}];
|
||||
table_opts(auth_session) -> [{disc_copies, [node()]}, {attributes, record_info(fields, auth_session)}];
|
||||
table_opts(node_metric) -> [{disc_copies, [node()]}, {local_content, true}, {attributes, record_info(fields, node_metric)}].
|
||||
|
||||
%% ===================================================================
|
||||
%% Индексы
|
||||
%% ===================================================================
|
||||
|
||||
create_indices() ->
|
||||
mnesia:add_table_index(event, calendar_id),
|
||||
mnesia:add_table_index(event, title),
|
||||
mnesia:add_table_index(event, created_at),
|
||||
mnesia:add_table_index(event, start_time),
|
||||
mnesia:add_table_index(event, event_type),
|
||||
mnesia:add_table_index(event, master_id),
|
||||
mnesia:add_table_index(event, specialist_id),
|
||||
mnesia:add_table_index(event, status),
|
||||
mnesia:add_table_index(booking, event_id),
|
||||
mnesia:add_table_index(booking, user_id),
|
||||
mnesia:add_table_index(booking, status),
|
||||
mnesia:add_table_index(calendar, owner_id),
|
||||
mnesia:add_table_index(calendar, status),
|
||||
mnesia:add_table_index(calendar, short_name),
|
||||
mnesia:add_table_index(calendar, category),
|
||||
mnesia:add_table_index(calendar_specialist, calendar_id),
|
||||
mnesia:add_table_index(calendar_specialist, user_id),
|
||||
mnesia:add_table_index(user, nickname),
|
||||
mnesia:add_table_index(user, email),
|
||||
mnesia:add_table_index(notification, user_id),
|
||||
mnesia:add_table_index(notification, is_read),
|
||||
mnesia:add_table_index(auth_session, family_id),
|
||||
mnesia:add_table_index(auth_session, subject_id),
|
||||
%% ===================================================================
|
||||
%% EventHub – infra_mnesia (финальная версия с автоочисткой кластера)
|
||||
%% ===================================================================
|
||||
-module(infra_mnesia).
|
||||
-behaviour(gen_server).
|
||||
|
||||
-include("records.hrl").
|
||||
|
||||
-export([start_link/0, init_tables/0, wait_for_tables/0, wait_for_table/1]).
|
||||
-export([add_cluster_nodes/1]).
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||
terminate/2, code_change/3]).
|
||||
|
||||
-define(TABLES, [
|
||||
user, session, verification, admin, admin_session, auth_session,
|
||||
calendar, calendar_share, calendar_specialist,
|
||||
event, recurrence_exception,
|
||||
booking,
|
||||
review, report, banned_word,
|
||||
ticket, subscription,
|
||||
admin_audit, notification,
|
||||
stats, node_metric, schema_migration
|
||||
]).
|
||||
|
||||
-define(DISC_TABLES, ?TABLES -- [session, verification, admin_session, node_metric]).
|
||||
-define(TABLE_WAIT_TIMEOUT, 5000).
|
||||
-define(CLEANUP_INTERVAL, 30000). % 30 секунд
|
||||
|
||||
%% ===================================================================
|
||||
%% API
|
||||
%% ===================================================================
|
||||
|
||||
start_link() ->
|
||||
% Счётчики для метрик (сессии, ws-соединения)
|
||||
ets:new(eventhub_counters, [named_table, public, set, {write_concurrency, true}]),
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
init_tables() ->
|
||||
gen_server:call(?MODULE, 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
|
||||
%% ===================================================================
|
||||
|
||||
init([]) ->
|
||||
{ok, #{}}.
|
||||
|
||||
handle_call(init_tables, _From, State) ->
|
||||
ExtraNodes = application:get_env(eventhub, extra_db_nodes, []),
|
||||
case ExtraNodes of
|
||||
[] ->
|
||||
ok = maybe_recreate_schema();
|
||||
_ ->
|
||||
ok = join_cluster(ExtraNodes)
|
||||
end,
|
||||
lists:foreach(fun create_table/1, ?TABLES),
|
||||
% Принудительное создание node_metric на каждом узле
|
||||
case mnesia:create_table(node_metric, [
|
||||
{disc_copies, [node()]}, % хранить на диске
|
||||
{local_content, true}, % не реплицировать
|
||||
{attributes, record_info(fields, node_metric)}
|
||||
]) of
|
||||
{atomic, ok} -> ok;
|
||||
{aborted, {already_exists, node_metric}} -> ok;
|
||||
_ -> ok
|
||||
end,
|
||||
% ГАРАНТИРУЕМ, что узел имеет локальную копию node_metric (критично для присоединяющихся узлов)
|
||||
case lists:member(node(), mnesia:table_info(node_metric, disc_copies) ++
|
||||
mnesia:table_info(node_metric, ram_copies)) of
|
||||
false -> mnesia:add_table_copy(node_metric, node(), disc_copies);
|
||||
true -> ok
|
||||
end,
|
||||
ok = create_indices(),
|
||||
ok = stats_collector:subscribe(),
|
||||
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) ->
|
||||
mnesia:wait_for_tables(?TABLES, ?TABLE_WAIT_TIMEOUT),
|
||||
{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}.
|
||||
|
||||
%% ===================================================================
|
||||
%% Управление схемой
|
||||
%% ===================================================================
|
||||
|
||||
maybe_recreate_schema() ->
|
||||
MnesiaDir = mnesia:system_info(directory),
|
||||
case filelib:is_dir(MnesiaDir) of
|
||||
false ->
|
||||
io:format("Mnesia directory (~s) not found. Creating fresh schema...~n", [MnesiaDir]),
|
||||
mnesia:stop(),
|
||||
mnesia:delete_schema([node()]),
|
||||
mnesia:create_schema([node()]),
|
||||
mnesia:start(),
|
||||
ok;
|
||||
true ->
|
||||
io:format("Mnesia directory exists (~s). Reusing existing schema.~n", [MnesiaDir]),
|
||||
case mnesia:system_info(is_running) of
|
||||
yes -> ok;
|
||||
_ -> mnesia:start()
|
||||
end
|
||||
end.
|
||||
|
||||
join_cluster(Nodes) ->
|
||||
case mnesia:system_info(is_running) of
|
||||
yes -> mnesia:stop();
|
||||
no -> ok
|
||||
end,
|
||||
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).
|
||||
|
||||
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
|
||||
false ->
|
||||
io:format("Changing schema copy to disc...~n"),
|
||||
case mnesia:change_table_copy_type(schema, node(), disc_copies) of
|
||||
{atomic, ok} -> ok;
|
||||
{aborted, {already_exists, _, _}} -> ok;
|
||||
{aborted, Reason} -> error({failed_schema_disc, Reason})
|
||||
end;
|
||||
true -> ok
|
||||
end.
|
||||
|
||||
add_local_disc_copy(Tab) ->
|
||||
case lists:member(node(), mnesia:table_info(Tab, disc_copies)) of
|
||||
false ->
|
||||
io:format("Adding local disc copy of table ~p...~n", [Tab]),
|
||||
case mnesia:add_table_copy(Tab, node(), disc_copies) of
|
||||
{atomic, ok} -> ok;
|
||||
{aborted, {already_exists, _}} -> ok;
|
||||
{aborted, Reason} ->
|
||||
io:format("Could not add disc copy for ~p: ~p~n", [Tab, Reason])
|
||||
end;
|
||||
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).
|
||||
|
||||
%% ===================================================================
|
||||
%% Создание / открытие таблиц
|
||||
%% ===================================================================
|
||||
|
||||
create_table(Table) ->
|
||||
Opts = table_opts(Table),
|
||||
case mnesia:create_table(Table, Opts) of
|
||||
{atomic, ok} -> ok;
|
||||
{aborted, {already_exists, _}} -> ok;
|
||||
{aborted, Reason} ->
|
||||
error({table_creation_failed, Table, Reason})
|
||||
end.
|
||||
|
||||
%% ===================================================================
|
||||
%% Опции хранения таблиц
|
||||
%% ===================================================================
|
||||
|
||||
table_opts(user) -> [{disc_copies, [node()]}, {attributes, record_info(fields, user)}];
|
||||
table_opts(admin) -> [{disc_copies, [node()]}, {attributes, record_info(fields, admin)}];
|
||||
table_opts(calendar) -> [{disc_copies, [node()]}, {attributes, record_info(fields, calendar)}];
|
||||
table_opts(calendar_share) -> [{disc_copies, [node()]}, {attributes, record_info(fields, calendar_share)}];
|
||||
table_opts(calendar_specialist) -> [{disc_copies, [node()]}, {attributes, record_info(fields, calendar_specialist)}];
|
||||
table_opts(event) -> [{disc_copies, [node()]}, {attributes, record_info(fields, event)}];
|
||||
table_opts(recurrence_exception) -> [{disc_copies, [node()]}, {attributes, record_info(fields, recurrence_exception)}];
|
||||
table_opts(booking) -> [{disc_copies, [node()]}, {attributes, record_info(fields, booking)}];
|
||||
table_opts(review) -> [{disc_copies, [node()]}, {attributes, record_info(fields, review)}];
|
||||
table_opts(report) -> [{disc_copies, [node()]}, {attributes, record_info(fields, report)}];
|
||||
table_opts(banned_word) -> [{disc_copies, [node()]}, {attributes, record_info(fields, banned_word)}];
|
||||
table_opts(ticket) -> [{disc_copies, [node()]}, {attributes, record_info(fields, ticket)}];
|
||||
table_opts(subscription) -> [{disc_copies, [node()]}, {attributes, record_info(fields, subscription)}];
|
||||
table_opts(admin_audit) -> [{disc_copies, [node()]}, {attributes, record_info(fields, admin_audit)}];
|
||||
table_opts(notification) -> [{disc_copies, [node()]}, {attributes, record_info(fields, notification)}];
|
||||
table_opts(stats) -> [{disc_copies, [node()]}, {attributes, record_info(fields, stats)}];
|
||||
table_opts(schema_migration) -> [{disc_copies, [node()]}, {attributes, record_info(fields, schema_migration)}];
|
||||
table_opts(session) -> [{ram_copies, [node()]}, {attributes, record_info(fields, session)}];
|
||||
table_opts(verification) -> [{ram_copies, [node()]}, {attributes, record_info(fields, verification)}];
|
||||
table_opts(admin_session) -> [{ram_copies, [node()]}, {attributes, record_info(fields, admin_session)}];
|
||||
table_opts(auth_session) -> [{disc_copies, [node()]}, {attributes, record_info(fields, auth_session)}];
|
||||
table_opts(node_metric) -> [{disc_copies, [node()]}, {local_content, true}, {attributes, record_info(fields, node_metric)}].
|
||||
|
||||
%% ===================================================================
|
||||
%% Индексы
|
||||
%% ===================================================================
|
||||
|
||||
create_indices() ->
|
||||
mnesia:add_table_index(event, calendar_id),
|
||||
mnesia:add_table_index(event, title),
|
||||
mnesia:add_table_index(event, created_at),
|
||||
mnesia:add_table_index(event, start_time),
|
||||
mnesia:add_table_index(event, event_type),
|
||||
mnesia:add_table_index(event, master_id),
|
||||
mnesia:add_table_index(event, specialist_id),
|
||||
mnesia:add_table_index(event, status),
|
||||
mnesia:add_table_index(booking, event_id),
|
||||
mnesia:add_table_index(booking, user_id),
|
||||
mnesia:add_table_index(booking, status),
|
||||
mnesia:add_table_index(calendar, owner_id),
|
||||
mnesia:add_table_index(calendar, status),
|
||||
mnesia:add_table_index(calendar, short_name),
|
||||
mnesia:add_table_index(calendar, category),
|
||||
mnesia:add_table_index(calendar_specialist, calendar_id),
|
||||
mnesia:add_table_index(calendar_specialist, user_id),
|
||||
mnesia:add_table_index(user, nickname),
|
||||
mnesia:add_table_index(user, email),
|
||||
mnesia:add_table_index(notification, user_id),
|
||||
mnesia:add_table_index(notification, is_read),
|
||||
mnesia:add_table_index(auth_session, family_id),
|
||||
mnesia:add_table_index(auth_session, subject_id),
|
||||
ok.
|
||||
+198
-121
@@ -1,121 +1,198 @@
|
||||
-module(migration_engine).
|
||||
-behaviour(gen_server).
|
||||
|
||||
-include("records.hrl").
|
||||
|
||||
%% API
|
||||
-export([start_link/0, init_migrations_table/0, apply_pending/0,
|
||||
rollback/1, status/0]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||
terminate/2, code_change/3]).
|
||||
|
||||
-define(TABLE, schema_migration).
|
||||
|
||||
%% ------------------------------
|
||||
%% API
|
||||
%% ------------------------------
|
||||
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
init_migrations_table() ->
|
||||
gen_server:call(?MODULE, init_table).
|
||||
|
||||
apply_pending() ->
|
||||
gen_server:call(?MODULE, apply_pending).
|
||||
|
||||
rollback(Version) ->
|
||||
gen_server:call(?MODULE, {rollback, Version}).
|
||||
|
||||
status() ->
|
||||
gen_server:call(?MODULE, status).
|
||||
|
||||
%% ------------------------------
|
||||
%% gen_server callbacks
|
||||
%% ------------------------------
|
||||
|
||||
init([]) ->
|
||||
{ok, #{}}.
|
||||
|
||||
handle_call(init_table, _From, State) ->
|
||||
case lists:member(?TABLE, mnesia:system_info(tables)) of
|
||||
true -> ok;
|
||||
false ->
|
||||
mnesia:create_table(?TABLE, [
|
||||
{disc_copies, [node()]},
|
||||
{attributes, record_info(fields, schema_migration)},
|
||||
{type, set}
|
||||
])
|
||||
end,
|
||||
infra_mnesia:wait_for_table(?TABLE),
|
||||
{reply, ok, State};
|
||||
|
||||
handle_call(apply_pending, _From, State) ->
|
||||
Result = do_apply_pending(),
|
||||
{reply, Result, State};
|
||||
|
||||
handle_call({rollback, Version}, _From, State) ->
|
||||
Result = do_rollback(Version),
|
||||
{reply, Result, State};
|
||||
|
||||
handle_call(status, _From, State) ->
|
||||
Applied = applied_versions(),
|
||||
Pending = pending_versions() -- Applied,
|
||||
{reply, #{applied => lists:map(fun atom_to_list/1, Applied),
|
||||
pending => lists:map(fun atom_to_list/1, Pending)}, State}.
|
||||
|
||||
handle_cast(_Msg, State) -> {noreply, State}.
|
||||
handle_info(_Msg, State) -> {noreply, State}.
|
||||
terminate(_Reason, _State) -> ok.
|
||||
code_change(_OldVsn, State, _Extra) -> {ok, State}.
|
||||
|
||||
%% ------------------------------
|
||||
%% Внутренняя логика
|
||||
%% ------------------------------
|
||||
|
||||
do_apply_pending() ->
|
||||
Pending = pending_versions() -- applied_versions(),
|
||||
lists:foreach(fun(Module) -> code:ensure_loaded(Module) end, Pending),
|
||||
lists:foldl(fun(Version, Acc) ->
|
||||
try Version:up() of
|
||||
_ -> mark_applied(Version), Acc
|
||||
catch _:Reason ->
|
||||
[{error, atom_to_list(Version), Reason} | Acc]
|
||||
end
|
||||
end, [], Pending).
|
||||
|
||||
do_rollback(TargetStr) ->
|
||||
Target = list_to_atom(TargetStr),
|
||||
Applied = applied_versions(),
|
||||
ToRollback = lists:sort(fun(A,B) -> A > B end,
|
||||
[V || V <- Applied, V > Target]),
|
||||
lists:foreach(fun(Version) ->
|
||||
code:ensure_loaded(Version),
|
||||
try Version:down() of
|
||||
_ -> unmark_applied(Version)
|
||||
catch _:Reason ->
|
||||
throw({rollback_failed, atom_to_list(Version), Reason})
|
||||
end
|
||||
end, ToRollback).
|
||||
|
||||
applied_versions() ->
|
||||
[list_to_atom(V) || #schema_migration{version = V} <-
|
||||
mnesia:dirty_match_object(#schema_migration{_ = '_'})].
|
||||
|
||||
pending_versions() ->
|
||||
AllMods = code:all_available(),
|
||||
[list_to_atom(Module) || Module <- extract_module_names(AllMods), lists:prefix("20", Module)].
|
||||
|
||||
extract_module_names(ModInfoList) ->
|
||||
[Name || {Name, _, _} <- ModInfoList].
|
||||
|
||||
mark_applied(Version) ->
|
||||
mnesia:dirty_write(#schema_migration{
|
||||
version = atom_to_list(Version),
|
||||
applied_at = calendar:local_time()
|
||||
}).
|
||||
|
||||
unmark_applied(Version) ->
|
||||
mnesia:dirty_delete({?TABLE, atom_to_list(Version)}).
|
||||
-module(migration_engine).
|
||||
-behaviour(gen_server).
|
||||
|
||||
-include("records.hrl").
|
||||
|
||||
%% API
|
||||
-export([start_link/0, ensure_applied/0, apply_pending/0,
|
||||
rollback/1, status/0]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||
terminate/2, code_change/3]).
|
||||
|
||||
-define(TABLE, schema_migration).
|
||||
-define(LOCK_VERSION, "__migration_lock__").
|
||||
-define(LOCK_STALE_SECONDS, 300).
|
||||
-define(WAIT_TIMEOUT_MS, 120000).
|
||||
-define(WAIT_INTERVAL_MS, 200).
|
||||
|
||||
%% Упорядоченный реестр миграций (добавлять новые модули в конец списка).
|
||||
-define(ALL_MIGRATIONS, [
|
||||
'20260501120000_base_schema',
|
||||
'20260504150000_test_migration'
|
||||
]).
|
||||
|
||||
%% ------------------------------
|
||||
%% API
|
||||
%% ------------------------------
|
||||
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
%% @doc Безопасно применить pending-миграции при старте (с global lock в Mnesia).
|
||||
-spec ensure_applied() -> ok | {error, term()}.
|
||||
ensure_applied() ->
|
||||
gen_server:call(?MODULE, ensure_applied, infinity).
|
||||
|
||||
apply_pending() ->
|
||||
gen_server:call(?MODULE, apply_pending).
|
||||
|
||||
rollback(Version) ->
|
||||
gen_server:call(?MODULE, {rollback, Version}).
|
||||
|
||||
status() ->
|
||||
gen_server:call(?MODULE, status).
|
||||
|
||||
%% ------------------------------
|
||||
%% gen_server callbacks
|
||||
%% ------------------------------
|
||||
|
||||
init([]) ->
|
||||
{ok, #{}}.
|
||||
|
||||
handle_call(ensure_applied, _From, State) ->
|
||||
Result = do_ensure_applied(),
|
||||
{reply, Result, State};
|
||||
|
||||
handle_call(apply_pending, _From, State) ->
|
||||
Result = do_apply_pending(),
|
||||
{reply, Result, State};
|
||||
|
||||
handle_call({rollback, Version}, _From, State) ->
|
||||
Result = do_rollback(Version),
|
||||
{reply, Result, State};
|
||||
|
||||
handle_call(status, _From, State) ->
|
||||
Applied = applied_versions(),
|
||||
Pending = pending_versions() -- Applied,
|
||||
{reply, #{applied => [atom_to_list(V) || V <- Applied],
|
||||
pending => [atom_to_list(V) || V <- Pending]}, State}.
|
||||
|
||||
handle_cast(_Msg, State) -> {noreply, State}.
|
||||
handle_info(_Msg, State) -> {noreply, State}.
|
||||
terminate(_Reason, _State) -> ok.
|
||||
code_change(_OldVsn, State, _Extra) -> {ok, State}.
|
||||
|
||||
%% ------------------------------
|
||||
%% Координация при старте
|
||||
%% ------------------------------
|
||||
|
||||
do_ensure_applied() ->
|
||||
case all_applied() of
|
||||
true -> ok;
|
||||
false ->
|
||||
case acquire_lock() of
|
||||
ok ->
|
||||
try
|
||||
case do_apply_pending() of
|
||||
ok -> ok;
|
||||
{error, Reason} -> {error, Reason}
|
||||
end
|
||||
after
|
||||
release_lock()
|
||||
end;
|
||||
{error, locked} ->
|
||||
wait_until_applied(?WAIT_TIMEOUT_MS)
|
||||
end
|
||||
end.
|
||||
|
||||
all_applied() ->
|
||||
pending_versions() -- applied_versions() =:= [].
|
||||
|
||||
wait_until_applied(TimeoutMs) when TimeoutMs =< 0 ->
|
||||
{error, migration_timeout};
|
||||
wait_until_applied(TimeoutMs) ->
|
||||
case all_applied() of
|
||||
true -> ok;
|
||||
false ->
|
||||
timer:sleep(?WAIT_INTERVAL_MS),
|
||||
wait_until_applied(TimeoutMs - ?WAIT_INTERVAL_MS)
|
||||
end.
|
||||
|
||||
acquire_lock() ->
|
||||
Now = calendar:universal_time(),
|
||||
case mnesia:sync_transaction(fun() -> try_acquire_lock(Now) end) of
|
||||
{atomic, ok} -> ok;
|
||||
{aborted, locked} -> {error, locked};
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
try_acquire_lock(Now) ->
|
||||
case mnesia:read(?TABLE, ?LOCK_VERSION, read) of
|
||||
[] ->
|
||||
mnesia:write(#schema_migration{version = ?LOCK_VERSION, applied_at = Now}),
|
||||
ok;
|
||||
[#schema_migration{applied_at = At}] ->
|
||||
case lock_stale(At, Now) of
|
||||
true ->
|
||||
mnesia:write(#schema_migration{version = ?LOCK_VERSION, applied_at = Now}),
|
||||
ok;
|
||||
false ->
|
||||
mnesia:abort(locked)
|
||||
end
|
||||
end.
|
||||
|
||||
lock_stale(At, Now) ->
|
||||
SecAt = calendar:datetime_to_gregorian_seconds(At),
|
||||
SecNow = calendar:datetime_to_gregorian_seconds(Now),
|
||||
SecNow - SecAt > ?LOCK_STALE_SECONDS.
|
||||
|
||||
release_lock() ->
|
||||
mnesia:sync_transaction(fun() ->
|
||||
mnesia:delete({?TABLE, ?LOCK_VERSION})
|
||||
end),
|
||||
ok.
|
||||
|
||||
%% ------------------------------
|
||||
%% Применение / откат
|
||||
%% ------------------------------
|
||||
|
||||
do_apply_pending() ->
|
||||
Pending = pending_versions() -- applied_versions(),
|
||||
lists:foreach(fun(Module) -> code:ensure_loaded(Module) end, Pending),
|
||||
lists:foldl(fun(Version, ok) ->
|
||||
try Version:up() of
|
||||
_ ->
|
||||
mark_applied(Version),
|
||||
ok
|
||||
catch
|
||||
_:Reason ->
|
||||
{error, {migration_failed, atom_to_list(Version), Reason}}
|
||||
end;
|
||||
(_, Acc) -> Acc
|
||||
end, ok, Pending).
|
||||
|
||||
do_rollback(TargetStr) ->
|
||||
Target = list_to_atom(TargetStr),
|
||||
Applied = applied_versions(),
|
||||
ToRollback = lists:sort(fun(A, B) -> A > B end,
|
||||
[V || V <- Applied, V > Target]),
|
||||
lists:foreach(fun(Version) ->
|
||||
code:ensure_loaded(Version),
|
||||
try Version:down() of
|
||||
_ -> unmark_applied(Version)
|
||||
catch _:Reason ->
|
||||
throw({rollback_failed, atom_to_list(Version), Reason})
|
||||
end
|
||||
end, ToRollback).
|
||||
|
||||
applied_versions() ->
|
||||
[list_to_atom(V) || #schema_migration{version = V} <-
|
||||
mnesia:dirty_match_object(#schema_migration{_ = '_'}),
|
||||
not is_lock_version(V)].
|
||||
|
||||
pending_versions() ->
|
||||
?ALL_MIGRATIONS.
|
||||
|
||||
is_lock_version(?LOCK_VERSION) -> true;
|
||||
is_lock_version(_) -> false.
|
||||
|
||||
mark_applied(Version) ->
|
||||
mnesia:dirty_write(#schema_migration{
|
||||
version = atom_to_list(Version),
|
||||
applied_at = calendar:universal_time()
|
||||
}).
|
||||
|
||||
unmark_applied(Version) ->
|
||||
mnesia:dirty_delete({?TABLE, atom_to_list(Version)}).
|
||||
|
||||
Reference in New Issue
Block a user