%%% -------------------------------------------------------
%%% @author huangyongxing@yeah.net
%%% @doc
%%% 关服管理
%%% 在监控树关闭时，会先保存角色数据（不在监控树下），
%%% 以及处理其他非处于监控树下，而需要保存数据的内容
%%% @end
%%% @since 2017-05-05
%%% -------------------------------------------------------
-module(shutdown_mgr).
-behaviour(gen_server).
-author("huangyongxing@yeah.net").
%% API
-export([
        start_link/0
    ]).
%% gen_server callbacks
-export([init/1,
        handle_call/3,
        handle_cast/2,
        handle_info/2,
        terminate/2,
        code_change/3
    ]).
-define(SERVER, ?MODULE).

-include("common.hrl").
-include("server.hrl").

-record(state, {}).

%%%===================================================================
%%% API
%%%===================================================================

start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

init([]) ->
    process_flag(trap_exit, true),
    {ok, #state{}, hibernate}.

handle_call(_Request, _From, State) ->
    Reply = ok,
    {reply, Reply, State, hibernate}.

handle_cast(_Msg, State) ->
    {noreply, State, hibernate}.

handle_info(_Info, State) ->
    {noreply, State, hibernate}.

terminate(_Reason, _State) ->
    ?INFO_TTY("Start kick out all users ..."),
    server_stop_all(),
    ?INFO_TTY("Finished kick out all users!"),
    %% 执行其他清理操作
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================

%% 服务器停止调用
%% 注意：关服时候，不要有sleep之类的操作影响整体关服时间
%% 因为运维那边有超时（一般10~20秒），会强制结束erlang节点
server_stop_all()->
    % mod_ban:ban_all(),
    {ok, BinDataStop} = pt_590:write(59004, 11),
    do_server_stop_all(ets:tab2list(?ETS_ONLINE), BinDataStop, 1, self(), []).

%% 让所有玩家自动退出
%% 此处采用批次并发的方式进行踢出操作，以尽快完成关服，
%% 并尽量保证批次之间错开时间执行，避免IO堵塞
%% 原理是每M人一批次，每批次最多等待N毫秒时间，如果提前执行完则提前继续下一批次踢出操作
%% 总预估时间要小于强制关服时间（目前外服要求运维预留20秒），
%% 还要预留其他服务型功能系统的进程保存数据的时间
do_server_stop_all([], _BinDataStop, _Count, _SelfPid, WaitList) ->
    case WaitList of
        [] -> ignore;
        _  ->
            % ?INFO("wait_stop_over wait start 2"),
            wait_stop_over(WaitList, utime:unixtime(ms) + 600)
    end,
    ok;
do_server_stop_all([#ets_online{sid = Sid, pid = Pid} | T], BinDataStop, Count, SelfPid, WaitList) ->
    spawn(fun() ->
                case is_pid(Sid) andalso is_process_alive(Sid) of
                    true  ->
                        lib_server_send:send_to_sid(Sid, BinDataStop),
                        %% 关闭socket
                        lib_server_send:send_to_sid(Sid, close);
                    false -> ignore
                end,
                case is_process_alive(Pid) of
                    true  -> mod_server:server_stop(Pid);
                    false -> ignore
                end,
                SelfPid ! {over, Pid}
        end),
    %% 并发数100的选择： 基于数据库连接数，稍小但是接近是比较合理充分利用的值，可以尽可能缩短等待时间
    % 预计合服后期，大服最多3000角色进程，则会分30批次，
    % 要压缩在15秒完成（预留其他功能系统写入时间），预计一批最多500ms
    % （Win版开发机上测试400人操作，大约600ms才写入一批玩家数据，
    % 产品线Linux上预计性能更强劲一些，具体看提供的机器情况，目前设定500ms应比较合理）
    % 如果时间不充足，应让运维延长强杀进程的时间
    NewWaitList =
    case Count rem 100 =:= 0 of
        true  ->
            % ?INFO("wait_stop_over wait start 1"),
            wait_stop_over([Pid | WaitList], utime:unixtime(ms) + 500),
            [];
        false ->
            [Pid | WaitList]
    end,
    do_server_stop_all(T, BinDataStop, Count + 1, SelfPid, NewWaitList).

%% 等待stop_player完成
wait_stop_over([], _) ->
    % ?INFO("wait_stop_over over"),
    over;
wait_stop_over(timeout, _) ->
    % ?INFO("wait_stop_over timeout"),
    timeout;
wait_stop_over(WaitList, EndTime) ->
    RestTime = max(EndTime - utime:unixtime(ms), 0),
    NewWaitList =
    receive
        {over, Key} ->
            % ?INFO("del Key:~w, old len:~w", [Key, length(WaitList)]),
            lists:delete(Key, WaitList);
        _ ->
            WaitList
    after RestTime ->
            timeout
    end,
    wait_stop_over(NewWaitList, EndTime).

