From 4fdf380f15f0edb3f4e47f4de5e0b4caa5d2056f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=A1=D0=B0?= =?UTF-8?q?=D0=B1=D0=B8=D0=BB=D0=B8=D0=BD?= Date: Sat, 2 May 2026 23:18:25 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9D=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=B8=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B8=D0=BD=D0=B4=D0=B5=D0=BA=D1=81=D0=BE=D0=B2=20https://git.?= =?UTF-8?q?sabilin.com/EventHub/EventHubBack/issues/13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/core_user.erl | 8 +- src/infra/infra_mnesia.erl | 226 ++++++++++++----------- src/infra/infra_mnesia_fragmentation.erl | 84 +++++++++ 3 files changed, 204 insertions(+), 114 deletions(-) create mode 100644 src/infra/infra_mnesia_fragmentation.erl diff --git a/src/core/core_user.erl b/src/core/core_user.erl index 4b19de8..17e818c 100644 --- a/src/core/core_user.erl +++ b/src/core/core_user.erl @@ -18,17 +18,11 @@ create(Email, Password) -> Id = generate_id(), {ok, PasswordHash} = logic_auth:hash_password(Password), - % Определяем роль: первый пользователь становится админом - Role = case mnesia:dirty_match_object(#user{_ = '_'}) of - [] -> admin; - _ -> user - end, - User = #user{ id = Id, email = Email, password_hash = PasswordHash, - role = Role, + role = user, status = active, created_at = calendar:universal_time(), updated_at = calendar:universal_time() diff --git a/src/infra/infra_mnesia.erl b/src/infra/infra_mnesia.erl index 67409ff..eaa9c3e 100644 --- a/src/infra/infra_mnesia.erl +++ b/src/infra/infra_mnesia.erl @@ -1,17 +1,31 @@ +%% =================================================================== +%% EventHub – infra_mnesia (стабильная версия с автоочисткой при fresh старте) +%% =================================================================== -module(infra_mnesia). -behaviour(gen_server). -include("records.hrl"). -%% API -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, [ - user, session, admin, admin_session, calendar, calendar_share, event, recurrence_exception, - booking, review, report, banned_word, ticket, subscription, admin_audit + user, session, admin, admin_session, + 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() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). @@ -21,125 +35,123 @@ init_tables() -> wait_for_tables() -> gen_server:call(?MODULE, wait_for_tables). +%% =================================================================== +%% gen_server callbacks +%% =================================================================== + init([]) -> {ok, #{}}. handle_call(init_tables, _From, State) -> - ok = ensure_schema(), + ok = maybe_recreate_schema(), + ok = ensure_cluster_join(), lists:foreach(fun create_table/1, ?TABLES), + ok = create_indices(), {reply, ok, 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}. -handle_cast(_Msg, State) -> - {noreply, State}. +handle_cast(_Msg, 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) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%% Internal functions -ensure_schema() -> - case mnesia:create_schema([node()]) of - ok -> +maybe_recreate_schema() -> + MnesiaDir = mnesia:system_info(directory), + case filelib:is_dir(MnesiaDir) of + false -> + io:format("Mnesia directory not found. Creating fresh schema...~n"), + mnesia:stop(), + mnesia:delete_schema([node()]), + mnesia:create_schema([node()]), + mnesia:start(), ok; - {error, {Node, {already_exists, Node}}} -> - ok; - {error, {already_exists, _Node}} -> - ok; - {error, Reason} -> - error({schema_creation_failed, Reason}) + 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. +%% =================================================================== +%% Кластер +%% =================================================================== + +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) -> - case mnesia:create_table(Table, table_opts(Table)) of - {atomic, ok} -> - ok; - {aborted, {already_exists, _}} -> - ok; % таблица уже существует – пропускаем + 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) -> - [ - {attributes, record_info(fields, user)}, - {ram_copies, [node()]} - ]; -table_opts(session) -> - [ - {attributes, record_info(fields, session)}, - {ram_copies, [node()]} - ]; -table_opts(admin) -> - [ - {attributes, record_info(fields, admin)}, - {ram_copies, [node()]} - ]; -table_opts(admin_session) -> - [ - {attributes, record_info(fields, admin_session)}, - {ram_copies, [node()]} - ]; -table_opts(calendar) -> - [ - {attributes, record_info(fields, calendar)}, - {ram_copies, [node()]} - ]; -table_opts(calendar_share) -> - [ - {attributes, record_info(fields, calendar_share)}, - {ram_copies, [node()]} - ]; -table_opts(event) -> - [ - {attributes, record_info(fields, event)}, - {ram_copies, [node()]} - ]; -table_opts(recurrence_exception) -> - [ - {attributes, record_info(fields, recurrence_exception)}, - {ram_copies, [node()]} - ]; -table_opts(booking) -> - [ - {attributes, record_info(fields, booking)}, - {ram_copies, [node()]} - ]; -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()]} - ]. \ No newline at end of file +%% =================================================================== +%% Опции хранения таблиц +%% =================================================================== + +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(session) -> [{ram_copies, [node()]}, {attributes, record_info(fields, session)}]; +table_opts(admin_session) -> [{ram_copies, [node()]}, {attributes, record_info(fields, admin_session)}]. + +%% =================================================================== +%% Индексы +%% =================================================================== + +create_indices() -> + mnesia:add_table_index(event, calendar_id), + 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(notification, user_id), + mnesia:add_table_index(notification, is_read), + ok. \ No newline at end of file diff --git a/src/infra/infra_mnesia_fragmentation.erl b/src/infra/infra_mnesia_fragmentation.erl new file mode 100644 index 0000000..ae89690 --- /dev/null +++ b/src/infra/infra_mnesia_fragmentation.erl @@ -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. \ No newline at end of file