%%% ------------------------------------------------------------------------------------------------ %%% Director is available for use under the following license, commonly known as the 3-clause (or %%% "modified") BSD license: %%% %%% Copyright (c) 2017-2018, Pouriya Jahanbakhsh %%% (pouriya.jahanbakhsh@gmail.com) %%% All rights reserved. %%% %%% Redistribution and use in source and binary forms, with or without modification, are permitted %%% provided that the following conditions are met: %%% %%% 1. Redistributions of source code must retain the above copyright notice, this list of %%% conditions and the following disclaimer. %%% %%% 2. Redistributions in binary form must reproduce the above copyright notice, this list of %%% conditions and the following disclaimer in the documentation and/or other materials provided %%% with the distribution. %%% %%% 3. Neither the name of the copyright holder nor the names of its contributors may be used to %%% endorse or promote products derived from this software without specific prior written %%% permission. %%% %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR %%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND %%% FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR %%% CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR %%% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR %%% SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY %%% THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR %%% OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE %%% POSSIBILITY OF SUCH DAMAGE. %%% ------------------------------------------------------------------------------------------------ %% @author Pouriya Jahanbakhsh %% @version 17.9.16 %% @doc %% Director is a production-ready supervisor and manager for Erlang/Elixir processes with %% focus on speed and flexibility. %% @end %% ------------------------------------------------------------------------------------------------- -module(director). -author("pouriya.jahanbakhsh@gmail.com"). %% ------------------------------------------------------------------------------------------------- %% Exports: %% supervisor-like API: -export([start_link/2 ,start_link/3 ,start_child/2 ,restart_child/2 ,terminate_child/2 ,delete_child/2 ,count_children/1 ,which_children/1 ,get_childspec/2 ,check_childspec/1]). %% Director Specific API: -export([get_pid/2 ,get_pids/1 ,get_plan/2 ,get_count/2 ,change_plan/3 ,change_count/3 ,get_default_childspec/1 ,change_default_childspec/2 ,change_log_validator/2 ,change_log_validator/3 ,start_link/4 ,start/2 ,start/3 ,start/4 ,stop/1 ,stop/2 ,stop/3 ,plan_element_fun/3 ,log_validator/2]). %% Previous APIs with timeout argument: -export([start_child/3 ,restart_child/3 ,terminate_child/3 ,delete_child/3 ,count_children/2 ,which_children/2 ,get_childspec/3 ,get_plan/3 ,get_count/3 ,change_plan/4 ,change_count/4 ,get_pid/3 ,get_pids/2 ,get_default_childspec/2 ,change_default_childspec/3]). %% gen callback: -export([init_it/6]). %% sys callbacks: -export([system_code_change/4 ,system_continue/3 ,system_get_state/1 ,system_replace_state/2 ,system_terminate/4 ,format_status/2]). %% ------------------------------------------------------------------------------------------------- %% Types: %% 'id' is mandatory. %% If you don't set 'start' for default childspec, 'start' is mandatory. %% Default values for other keys: %% #{plan => [fun %% (_Id, _Reason, _RestartCount) -> %% restart %% end] %% ,count => 5 %% ,terminate_timeout => infinity % For supervisors %% 1000 % For workers %% ,type => worker %% ,modules => [Module] % Will get from value of 'start' key %% ,append => false} -type childspec() :: #{'id' => id() ,'start' => start() ,'plan' => plan() ,'count' => count() ,'terminate_timeout' => terminate_timeout() ,'type' => type() ,'modules' => modules() ,'append' => append() ,'log_validator' => log_validator()}. -type id() :: term(). -type start() :: module() % default is {module(), start_link, []} | {module(), function()} % default Args is [] | mfa(). -type plan() :: [plan_element()] | []. -type plan_element() :: 'restart' | {'restart', pos_integer()} | 'wait' | 'stop' | {'stop', Reason::term()} | fun((Id::term(), Reason::term(), RestartCount::pos_integer()) -> 'restart' | {'restart', pos_integer()} | 'wait' | 'stop' | {'stop', Reason::term()}) . -type count() :: 'infinity' | non_neg_integer(). -type terminate_timeout() :: 'infinity' | non_neg_integer(). -type type() :: 'worker' | 'supervisor'. -type modules() :: [module()] | 'dynamic'. -type append() :: boolean(). -type default_childspec() :: #{'start' => start() ,'plan' => plan() ,'count' => count() ,'terminate_timeout' => terminate_timeout() ,'type' => type() ,'modules' => modules() ,'log_validator' => log_validator()}. -type log_validator() :: fun((Type:: log_type(), Extra::term()) -> log_mode()). -type log_type() :: 'info' | 'error' | 'warning'. -type log_mode() :: 'short' | 'long' | 'none'. -type start_return() :: {'ok', pid()} | 'ignore' | {'error', term()}. -type register_name() :: {'local', atom()} | {'global', atom()} | {'via', module(), term()}. -type director() :: pid() | atom(). -type start_options() :: [start_option()] | []. -type start_option() :: debug_option() | spawn_options() | timeout_option() | log_validator_option(). -type debug_option() :: {'debug', ['trace'|'log'|'statistics'|'debug'] | []}. -type spawn_options() :: {'spawn_opt', proc_lib:spawn_option()}. -type timeout_option() :: {'timeout', timeout()}. -type log_validator_option() :: {'log_validator', log_validator()}. -export_type([childspec/0 ,default_childspec/0 ,start_options/0 ,start_option/0 ,start_return/0 ,log_validator/0 ,log_mode/0 ,log_type/0]). %% ------------------------------------------------------------------------------------------------- %% Behaviour information: -callback init(InitArg) -> {'ok', Childspecs} | {'ok', Childspecs, DefaultChildspec} | 'ignore' | {'stop', Reason} when InitArg :: term(), Childspecs :: [childspec()] | [], DefaultChildspec :: default_childspec(), Reason :: term(). %% ------------------------------------------------------------------------------------------------- %% Records & Macros & Includes: %% Dependencies: %% all macros -include("internal/director_defaults.hrl"). %% Dependecies: %% #?CHILD{} -include("internal/director_child.hrl"). -define(STATE, director_state_record). -record(?STATE, {name ,module ,init_argument ,table ,default_childspec ,log_validator ,table_type}). %% ------------------------------------------------------------------------------------------------- %% supervisor-like API: -spec start_link(module(), InitArg::term()) -> start_return(). %% @doc %% Starts and links a director. %% @end start_link(Mod, InitArg) -> gen:start(?MODULE, link, Mod, InitArg, ?DEF_START_OPTIONS). -spec start_link(register_name()|module(), module()|term(), term()|start_options()) -> start_return(). %% @doc %% Starts and links a director. %% @end start_link(Name_or_Mod, Mod_or_InitArg, InitArg_or_Opts) when is_tuple(Name_or_Mod) -> gen:start(?MODULE, link, Name_or_Mod, Mod_or_InitArg, InitArg_or_Opts, ?DEF_START_OPTIONS); start_link(Name_or_Mod, Mod_or_InitArg, InitArg_or_Opts) -> gen:start(?MODULE, link, Name_or_Mod, Mod_or_InitArg, InitArg_or_Opts). -spec start_child(director(), childspec()) -> start_return(). %% @doc %% Starts a child using childspec. %% Reason of error may be for childspec not starting process. %% @end start_child(Director, ChildSpec) -> do_call(Director, {?START_CHILD_TAG, ChildSpec}). -spec restart_child(director(), id()) -> {'ok', pid()} | {'ok', pid(), Extra::term()} | {'error', Reason::'running'|'restarting'|'not_found'|term()}. %% @doc %% Restarts a terminated or waited child using child id. %% Reason of error may be for starting process. %% @end restart_child(Director, Id) -> do_call(Director, {?RESTART_CHILD_TAG, Id}). -spec terminate_child(director(), id() | pid()) -> 'ok' | {'error', Reason :: 'not_found' | term()}. %% @doc %% Restarts a running child using child id or child pid. %% @end terminate_child(Director, Id_or_Pid) -> do_call(Director, {?TERMINATE_CHILD_TAG, Id_or_Pid}). -spec delete_child(director(), id()) -> 'ok' | {'error', Reason :: 'not_found' | 'running' | term()}. %% @doc %% Deletes a terminated or waited child using child id. %% @end delete_child(Director, Id) -> do_call(Director, {?DELETE_CHILD_TAG, Id}). -spec count_children(director()) -> [{'specs', non_neg_integer()} |{'active', non_neg_integer()} |{'supervisors', non_neg_integer()} |{'workers', non_neg_integer()}]. %% @doc %% Returns count of children. %% @end count_children(Director) -> do_call(Director, ?COUNT_CHILDREN_TAG). -spec which_children(director()) -> [{id(), type(), pid()|'restarting'|'undefined', modules()}] | []. %% @doc %% Returns information about each children. %% @end which_children(Director) -> do_call(Director, ?WHICH_CHILDREN_TAG). -spec get_childspec(director(), id() | pid()) -> {'ok', childspec()} | {'error', 'not_found'}. %% @doc %% Returns childspec of child. %% @end get_childspec(Director, Name) -> do_call(Director, {?GET_CHILDSPEC_TAG, Name}). -spec check_childspec(childspec()) -> 'ok' | {'error', Reason::term()}. %% @doc %% Returns childspec of child. %% @end check_childspec(ChildSpec) -> case director_utils:check_childspec(ChildSpec, ?DEF_DEF_CHILDSPEC) of {ok, _FixedChildSpec} -> ok; {error, _Reason}=Error -> Error end. %% ------------------------------------------------------------------------------------------------- %% Specific API: -spec change_plan(director(), id(), plan()) -> 'ok' | {'error', 'not_found' | term()}. %% @doc %% Changes the plan of running or waited or terminated child. %% If 'count' is equal to crash count, new plan will never run, Because in next crash director %% will crash with reason {reached_max_restart_plan, ... %% @end change_plan(Director, Id, Plan) -> do_call(Director, {?CHANGE_PLAN_TAG, Id, Plan}). -spec change_count(director(), id(), count()) -> 'ok' | {'error', 'not_found'|term()}. %% @doc %% Changes the count of running or waited or terminated child. %% @end change_count(Director, Id, Count) -> do_call(Director, {?CHANGE_COUNT_TAG, Id, Count}). -spec get_plan(director(), id()) -> 'ok' | {'error', 'not_found'|term()}. %% @doc %% Returns plan of a child. %% @end get_plan(Director, Id) -> do_call(Director, {?GET_PLAN_TAG, Id}). -spec get_count(director(), id()) -> 'ok' | {'error', 'not_found'|term()}. %% @doc %% Returns count of a childspec for child. %% @end get_count(Director, Id) -> do_call(Director, {?GET_COUNT_TAG, Id}). -spec get_pid(director(), id()) -> {'ok', pid()} | {'error', 'not_found'|'restarting'|'undefined'}. %% @doc %% Returns pid of a running child. %% @end get_pid(Director, Id) -> do_call(Director, {?GET_PID_TAG, Id}). -spec get_pids(director()) -> [{id(), pid()}] | []. %% @doc %% Returns list of {id, pid}s for all running children. %% @end get_pids(Director) -> do_call(Director, ?GET_PIDS_TAG). -spec get_default_childspec(director()) -> default_childspec(). %% @doc %% Returns director default childspec. %% @end get_default_childspec(Director) -> do_call(Director, ?GET_DEF_CHILDSPEC). -spec change_default_childspec(director(), default_childspec()) -> {'ok', default_childspec()} | {'error', Reason::term()}. %% @doc %% Changes director default childspec. %% Be careful about using this function. This will change all children with append => true in %% their next restart. %% @end change_default_childspec(Director, DefChildSpec) -> do_call(Director, {?CHANGE_DEF_CHILDSPEC, DefChildSpec}). -spec change_log_validator(director(), log_validator()) -> 'ok' | {'error', term()}. %% @doc %% Changes validate fun. %% @end change_log_validator(Director, LogValidator) -> do_call(Director, {?CHANGE_LOG_VALIDATOR, LogValidator}). -spec change_log_validator(director(), log_validator(), Id::term()) -> 'ok' | {'error', term()}. change_log_validator(Director, LogValidator, Id) -> do_call(Director, {?CHANGE_LOG_VALIDATOR, LogValidator, Id}). -spec start_link(register_name(), module(), InitArg::term(), start_options()) -> start_return(). %% @doc %% Starts and links a director. %% @end start_link(Name, Mod, InitArg, Opts) -> gen:start(?MODULE, link, Name, Mod, InitArg, Opts). -spec start(module(), InitArg::term()) -> start_return(). %% @doc %% Starts a director. %% @end start(Mod, InitArg) -> gen:start(?MODULE, nolink, Mod, InitArg, ?DEF_START_OPTIONS). -spec start(register_name()|module(), module()|term(), term()|start_options()) -> start_return(). %% @doc %% Starts a director. %% @end start(Name_or_Mod, Mod_or_InitArg, InitArg_or_Opts) when is_tuple(Name_or_Mod) -> gen:start(?MODULE,nolink,Name_or_Mod,Mod_or_InitArg, InitArg_or_Opts, ?DEF_START_OPTIONS); start(Name_or_Mod, Mod_or_InitArg, InitArg_or_Opts) -> gen:start(?MODULE, nolink, Name_or_Mod, Mod_or_InitArg, InitArg_or_Opts). -spec start(register_name(), module(), InitArg::term(), start_options()) -> start_return(). %% @doc %% Starts a director. %% @end start(Name, Mod, InitArg, Opts) -> gen:start(?MODULE, nolink, Name, Mod, InitArg, Opts). -spec stop(director()) -> 'ok'. %% @doc %% Stops a director with reason termination 'normal' and 5000 mili %% seconds timeout. %% @end stop(Director) -> proc_lib:stop(Director, normal, ?DEF_STOP_TIMEOUT). -spec stop(director(), Reason::term()) -> 'ok'. %% @doc %% Stops a director with 5000 milli-seconds timeout. %% @end stop(Director, Reason) -> proc_lib:stop(Director, Reason, ?DEF_STOP_TIMEOUT). -spec stop(director(), Reason::term(), timeout()) -> 'ok'. %% @doc %% Stops a director. %% @end stop(Director, Reason, Timeout) -> proc_lib:stop(Director, Reason, Timeout). -spec plan_element_fun(Id::term(), Reason::term(), RestartCount::non_neg_integer()) -> 'restart'. %% @doc %% Restarting if child crashed with any reason. %% @end plan_element_fun(_Id, _Other, _Count) -> restart. -spec log_validator(Type::log_type(), Extra::term()) -> 'short'. %% @doc %% Short description for every log. %% @end log_validator(_Type, _Extra) -> short. %% ------------------------------------------------------------------------------------------------- %% previous APIs with Timeout argument: -spec start_child(director(), childspec(), timeout()) -> start_return(). %% @doc %% Starts a child using childspec. %% Reason of error may be for childspec not starting process. %% @end start_child(Director, ChildSpec, Timeout) -> do_call(Director, {?START_CHILD_TAG, ChildSpec}, Timeout). -spec restart_child(director(), id(), timeout()) -> {'ok', pid()} | {'ok', pid(), Extra::term()} | {'error', Reason::'running'|'restarting'|'not_found'|term()}. %% @doc %% Restarts a terminated or waited child using child id. %% Reason of error may be for starting process. %% @end restart_child(Director, Id, Timeout) -> do_call(Director, {?RESTART_CHILD_TAG, Id}, Timeout). -spec terminate_child(director(), id() | pid(), timeout()) -> 'ok' | {'error', Reason::'not_found'|term()}. %% @doc %% Restarts a running child using child id or child pid. %% @end terminate_child(Director, Id_or_Pid, Timeout) -> do_call(Director, {?TERMINATE_CHILD_TAG, Id_or_Pid}, Timeout). -spec delete_child(director(), id(), timeout()) -> 'ok' | {'error', Reason::'not_found'|'running'|term()}. %% @doc %% Deletes a terminated or waited child using child id. %% @end delete_child(Director, Id, Timeout) -> do_call(Director, {?DELETE_CHILD_TAG, Id}, Timeout). -spec count_children(director(), timeout()) -> [{'specs', non_neg_integer()} |{'active', non_neg_integer()} |{'supervisors', non_neg_integer()} |{'workers', non_neg_integer()}]. %% @doc %% Returns count of children. %% @end count_children(Director, Timeout) -> do_call(Director, ?COUNT_CHILDREN_TAG, Timeout). -spec which_children(director(), timeout()) -> [{id(), type(), pid()|'restarting'|'undefined', modules()}] | []. %% @doc %% Returns information about each children. %% @end which_children(Director, Timeout) -> do_call(Director, ?WHICH_CHILDREN_TAG, Timeout). -spec get_childspec(director(), id() | pid(), timeout()) -> {'ok', childspec()} | {'error', 'not_found'}. %% @doc %% Returns childspec of child. %% @end get_childspec(Director, Name, Timeout) -> do_call(Director, {?GET_CHILDSPEC_TAG, Name}, Timeout). -spec get_count(director(), id(), timeout()) -> 'ok' | {'error', 'not_found'|term()}. %% @doc %% Returns count of a childspec for child. %% @end get_count(Director, Id, Timeout) -> do_call(Director, {?GET_COUNT_TAG, Id}, Timeout). -spec change_plan(director(), id(), plan(), timeout()) -> 'ok' | {'error', not_found | term()}. %% @doc %% Changes the plan of running or waited or terminated child. %% If 'count' is equal to crash count, new plan will never run, Because in next crash director %% will crash with reason {reached_max_restart_plan, ... %% @end change_plan(Director, Id, Plan, Timeout) -> do_call(Director, {?CHANGE_PLAN_TAG, Id, Plan}, Timeout). -spec change_count(director(), id(), count(), timeout()) -> 'ok' | {'error', 'not_found'|term()}. %% @doc %% Changes the count of running or waited or terminated child. %% @end change_count(Director, Id, Count, Timeout) -> do_call(Director, {?CHANGE_COUNT_TAG, Id, Count}, Timeout). -spec get_plan(director(), id(), timeout()) -> 'ok' | {'error', not_found | term()}. %% @doc %% Returns plan of a child. %% @end get_plan(Director, Id, Timeout) -> do_call(Director, {?GET_PLAN_TAG, Id}, Timeout). -spec get_pid(director(), id(), timeout()) -> {'ok', pid()} | {'error', 'not_found'|'restarting'|'undefined'}. %% @doc %% Returns pid of a running child. %% @end get_pid(Director, Id, Timeout) -> do_call(Director, {?GET_PID_TAG, Id}, Timeout). -spec get_pids(director(), timeout()) -> [{id(), pid()}] | []. %% @doc %% Returns list of {id, pid}s for all running children. %% @end get_pids(Director, Timeout) -> do_call(Director, ?GET_PIDS_TAG, Timeout). -spec get_default_childspec(director(), timeout()) -> default_childspec(). %% @doc %% Returns director default childspec. %% @end get_default_childspec(Director, Timeout) -> do_call(Director, ?GET_DEF_CHILDSPEC, Timeout). -spec change_default_childspec(director(), default_childspec(), timeout()) -> {'ok', default_childspec()} | {'error', Reason::term()}. %% @doc %% Changes director default childspec. %% Be careful about using this function. This will change all children with append => true in %% next restart. %% @end change_default_childspec(Director, ChildSpec, Timeout) -> do_call(Director, {?CHANGE_DEF_CHILDSPEC, ChildSpec}, Timeout). %% ------------------------------------------------------------------------------------------------- %% 'gen' callback: %% @hidden init_it(Starter, self, Name, Mod, InitArg, Opts) -> init_it(Starter, erlang:self(), Name, Mod, InitArg, Opts); init_it(Starter, Parent, Name0, Mod, InitArg, Opts) -> Name = name(Name0), LogValidator = director_utils:get_log_validator(Name, Opts, ?DEF_LOG_VALIDATOR), TabType = director_utils:get_table_type(Name, Opts, ?DEF_TABLE_TYPE), Dbg = director_utils:get_debug_options(Name, Opts, ?DEF_DEBUG_OPTIONS), erlang:process_flag(trap_exit, true), case init_module(Mod, InitArg) of {ok, Children, DefChildSpec} -> case director_table:create(TabType) of {ok, Tab} -> case start_children(Name, Children, Tab, TabType) of {ok, Tab2} -> State = #?STATE{name = Name ,module = Mod ,init_argument = InitArg ,table = Tab2 ,default_childspec = DefChildSpec ,log_validator = LogValidator ,table_type = TabType}, proc_lib:init_ack(Starter, {ok, erlang:self()}), %% exit(element(2, (catch loop(Parent, Dbg, State)))); loop(Parent, Dbg, State); {error, Reason}=Error -> case director_utils:run_log_validator(LogValidator,error , Reason) of none -> ok; _ -> error_logger:error_msg("** Director ~p terminating in initializ" "e state\n" "** Reason for termination == ~p\n" ,[Name,Reason]) end, unregister_name(Name0), proc_lib:init_ack(Starter, Error), erlang:exit(Reason) end; {error, Reason}=Err -> unregister_name(Name0), proc_lib:init_ack(Starter, Err), erlang:exit(Reason) end; Other -> Reason = case Other of {error, Reason2} -> Reason2; ignore -> normal end, case director_utils:run_log_validator(LogValidator, error, Reason) of none -> ok; _ -> error_logger:error_msg("** Director ~p terminating in initialize state\n" "** Reason for termination == ~p\n" ,[Name,Reason]) end, unregister_name(Name0), proc_lib:init_ack(Starter, Other), erlang:exit(Reason) end. %% ------------------------------------------------------------------------------------------------- %% 'sys' callbacks: %% @hidden system_continue(Parent, Dbg, State) -> loop(Parent, Dbg, State). %% @hidden system_terminate(Reason, _Parent, Dbg, State) -> terminate(Dbg, State, Reason). %% @hidden system_get_state(State) -> {ok, State}. %% @hidden system_replace_state(ReplaceStateFun, State) -> NewState = ReplaceStateFun(State), {ok, NewState, NewState}. %% @hidden system_code_change(#?STATE{module = Mod ,init_argument = InitArg ,table = Tab ,table_type = TabType}=State ,_Module ,_OldVsn ,_Extra) -> case init_module(Mod, InitArg) of {ok, Children, DefChildSpec} -> case check_duplicate_ids(Children) of ok -> {ok, State#?STATE{table = change_old_children_pids(Children, Tab, TabType) ,default_childspec = DefChildSpec}}; {error, _}=Err -> Err end; ignore -> {ok, State}; {error, _}=Err -> Err end. %% @hidden format_status(_, [_PDict, SysState, Parent, Debug, #?STATE{name = Name, module = Mod}=State]) -> Header = {header, "Status for director " ++ io_lib:print(Name)}, Data = {data, [{"Status", SysState} ,{"Parent", Parent} ,{"Logged events", sys:get_debug(log, Debug, [])}]}, State2 = case string:to_integer(erlang:system_info(otp_release)) of {Ver, []} when Ver >= 19-> {state ,name ,strategy ,children ,dynamics ,intensity ,period ,restarts ,Mod ,Mod ,args}; _ -> State end, Specific = [{data, [{"State", State2}]} ,{supervisor, [{"Callback", Mod}]}], [Header, Data, Specific]. %% ------------------------------------------------------------------------------------------------- %% Process main loop and its main subcategories: loop(Parent, Dbg, State) -> process_message(Parent, Dbg, State, receive Msg -> Msg end). process_message(Parent ,Dbg ,#?STATE{name = Name}=State ,{?GEN_CALL_TAG, From, Request}=Msg) -> {Dbg3, State2} = process_request(director_utils:debug(Dbg, Name, Msg), State, From, Request), loop(Parent, Dbg3, State2); process_message(Parent, Dbg, #?STATE{name= Name}=State, {'EXIT', Pid, Reason}=Msg) -> process_exit(Parent, director_utils:debug(Dbg, Name, Msg), State, Pid, Reason); process_message(Parent, Dbg, #?STATE{name = Name}=State, {timeout, TimerRef, Id}=Msg) -> {Dbg2, State2} = process_timeout(director_utils:debug(Dbg, Name, Msg), State, TimerRef, Id), loop(Parent, Dbg2, State2); process_message(Parent, Dbg, State, {cancel_timer, _TimerRef, _Result}) -> loop(Parent, Dbg, State); process_message(Parent, Dbg, State, {system, From, Msg}) -> sys:handle_system_msg(Msg, From, Parent, ?MODULE, Dbg, State); %% Catch clause: process_message(Parent, Dbg, #?STATE{name = Name, log_validator = LogValidator}=State, Msg) -> case director_utils:run_log_validator(LogValidator ,warning ,{receive_unexpected_message, Msg}) of short -> error_logger:error_msg("Director ~p received an unexpected message~n", [Name]); none -> ok; long -> error_logger:error_msg("Director ~p received unexpected message: ~p~n", [Name, Msg]) end, loop(Parent, Dbg, State). process_request(Dbg ,#?STATE{table = Table, name = Name, table_type = TabType}=State ,From ,{?GET_PID_TAG, Id}) -> Result = case director_table:lookup(Table, Id, TabType) of not_found -> {error, not_found}; #?CHILD{pid = Pid} when erlang:is_pid(Pid) -> {ok, Pid}; #?CHILD{pid = Other} -> {error, Other} end, {reply(Dbg, Name, From, Result), State}; process_request(Dbg ,#?STATE{table = Tab, name = Name, table_type = TabType}=State ,From ,?GET_PIDS_TAG) -> Pids = [{Id, Pid} || #?CHILD{pid = Pid, id = Id} <- director_table:tab2list(Tab, TabType) , erlang:is_pid(Pid)], {reply(Dbg, Name, From, Pids), State}; process_request(Dbg ,#?STATE{table = Tab, name = Name, table_type = TabType}=State ,From ,?COUNT_CHILDREN_TAG) -> Specs = director_table:count(Tab, TabType), Fun = fun(#?CHILD{pid = Pid, type = Type}, {Actives, Sups, Workers}) -> Actives2 = if erlang:is_pid(Pid) -> Actives+1; true -> Actives end, {Sups2, Workers2} = if Type =:= supervisor -> {Sups+1, Workers}; Type =:= worker -> {Sups, Workers+1} end, {Actives2, Sups2, Workers2} end, {Actives, Sups, Workers} = lists:foldl(Fun, {0, 0, 0}, director_table:tab2list(Tab, TabType)), Result = [{specs, Specs} ,{active, Actives} ,{supervisors, Sups} ,{workers, Workers}], {reply(Dbg, Name, From, Result), State}; process_request(Dbg ,#?STATE{table = Tab, name = Name, table_type = TabType}=State ,From ,{?DELETE_CHILD_TAG, Id}) -> {Result, State2} = case director_table:lookup(Tab, Id, TabType) of not_found -> {{error, not_found}, State}; #?CHILD{pid = Pid} when erlang:is_pid(Pid) -> {{error, running}, State}; _Child -> Tab2 = director_table:delete(Tab, Id, TabType), {ok, State#?STATE{table = Tab2}} end, {reply(Dbg, Name, From, Result), State2}; process_request(Dbg ,#?STATE{table = Table, name = Name, table_type = TabType}=State ,From ,{?GET_CHILDSPEC_TAG, Term}) -> Result = case director_table:lookup(Table, Term, TabType) of not_found -> if erlang:is_pid(Term) -> case director_table:lookup_by_pid(Table, Term, TabType) of not_found -> {error, not_found}; Child -> {ok, director_utils:c2cs(Child)} end; true -> {error, not_found} end; Child -> {ok, director_utils:c2cs(Child)} end, {reply(Dbg, Name, From, Result), State}; process_request(Dbg ,#?STATE{table = Tab ,name = Name ,table_type = TabType}=State ,From ,{?RESTART_CHILD_TAG, Id}) -> {Result, State2} = case do_restart_child(Name, Id, Tab, TabType) of {ok, Pid, Tab2} -> {{ok, Pid}, State#?STATE{table = Tab2}}; {ok, Pid, Extra, Tab2} -> {{ok, Pid, Extra}, State#?STATE{table = Tab2}}; {error, _}=Error -> {Error, State} end, {reply(Dbg ,Name ,From ,Result), State2}; process_request(Dbg ,#?STATE{table = Tab ,name = Name ,default_childspec = DefChildSpec ,table_type = TabType}=State ,From ,{?START_CHILD_TAG, ChildSpec}) -> {Result, State2} = case director_utils:check_childspec(ChildSpec, DefChildSpec) of {ok, Child} -> case do_start_child(Name, Child, Tab, TabType) of {ok, Pid, Tab2} -> {{ok, Pid}, State#?STATE{table = Tab2}}; {ok, Pid, Extra, Tab2} -> {{ok, Pid, Extra}, State#?STATE{table = Tab2}}; {error, _}=Error -> {Error, State} end; {error, _}=Error -> {Error, State} end, {reply(Dbg, Name, From, Result), State2}; process_request(Dbg ,#?STATE{table = Tab ,name = Name ,table_type = TabType}=State ,From ,{?TERMINATE_CHILD_TAG, Term}) -> {Result, State2} = case do_terminate_child(Name, Term, Tab, TabType) of not_found -> {{error, not_found}, State}; Tab2 -> {ok, State#?STATE{table = Tab2}} end, {reply(Dbg, Name, From, Result), State2}; process_request(Dbg ,#?STATE{name = Name, table = Tab, table_type = TabType}=State ,From ,?WHICH_CHILDREN_TAG) -> Result = [{Id, Pid, Type, Mods} || #?CHILD{id = Id ,pid = Pid ,type = Type ,modules = Mods} <- director_table:tab2list(Tab, TabType)], {reply(Dbg, Name, From, Result), State}; process_request(Dbg ,#?STATE{table = Table, name = Name, table_type = TabType}=State ,From ,{?GET_PLAN_TAG, Id}) -> Result = case director_table:lookup(Table, Id, TabType) of not_found -> {error, not_found}; #?CHILD{plan = Plan} -> {ok, Plan} end, {reply(Dbg, Name, From, Result), State}; process_request(Dbg ,#?STATE{table = Table, name = Name, table_type = TabType}=State ,From ,{?CHANGE_PLAN_TAG, Id, Plan}) -> {Result, State2} = case director_utils:filter_plan(Plan) of {ok, Plan2} -> case director_table:lookup(Table, Id, TabType) of not_found -> {{error, not_found}, State}; Child -> PlanElemIndex = if Plan =:= [] -> 0; true -> 1 end, PlanLen = erlang:length(Plan2), Child2 = Child#?CHILD{plan = Plan2 ,plan_element_index = PlanElemIndex ,plan_length = PlanLen}, Tab2 = director_table:insert(Table, Child2, TabType), {ok, State#?STATE{table = Tab2}} end; {error, _Reason}=Error -> {Error, State} end, {reply(Dbg, Name, From, Result), State2}; process_request(Dbg ,#?STATE{table = Table, name = Name, table_type = TabType}=State ,From ,{?CHANGE_COUNT_TAG, Id, Count0}) -> CountCheck = case director_utils:is_whole_integer(Count0) of true -> {ok, Count0}; false when Count0 == infinity -> {ok, Count0}; false -> error end, {Result, State2} = case CountCheck of {ok, Count} -> case director_table:lookup(Table, Id, TabType) of not_found -> {{error, not_found}, State}; Child -> Tab2 = director_table:insert(Table,Child#?CHILD{count = Count}, TabType), {ok, State#?STATE{table = Tab2}} end; error -> {{error, {count_format, [{count, Count0}]}}, State} end, {reply(Dbg, Name, From, Result), State2}; process_request(Dbg ,#?STATE{table = Table, name = Name, table_type = TabType}=State ,From ,{?GET_COUNT_TAG, Id}) -> Result = case director_table:lookup(Table, Id, TabType) of not_found -> {error, not_found}; #?CHILD{count = Count} -> {ok, Count} end, {reply(Dbg, Name, From, Result), State}; process_request(Dbg ,#?STATE{name = Name, default_childspec = DefChildSpec}=State ,From ,?GET_DEF_CHILDSPEC) -> {reply(Dbg, Name, From, DefChildSpec), State}; process_request(Dbg ,#?STATE{table = Tab ,name = Name ,default_childspec = DefChildSpec ,table_type = TabType}=State ,From ,{?CHANGE_DEF_CHILDSPEC, ChildSpec}) -> {State2, Result} = case director_utils:check_default_childspec(ChildSpec) of {ok, DefChildSpec2} -> Tab2 = director_table:separate_children(DefChildSpec, Tab, TabType), Tab3 = director_table:combine_children(DefChildSpec2, Tab2, TabType), {State#?STATE{default_childspec = DefChildSpec2, table = Tab3}, ok}; {error, _Reason}=Error -> {State, Error} end, {reply(Dbg, Name, From, Result), State2}; %%process_request(Dbg %% ,#?STATE{name = Name, log_validator = LogValidator}=State %% ,From %% ,?GET_LOG_VALIDATOR) -> %% {reply(Dbg, Name, From, LogValidator), State}; process_request(Dbg ,#?STATE{name = Name}=State ,From ,{?CHANGE_LOG_VALIDATOR, LogValidator}) -> {State2, Result} = if erlang:is_function(LogValidator, 2) -> {State#?STATE{log_validator = LogValidator}, ok}; true -> {State, {error, {bad_log_validator, [{log_validator, LogValidator}]}}} end, {reply(Dbg, Name, From, Result), State2}; process_request(Dbg ,#?STATE{name = Name, table = Tab, table_type = TabType}=State ,From ,{?CHANGE_LOG_VALIDATOR, LogValidator, Id}) -> Result = case director_table:lookup(Tab, Id, TabType) of not_found -> {error, not_found}; Child -> if erlang:is_function(LogValidator, 2) -> Tab2 = director_table:insert(Tab ,Child#?CHILD{log_validator = LogValidator} ,TabType), {State#?STATE{table = Tab2}, ok}; true -> {State, {error, {bad_log_validator, [{log_validator, LogValidator}]}}} end end, {reply(Dbg, Name, From, Result), State}; %% Catch clause: process_request(Dbg, #?STATE{name = Name, log_validator = LogValidator}=State, From, Other) -> case director_utils:run_log_validator(LogValidator ,warning ,{receive_unexpected_call, From, Other}) of short -> error_logger:error_msg("Director ~p received an unexpected call request~n", [Name]); none -> ok; long -> error_logger:error_msg("Director ~p received unexpected call request ~p with from ~p~n" ,[Name, Other, From]) end, {reply(Dbg, Name, From, {error, {unknown_request, Other}}), State}. process_exit(Parent, Dbg, State, Parent, Reason) -> terminate(Dbg, State, Reason); process_exit(Parent ,Dbg ,#?STATE{table = Tab ,name = Name ,table_type = TabType}=State ,Pid ,Reason) -> case director_table:lookup_by_pid(Tab, Pid, TabType) of not_found -> loop(Parent, Dbg, State); Child -> director_utils:error_report(Name ,child_terminated ,Reason ,Child), Child2 = Child#?CHILD{pid = undefined, extra = undefined}, Tab2 = director_table:insert(Tab, Child2, TabType), {Dbg2, State2} = handle_exit(Dbg, State#?STATE{table = Tab2}, Child2, Reason), loop(Parent, Dbg2, State2) end. handle_exit(Dbg ,State ,#?CHILD{plan = []}=Child ,Reason) -> Reason2 = {empty_plan_child_terminated, [{reason, Reason}|director_utils:c_r2p(Child, long)]}, terminate(Dbg, State, Reason2); handle_exit(Dbg ,State ,#?CHILD{count = Count ,count2 = _Count2 = Count}=Child ,Reason) -> Reason2 = {reached_max_restart_plan, [{reason, Reason}|director_utils:c_r2p(Child, long)]}, terminate(Dbg, State, Reason2); handle_exit(Dbg ,#?STATE{name = Name ,table = Table ,table_type = TabType}=State ,#?CHILD{id = Id ,plan = Plan ,count2 = Count2 ,restart_count = ResCount ,plan_element_index = PlanElemIndex ,plan_length = PlanLen}=Child ,Reason) -> PlanElem = lists:nth(PlanElemIndex, Plan), {PlanElemIndex2, Count2_2, ResCount2} = if PlanElemIndex =:= PlanLen -> {1, Count2+1, ResCount + 1}; true -> {PlanElemIndex+1, Count2, ResCount + 1} end, Child2 = Child#?CHILD{plan_element_index = PlanElemIndex2 ,count2 = Count2_2 ,restart_count = ResCount2}, Tab2 = director_table:insert(Table, Child2, TabType), Strategy = case PlanElem of Fun when erlang:is_function(Fun) -> case catch Fun(Id, Reason, ResCount2) of Term when erlang:is_function(Term) -> {error, {fun_bad_return, [{returned_value, Term} ,{'fun', Fun} ,{arguments, [Id, Reason, ResCount2]}]}}; {'EXIT', Reason2} -> {error, {fun_crash, [{reason, Reason2} ,{'fun', Fun} ,{arguments, [Reason, ResCount2]}]}}; Term -> case director_utils:filter_plan_element(Term) of {ok, Term2} -> Term2; {error, _Reason2} -> {error, {fun_bad_return, [{returned_value, Term} ,{'fun', Fun} ,{arguments, [Id, Reason, ResCount2]}]}} end end; _Other -> PlanElem end, _ = director_utils:debug(Dbg, Name, {plan, Id, Strategy}), case Strategy of restart -> Tab3 = case do_restart_child(Name, Id, Tab2, TabType) of {error, _Reason3} -> TimeRef = restart_timer(0, Id), director_table:insert(Tab2 ,Child2#?CHILD{pid = restarting ,timer_reference = TimeRef} ,TabType); {ok, _Pid, Tab4} -> Tab4; {ok, _Pid, _Extra, Tab4} -> Tab4 end, {Dbg, State#?STATE{table = Tab3}}; stop -> terminate(Dbg, State#?STATE{table = Tab2}, Reason); {stop, Reason3} -> terminate(Dbg, State, Reason3); delete -> Tab3 = director_table:delete(Tab2, Id, TabType), {Dbg, State#?STATE{table = Tab3}}; wait -> {Dbg, State#?STATE{table = Tab2}}; {restart, PosInt} -> TimeRef = restart_timer(PosInt, Id), Tab3 = director_table:insert(Tab2 ,Child2#?CHILD{pid = restarting ,timer_reference = TimeRef} ,TabType), {Dbg, State#?STATE{table = Tab3}}; {error, Reason3} -> terminate(Dbg ,State#?STATE{table = Tab2} ,{run_plan_element, [{reason, Reason3} ,{child_last_error_reason, Reason} |director_utils:c_r2p(Child2, long)]}) end. process_timeout(Dbg ,#?STATE{name = Name ,table = Tab ,table_type = TabType}=State ,TimerRef ,Id) -> case director_table:lookup(Tab, Id, TabType) of not_found -> {Dbg, State}; #?CHILD{timer_reference = TimerRef} -> case do_restart_child(Name, Id, Tab, TabType) of %% {error, not_found} -> %% {Dbg, State}; {error, Reason} -> handle_exit(Dbg ,State ,director_table:lookup(Tab, Id, TabType) ,Reason); {ok, _Pid, Tab2} -> {Dbg, State#?STATE{table = Tab2}}; {ok, _Pid, _Extra, Tab2} -> {Dbg, State#?STATE{table = Tab2}} end; _Child -> {Dbg, State} end. %% ------------------------------------------------------------------------------------------------- %% Other internal functions: do_call(Name, Request) -> do_call(Name, Request, ?DEF_CALL_TIMEOUT). do_call(Name, Request, Timeout) -> case catch gen:call(Name, ?GEN_CALL_TAG, Request, Timeout) of {ok, Res} -> Res; {'EXIT', Reason} -> erlang:exit({call_request, [{reason, Reason} ,{remote_process, Name} ,{request, Request}]}) end. name({local, Name}) -> Name; name({global, Name}) -> Name; name({via, _Mod, Name}) -> Name; name(Other) -> Other. unregister_name({local,Name}) -> try unregister(Name) of _Result -> ok catch _ErrorType:_Reason -> ok end; unregister_name({global,Name}) -> catch global:unregister_name(Name), ok; unregister_name({via, Mod, Name}) -> catch Mod:unregister_name(Name), ok; unregister_name(_Other) -> ok. init_module(Mod, InitArg) -> case catch Mod:init(InitArg) of {ok, ChildSpecs} -> case director_utils:check_childspecs(ChildSpecs) of {ok, ChildSpecs2} -> {ok, ChildSpecs2, ?DEF_DEF_CHILDSPEC}; {error, _Reason}=Error -> Error end; {ok, ChildSpecs, DefChildSpec} -> case director_utils:check_default_childspec(DefChildSpec) of {ok, DefChildSpec2} -> case director_utils:check_childspecs(ChildSpecs, DefChildSpec2) of {ok, ChildSpecs2} -> {ok, ChildSpecs2, DefChildSpec2}; {error, _Reason}=Error -> Error end; {error, _Reason}=Error -> Error end; ignore -> ignore; {stop, Reason} -> {error, Reason}; {'EXIT', Reason} -> {error, Reason}; Other -> {error, {init_bad_return, [{returned_value, Other} ,{module, Mod} ,{function, init} ,{argument, InitArg}]}} end. start_children(Name, [#?CHILD{id=Id}=Child|Children], Tab, TabType) -> case do_start_child(Name, Child, Tab, TabType) of {ok, _Pid, Tab2} -> start_children(Name, Children, Tab2, TabType); {error, already_present} -> {error, {duplicate_child_name, Id}}; % Like OTP/supervisor {error, {already_started, _}} -> {error, {duplicate_child_name, Id}}; % Like OTP/supervisor {error, _Reason}=Error -> _ = terminate_children(Name, Tab, TabType), Error end; start_children(_Name, [], Tab, _TabType) -> {ok, Tab}. do_start_child(Name, #?CHILD{id = Id}=Child ,Tab, TabType) -> case director_table:lookup(Tab, Id, TabType) of not_found -> start_mfa(Name, Child, Tab, TabType); #?CHILD{pid = Pid} when erlang:is_pid(Pid) -> {error, {already_started, Pid}}; _Child -> {error, already_present} end. do_restart_child(Name, Id, Tab, TabType) -> case director_table:lookup(Tab, Id, TabType) of not_found -> {error, not_found}; #?CHILD{pid = Pid} when erlang:is_pid(Pid) -> {error, running}; #?CHILD{pid = restarting, timer_reference = Ref}=Child -> _ = erlang:cancel_timer(Ref, [{async, true}]), start_mfa(Name ,Child#?CHILD{pid = undefined ,timer_reference = undefined} ,Tab ,TabType); Child -> start_mfa(Name, Child, Tab, TabType) end. start_mfa(Name ,#?CHILD{start = {Mod, Func, Args}}=Child ,Tab ,TabType) -> case catch erlang:apply(Mod, Func, Args) of {ok, Pid} when erlang:is_pid(Pid) -> Child2 = Child#?CHILD{pid = Pid, extra = undefined}, Tab2 = director_table:insert(Tab, Child2, TabType), director_utils:progress_report(Name, Child2), {ok, Pid, Tab2}; {ok, Pid, Extra} when erlang:is_pid(Pid) -> Child2 = Child#?CHILD{pid = Pid, extra = {extra, Extra}}, Tab2 = director_table:insert(Tab, Child2, TabType), director_utils:progress_report(Name, Child2), {ok, Pid, Extra, Tab2}; ignore -> {ok, undefined, Tab}; {error, _Reason}=Error -> Error; {'EXIT', Reason} -> {error, Reason}; Other -> {error, {start_bad_return, [{returned_value, Other} ,{module, Mod} ,{function, Func} ,{arguments, Args}]}} end. terminate(Dbg ,#?STATE{table = Tab ,name = Name ,log_validator = LogValidator ,table_type = TabType} ,Reason) -> Children = director_table:tab2list(Tab, TabType), _Tab2 = terminate_children(Name, Tab, TabType), case director_utils:run_log_validator(LogValidator, error, Reason) of none -> ok; short -> error_logger:error_msg("** Director ~p terminating \n" "** Reason for termination == ~p\n" ,[Name,Reason]); long -> ChildrenStr = string:join([io_lib:print(director_utils:c_r2p(Child, short)) || Child <- Children] ,"\n"), error_logger:error_msg("** Director \"~p\" terminating \n" "** Reason for termination == ~p\n" "** Children == \n~s\n" ,[Name, Reason, ChildrenStr]) end, sys:print_log(Dbg), erlang:exit(Reason). terminate_children(Name, Tab, TabType) -> Terminate = fun(Id, Tab2) -> case do_terminate_child(Name, Id, Tab2, TabType) of not_found -> Tab2; Tab3 -> Tab3 end end, _Tab3 = lists:foldl(Terminate, Tab, director_table:tab2list(Tab, TabType)). do_terminate_child(Name, Id_or_Pid, Tab, TabType) -> Search = case director_table:lookup(Tab, Id_or_Pid, TabType) of not_found -> director_table:lookup_by_pid(Tab, Id_or_Pid, TabType); Child -> Child end, _ = case Search of #?CHILD{pid = Pid}=Child2 when erlang:is_pid(Pid) -> ok = do_terminate_child(Name, Child2); #?CHILD{pid = restarting, timer_reference = Ref} -> _ = erlang:cancel_timer(Ref, [{async, true}]), ok; Other -> Other end, case Search of not_found -> not_found; Child3 -> _Tab2 = director_table:insert(Tab ,Child3#?CHILD{pid = undefined ,extra = undefined ,timer_reference = undefined} ,TabType) end. do_terminate_child(Name ,#?CHILD{pid=Pid ,terminate_timeout = TerminateTimeout}=Child) when erlang:is_pid(Pid) -> BrutalKill = fun() -> erlang:exit(Pid, kill), receive {'DOWN', _Ref, process, Pid, killed} -> ok; {'DOWN', _Ref, process, Pid, Reason} -> director_utils:error_report(Name, shutdown_error, Reason, Child), ok end end, case monitor_child(Pid) of ok -> if TerminateTimeout =:= 0 -> BrutalKill(); true -> erlang:exit(Pid, shutdown), receive {'DOWN', _Ref, process, Pid, shutdown} -> ok; {'DOWN', _Ref, process, Pid, Reason2} -> director_utils:error_report(Name ,shutdown_error ,Reason2 ,Child), ok after TerminateTimeout -> BrutalKill() end end; {error, Reason3} -> director_utils:error_report(Name, shutdown_error, Reason3, Child), ok end. monitor_child(Pid) -> erlang:monitor(process, Pid), catch erlang:unlink(Pid), receive {'EXIT', Pid, Reason} -> receive {'DOWN', _Ref, process, Pid, _Reason} -> {error, Reason} end after 0 -> ok end. restart_timer(PosInt, Id) -> erlang:start_timer(PosInt, erlang:self(), Id, []). reply(Dbg, Name, {Pid, Tag}=_From, Result) -> Pid ! {Tag, Result}, director_utils:debug(Dbg, Name, {out, Pid, Result}). check_duplicate_ids(Children) -> check_duplicate_ids([Id || #?CHILD{id = Id} <- Children], []). check_duplicate_ids([Id|Ids], Ids2) -> case lists:member(Id, Ids2) of true -> {error, {duplicate_child_name, Id}}; %% Like OTP/supervisor false -> check_duplicate_ids(Ids, [Id|Ids2]) end; check_duplicate_ids([], _Ids2) -> ok. change_old_children_pids([#?CHILD{id = Id}=Child|Children], Tab, TabType) -> Pid = case director_table:lookup(Tab, Id, TabType) of not_found -> undefined; #?CHILD{pid = Pid2} -> Pid2 end, Tab2 = director_table:insert(Tab, Child#?CHILD{pid = Pid}, TabType), change_old_children_pids(Children, Tab2, TabType); change_old_children_pids([], Tab, _TabType) -> Tab.