diff --git a/src/archive/archive_controller.erl b/src/archive/archive_controller.erl new file mode 100644 index 0000000..83d0be4 --- /dev/null +++ b/src/archive/archive_controller.erl @@ -0,0 +1,101 @@ +-module(archive_controller). +-compile([{nowarn_deprecated_function, [{slave, start, 3}, {slave, stop, 1}]}]). +-include("records.hrl"). +-export([archive_day/1]). + +archive_day(Day) -> + ArchiveNode = list_to_atom("eventhub_archive_" ++ Day ++ "@" ++ host()), + case start_archive_node(ArchiveNode) of + {ok, PeerOrSlave} -> + try + rpc:call(ArchiveNode, mnesia, create_schema, [[ArchiveNode]]), + rpc:call(ArchiveNode, mnesia, start, []), + rpc:call(ArchiveNode, code, ensure_loaded, [archive_fetcher]), + create_archive_table(ArchiveNode, event, + [id, calendar_id, start_time, end_time, event_type, + master_id, specialist_id, title, description, + attachments, edit_history, status, created_at, updated_at], + [calendar_id, start_time, event_type, master_id, + specialist_id, status]), + create_archive_table(ArchiveNode, booking, + [id, event_id, user_id, status, confirmed_at, + created_at, updated_at, notes, reminder_sent], + [event_id, user_id, status]), + create_archive_table(ArchiveNode, review, + [id, user_id, target_type, target_id, rating, comment, + status, reason, created_at, updated_at, likes, + dislikes, edited_at], []), + create_archive_table(ArchiveNode, report, + [id, reporter_id, target_type, target_id, reason, + status, created_at, resolved_at, resolved_by], []), + ok = transfer_data(ArchiveNode, Day), + io:format("Archived day ~s successfully.~n", [Day]) + after + stop_archive_node(PeerOrSlave, ArchiveNode) + end; + {error, Reason} -> + io:format("Failed to start archive node: ~p~n", [Reason]), + {error, Reason} + end. + +start_archive_node(Node) -> + case os:getenv("CLUSTER_MODE") of + "true" -> + peer:start_link(#{name => Node, host => host()}); + _ -> + CookieStr = atom_to_list(erlang:get_cookie()), + case slave:start(host(), Node, "-setcookie " ++ CookieStr) of + {ok, Slave} -> {ok, Slave}; + Error -> Error + end + end. + +stop_archive_node(PeerOrSlave, Node) -> + case os:getenv("CLUSTER_MODE") of + "true" -> peer:stop(PeerOrSlave); + _ -> slave:stop(Node) + end. + +create_archive_table(Node, Tab, Attributes, Indices) -> + Opts = [{disc_only_copies, [Node]}, + {attributes, Attributes}, + {type, set}] ++ case Indices of + [] -> []; + _ -> [{index, Indices}] + end, + rpc:call(Node, mnesia, create_table, [Tab, Opts]). + +transfer_data(ArchiveNode, Day) -> + Tables = [event, booking, review, report], + lists:foreach(fun(Tab) -> + Records = fetch_records(Tab, Day), + rpc:call(ArchiveNode, mnesia, transaction, [ + fun() -> [mnesia:write(Rec) || Rec <- Records] end + ]), + mnesia:transaction(fun() -> + [mnesia:delete({Tab, element(2, Rec)}) || Rec <- Records] + end) + end, Tables). + +fetch_records(event, Day) -> + Start = list_to_binary(Day ++ " 00:00:00"), + End = list_to_binary(Day ++ " 23:59:59"), + mnesia:dirty_select(event, [{#event{start_time = '$1', _ = '_'}, + [{'>=','$1', Start},{'=<','$1', End}], + ['$_']}]); +fetch_records(booking, Day) -> + mnesia:dirty_select(booking, [{#booking{created_at = '$1', _ = '_'}, + [{'>=','$1', Day},{'=<','$1', Day ++ " 23:59:59"}], + ['$_']}]); +fetch_records(review, Day) -> + mnesia:dirty_select(review, [{#review{created_at = '$1', _ = '_'}, + [{'>=','$1', Day},{'=<','$1', Day ++ " 23:59:59"}], + ['$_']}]); +fetch_records(report, Day) -> + mnesia:dirty_select(report, [{#report{created_at = '$1', _ = '_'}, + [{'>=','$1', Day},{'=<','$1', Day ++ " 23:59:59"}], + ['$_']}]). + +host() -> + {ok, Name} = inet:gethostname(), + Name. \ No newline at end of file diff --git a/src/archive/archive_fetcher.erl b/src/archive/archive_fetcher.erl new file mode 100644 index 0000000..184099b --- /dev/null +++ b/src/archive/archive_fetcher.erl @@ -0,0 +1,12 @@ +-module(archive_fetcher). +-include("records.hrl"). + +-export([fetch/3]). + +fetch(CalendarId, Year, Month) -> + Start = {Year, Month, 1, 0, 0, 0}, + End = {Year, Month, 31, 23, 59, 59}, + mnesia:dirty_select(event, [{#event{calendar_id = CalendarId, + start_time = '$1', _ = '_'}, + [{'>=','$1', Start},{'=<','$1', End}], + ['$_']}]). \ No newline at end of file diff --git a/src/archive/archive_manager.erl b/src/archive/archive_manager.erl new file mode 100644 index 0000000..c3e68a0 --- /dev/null +++ b/src/archive/archive_manager.erl @@ -0,0 +1,71 @@ +-module(archive_manager). +-behaviour(gen_server). + +-export([start_link/0, get_archive_node/1]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2]). + +-define(TIMEOUT, 30000). % 30 секунд неактивности + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +get_archive_node(Day) -> + gen_server:call(?MODULE, {get_node, Day}). + +init([]) -> + {ok, #{}}. + +handle_call({get_node, Day}, _From, State) -> + Node = list_to_atom("eventhub_archive_" ++ Day ++ "@" ++ host()), + case maps:find(Node, State) of + {ok, _Info} -> % переменная не используется, ок + {reply, {ok, Node}, update_access(State, Node)}; + error -> + case start_archive_node(Day) of + {ok, Node} -> + Ref = erlang:send_after(?TIMEOUT, self(), {release, Node}), + NewState = State#{Node => #{timer => Ref, last_access => erlang:monotonic_time()}}, + {reply, {ok, Node}, NewState}; + {error, Reason} -> + {reply, {error, Reason}, State} + end + end. + +handle_cast(_, State) -> {noreply, State}. + +handle_info({release, Node}, State) -> + case maps:find(Node, State) of + {ok, #{last_access := Last}} -> + Now = erlang:monotonic_time(), + if Now - Last >= (?TIMEOUT * 1000) -> + stop_archive_node(Node), + {noreply, maps:remove(Node, State)}; + true -> + Ref = erlang:send_after(?TIMEOUT, self(), {release, Node}), + {noreply, State#{Node => #{timer => Ref, last_access => Last}}} + end; + error -> + {noreply, State} + end; +handle_info(_, State) -> {noreply, State}. + +update_access(State, Node) -> + Info = maps:get(Node, State), + Ref = erlang:send_after(?TIMEOUT, self(), {release, Node}), + State#{Node => Info#{timer => Ref, last_access => erlang:monotonic_time()}}. + +start_archive_node(Day) -> + Node = list_to_atom("eventhub_archive_" ++ Day ++ "@" ++ host()), + case slave:start(host(), Node) of + {ok, _} -> + rpc:call(Node, mnesia, start, []), + {ok, Node}; + Error -> Error + end. + +stop_archive_node(Node) -> + rpc:cast(Node, init, stop, []). + +host() -> + {ok, Name} = inet:gethostname(), + Name. \ No newline at end of file diff --git a/src/archive/calendar_html_renderer.erl b/src/archive/calendar_html_renderer.erl new file mode 100644 index 0000000..e3f0cb4 --- /dev/null +++ b/src/archive/calendar_html_renderer.erl @@ -0,0 +1,57 @@ +-module(calendar_html_renderer). +-include("records.hrl"). + +-export([render_month/3, init_cache/0]). + +init_cache() -> + case ets:info(archive_html_cache) of + undefined -> + ets:new(archive_html_cache, [set, public, named_table, {keypos, 1}]); + _ -> ok + end. + +render_month(Year, Month, Events) -> + Key = {Year, Month}, + try ets:lookup(archive_html_cache, Key) of + [{Key, Html}] -> Html; + [] -> + Html = generate_html(Year, Month, Events), + ets:insert(archive_html_cache, {Key, Html}), + Html + catch + _:_ -> + generate_html(Year, Month, Events) + end. + +generate_html(Year, Month, Events) -> + DaysInMonth = calendar:last_day_of_the_month(Year, Month), + EventsByDay = group_events_by_day(Events), + DayList = lists:seq(1, DaysInMonth), + DayCells = lists:map(fun(D) -> + DayEvents = maps:get(D, EventsByDay, []), + ["