Настройка хранения и создание индексов #13

This commit is contained in:
2026-05-02 23:18:25 +03:00
parent 8b2c55f425
commit 4fdf380f15
3 changed files with 204 additions and 114 deletions

View File

@@ -18,17 +18,11 @@ create(Email, Password) ->
Id = generate_id(), Id = generate_id(),
{ok, PasswordHash} = logic_auth:hash_password(Password), {ok, PasswordHash} = logic_auth:hash_password(Password),
% Определяем роль: первый пользователь становится админом
Role = case mnesia:dirty_match_object(#user{_ = '_'}) of
[] -> admin;
_ -> user
end,
User = #user{ User = #user{
id = Id, id = Id,
email = Email, email = Email,
password_hash = PasswordHash, password_hash = PasswordHash,
role = Role, role = user,
status = active, status = active,
created_at = calendar:universal_time(), created_at = calendar:universal_time(),
updated_at = calendar:universal_time() updated_at = calendar:universal_time()

View File

@@ -1,17 +1,31 @@
%% ===================================================================
%% EventHub infra_mnesia (стабильная версия с автоочисткой при fresh старте)
%% ===================================================================
-module(infra_mnesia). -module(infra_mnesia).
-behaviour(gen_server). -behaviour(gen_server).
-include("records.hrl"). -include("records.hrl").
%% API
-export([start_link/0, init_tables/0, wait_for_tables/0]). -export([start_link/0, init_tables/0, wait_for_tables/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-define(TABLES, [ -define(TABLES, [
user, session, admin, admin_session, calendar, calendar_share, event, recurrence_exception, user, session, admin, admin_session,
booking, review, report, banned_word, ticket, subscription, admin_audit calendar, calendar_share, calendar_specialist,
event, recurrence_exception,
booking,
review, report, banned_word,
ticket, subscription,
admin_audit, notification
]). ]).
-define(TABLE_WAIT_TIMEOUT, 5000).
%% ===================================================================
%% API
%% ===================================================================
start_link() -> start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
@@ -21,125 +35,123 @@ init_tables() ->
wait_for_tables() -> wait_for_tables() ->
gen_server:call(?MODULE, wait_for_tables). gen_server:call(?MODULE, wait_for_tables).
%% ===================================================================
%% gen_server callbacks
%% ===================================================================
init([]) -> init([]) ->
{ok, #{}}. {ok, #{}}.
handle_call(init_tables, _From, State) -> handle_call(init_tables, _From, State) ->
ok = ensure_schema(), ok = maybe_recreate_schema(),
ok = ensure_cluster_join(),
lists:foreach(fun create_table/1, ?TABLES), lists:foreach(fun create_table/1, ?TABLES),
ok = create_indices(),
{reply, ok, State}; {reply, ok, State};
handle_call(wait_for_tables, _From, State) -> handle_call(wait_for_tables, _From, State) ->
mnesia:wait_for_tables(?TABLES, 5000), mnesia:wait_for_tables(?TABLES, ?TABLE_WAIT_TIMEOUT),
{reply, ok, State}. {reply, ok, State}.
handle_cast(_Msg, State) -> handle_cast(_Msg, State) -> {noreply, State}.
{noreply, State}. handle_info(_Info, State) -> {noreply, State}.
terminate(_Reason, _State) -> ok.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
handle_info(_Info, State) -> %% ===================================================================
{noreply, State}. %% Проверка директории Mnesia и при необходимости пересоздание схемы
%% ===================================================================
terminate(_Reason, _State) -> maybe_recreate_schema() ->
ok. MnesiaDir = mnesia:system_info(directory),
case filelib:is_dir(MnesiaDir) of
code_change(_OldVsn, State, _Extra) -> false ->
{ok, State}. io:format("Mnesia directory not found. Creating fresh schema...~n"),
mnesia:stop(),
%% Internal functions mnesia:delete_schema([node()]),
ensure_schema() -> mnesia:create_schema([node()]),
case mnesia:create_schema([node()]) of mnesia:start(),
ok ->
ok; ok;
{error, {Node, {already_exists, Node}}} -> true ->
ok; io:format("Mnesia directory exists (~s). Reusing existing schema.~n", [MnesiaDir]),
{error, {already_exists, _Node}} -> case mnesia:system_info(is_running) of
ok; yes -> ok;
{error, Reason} -> _ -> mnesia:start()
error({schema_creation_failed, Reason}) end
end. end.
%% ===================================================================
%% Кластер
%% ===================================================================
ensure_cluster_join() ->
ExtraNodes = application:get_env(eventhub, extra_db_nodes, []),
case ExtraNodes of
[] -> ok;
Nodes ->
ok = mnesia:change_config(extra_db_nodes, Nodes),
case lists:member(node(), mnesia:table_info(schema, disc_copies)) of
false -> mnesia:add_table_copy(schema, node(), disc_copies);
true -> ok
end
end.
%% ===================================================================
%% Создание таблиц
%% ===================================================================
create_table(Table) -> create_table(Table) ->
case mnesia:create_table(Table, table_opts(Table)) of Opts = table_opts(Table),
{atomic, ok} -> case mnesia:create_table(Table, Opts) of
ok; {atomic, ok} -> ok;
{aborted, {already_exists, _}} -> {aborted, {already_exists, _}} -> ok;
ok; % таблица уже существует пропускаем
{aborted, Reason} -> {aborted, Reason} ->
error({table_creation_failed, Table, Reason}) error({table_creation_failed, Table, Reason})
end. end.
%% Опции таблиц без индексов (добавим позже) %% ===================================================================
table_opts(user) -> %% Опции хранения таблиц
[ %% ===================================================================
{attributes, record_info(fields, user)},
{ram_copies, [node()]} table_opts(user) -> [{disc_copies, [node()]}, {attributes, record_info(fields, user)}];
]; table_opts(admin) -> [{disc_copies, [node()]}, {attributes, record_info(fields, admin)}];
table_opts(session) -> table_opts(calendar) -> [{disc_copies, [node()]}, {attributes, record_info(fields, calendar)}];
[ table_opts(calendar_share) -> [{disc_copies, [node()]}, {attributes, record_info(fields, calendar_share)}];
{attributes, record_info(fields, session)}, table_opts(calendar_specialist) -> [{disc_copies, [node()]}, {attributes, record_info(fields, calendar_specialist)}];
{ram_copies, [node()]} 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(admin) -> table_opts(booking) -> [{disc_copies, [node()]}, {attributes, record_info(fields, booking)}];
[ table_opts(review) -> [{disc_copies, [node()]}, {attributes, record_info(fields, review)}];
{attributes, record_info(fields, admin)}, table_opts(report) -> [{disc_copies, [node()]}, {attributes, record_info(fields, report)}];
{ram_copies, [node()]} 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(admin_session) -> table_opts(subscription) -> [{disc_copies, [node()]}, {attributes, record_info(fields, subscription)}];
[ table_opts(admin_audit) -> [{disc_copies, [node()]}, {attributes, record_info(fields, admin_audit)}];
{attributes, record_info(fields, admin_session)}, table_opts(notification) -> [{disc_copies, [node()]}, {attributes, record_info(fields, notification)}];
{ram_copies, [node()]} table_opts(session) -> [{ram_copies, [node()]}, {attributes, record_info(fields, session)}];
]; table_opts(admin_session) -> [{ram_copies, [node()]}, {attributes, record_info(fields, admin_session)}].
table_opts(calendar) ->
[ %% ===================================================================
{attributes, record_info(fields, calendar)}, %% Индексы
{ram_copies, [node()]} %% ===================================================================
];
table_opts(calendar_share) -> create_indices() ->
[ mnesia:add_table_index(event, calendar_id),
{attributes, record_info(fields, calendar_share)}, mnesia:add_table_index(event, start_time),
{ram_copies, [node()]} mnesia:add_table_index(event, event_type),
]; mnesia:add_table_index(event, master_id),
table_opts(event) -> mnesia:add_table_index(event, specialist_id),
[ mnesia:add_table_index(event, status),
{attributes, record_info(fields, event)}, mnesia:add_table_index(booking, event_id),
{ram_copies, [node()]} mnesia:add_table_index(booking, user_id),
]; mnesia:add_table_index(booking, status),
table_opts(recurrence_exception) -> mnesia:add_table_index(calendar, owner_id),
[ mnesia:add_table_index(calendar, status),
{attributes, record_info(fields, recurrence_exception)}, mnesia:add_table_index(calendar, short_name),
{ram_copies, [node()]} mnesia:add_table_index(calendar, category),
]; mnesia:add_table_index(calendar_specialist, calendar_id),
table_opts(booking) -> mnesia:add_table_index(calendar_specialist, user_id),
[ mnesia:add_table_index(user, nickname),
{attributes, record_info(fields, booking)}, mnesia:add_table_index(notification, user_id),
{ram_copies, [node()]} mnesia:add_table_index(notification, is_read),
]; ok.
table_opts(review) ->
[
{attributes, record_info(fields, review)},
{ram_copies, [node()]}
];
table_opts(report) ->
[
{attributes, record_info(fields, report)},
{ram_copies, [node()]}
];
table_opts(banned_word) ->
[
{attributes, record_info(fields, banned_word)},
{ram_copies, [node()]}
];
table_opts(ticket) ->
[
{attributes, record_info(fields, ticket)},
{ram_copies, [node()]}
];
table_opts(subscription) ->
[
{attributes, record_info(fields, subscription)},
{ram_copies, [node()]}
];
table_opts(admin_audit) ->
[
{attributes, record_info(fields, admin_audit)},
{ram_copies, [node()]}
].

View File

@@ -0,0 +1,84 @@
%% ===================================================================
%% EventHub утилита фрагментации больших таблиц Mnesia
%% ===================================================================
-module(infra_mnesia_fragmentation).
-export([
fragment_table/2,
defragment_table/1,
add_fragment/2,
info/1
]).
%% -------------------------------------------------------------------
%% @doc Включает фрагментацию для заданной таблицы.
%% Table - имя таблицы (atom)
%% FragCount - количество фрагментов (integer > 1)
%%
%% После вызова таблица будет автоматически распределена
%% по фрагментам. Новые записи будут попадать в нужный фрагмент
%% на основе хеша ключа.
%%
%% Пример:
%% infra_mnesia_fragmentation:fragment_table(event, 4).
%% -------------------------------------------------------------------
fragment_table(Table, FragCount) when FragCount > 1 ->
case mnesia:change_table_frag(Table, {activate, FragCount}) of
{atomic, ok} ->
io:format("Table ~p successfully fragmented into ~p fragments~n", [Table, FragCount]),
ok;
{aborted, Reason} ->
{error, {fragmentation_failed, Table, Reason}}
end;
fragment_table(_Table, FragCount) ->
{error, {invalid_frag_count, FragCount}}.
%% -------------------------------------------------------------------
%% @doc Отключает фрагментацию, возвращая таблицу к обычному виду.
%% Все данные остаются сохранными, но таблица перестаёт быть
%% фрагментированной. Может занять продолжительное время
%% на больших объёмах.
%% -------------------------------------------------------------------
defragment_table(Table) ->
case mnesia:change_table_frag(Table, deactivate) of
{atomic, ok} ->
io:format("Fragmentation removed for table ~p~n", [Table]),
ok;
{aborted, Reason} ->
{error, {defragmentation_failed, Table, Reason}}
end.
%% -------------------------------------------------------------------
%% @doc Добавляет новый фрагмент к уже фрагментированной таблице.
%% Полезно для постепенного масштабирования.
%% -------------------------------------------------------------------
add_fragment(Table, ExtraFrags) ->
case mnesia:add_table_fragment(Table, ExtraFrags) of
{atomic, ok} ->
io:format("Added ~p fragment(s) to table ~p~n", [ExtraFrags, Table]),
ok;
{aborted, Reason} ->
{error, {add_fragment_failed, Table, Reason}}
end.
%% -------------------------------------------------------------------
%% @doc Выводит информацию о фрагментации таблицы (если включена)
%% или о текущем состоянии.
%% -------------------------------------------------------------------
info(Table) ->
try
IsFrag = mnesia:table_info(Table, frag_property),
FragCount = case IsFrag of
[{n_fragments, N} | _] -> N;
_ -> 1
end,
io:format("Table ~p fragmentation: ~p fragments~n", [Table, FragCount]),
FragList = mnesia:table_info(Table, frag_dist),
io:format("Fragment distribution: ~p~n", [FragList]),
ok
catch
_:_ ->
io:format("Table ~p is not fragmented~n", [Table]),
ok
end.