Current section
Files
Jump to
Current section
Files
src/director.erl
%%% --------------------------------------------------------------------
%%% BSD 3-Clause 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 <pouriya.jahanbakhsh@gmail.com>
%% @version 17.8.9
%% @doc
%% Flexible, fast and powerful process supervisor.
%% @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
,get_debug_mode/1
,change_debug_mode/2
,start_link/4
,start/2
,start/3
,start/4
,stop/1
,stop/2
,stop/3
,default_plan_element_fun/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
,get_debug_mode/2
,change_debug_mode/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]).
%% ---------------------------------------------------------------------
%% Types:
%% 'id' is mandatory.
%% If you don't set 'start' for default childspec, 'start' is mandatory.
%% Default values for other keys:
%% #{plan => [fun
%% (normal, _RestartCount) ->
%% delete;
%% (shutdown, _RestartCount) ->
%% delete;
%% ({shutdown, _Reason}, _RestartCount) ->
%% delete;
%% (_Reason, _RestartCount) ->
%% restart
%% end]
%% ,count => 1
%% ,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()}.
-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'}
| {'stop', Reason::term()}
| fun((Reason::term()
,RestartCount::pos_integer()) ->
'restart'
| {'restart', pos_integer()}
| 'wait'
| 'stop'
| {'stop', 'reason'}
| {'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()}.
-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()
| debug_mode_option().
-type debug_option() :: {'debug'
,['trace'
|'log'
|'statistics'
|'debug']
|[]}.
-type spawn_options() :: {'spawn_opt', proc_lib:spawn_option()}.
-type timeout_option() :: {'timeout', timeout()}.
-type debug_mode_option() :: {'debug_mode', debug_mode()}.
-type debug_mode() :: 'short' | 'long' | 'off'.
-export_type([childspec/0
,default_childspec/0
,start_options/0
,start_return/0
,debug_mode/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").
-record(director_state_record, {name
,module
,init_argument
,table
,default_childspec
,debug_mode}).
-define(STATE, director_state_record).
%% ---------------------------------------------------------------------
%% 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, ?DEFAULT_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
,?DEFAULT_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_check:check_childspec(ChildSpec
,?DEFAULT_DEFAULT_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_DEFAULT_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 next
%% restart.
%% @end
change_default_childspec(Director, DefChildSpec) ->
do_call(Director, {?CHANGE_DEFAULT_CHILDSPEC, DefChildSpec}).
-spec
get_debug_mode(director()) ->
debug_mode().
%% @doc
%% Gets mode of debug for director.
%% @end
get_debug_mode(Director) ->
do_call(Director, ?GET_DEBUG_MODE_TAG).
-spec
change_debug_mode(director(), debug_mode()) ->
'ok' | {'error', term()}.
%% @doc
%% Changes debug_mode of director.
%% @end
change_debug_mode(Director, DbgMode) ->
do_call(Director, {?CHANGE_DEBUG_MODE_TAG, DbgMode}).
-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, ?DEFAULT_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
,?DEFAULT_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, ?DEFAULT_STOP_TIMEOUT).
-spec
stop(director(), Reason::term()) ->
'ok'.
%% @doc
%% Stops a director with 5000 mili seconds timeout.
%% @end
stop(Director, Reason) ->
proc_lib:stop(Director, Reason, ?DEFAULT_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
default_plan_element_fun(Reason::term()
,RestartCount::non_neg_integer()) ->
'delete' | 'restart'.
%% @doc
%% Deletes child if it crashed with reasons 'normal', 'shutdown' or
%% {'shutdown', Any} and restart it if crashed with other reasons.
%% @end
default_plan_element_fun(normal, _RestartCount) ->
delete;
default_plan_element_fun(shutdown, _RestartCount) ->
delete;
default_plan_element_fun({shutdown, _Reason}, _RestartCount) ->
delete;
default_plan_element_fun(_Other, _Count) ->
restart.
%% ---------------------------------------------------------------------
%% 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_DEFAULT_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_DEFAULT_CHILDSPEC, ChildSpec}, Timeout).
-spec
get_debug_mode(director(), timeout()) ->
debug_mode().
%% @doc
%% Gets mode of debug for director.
%% @end
get_debug_mode(Director, Timeout) ->
do_call(Director, ?GET_DEBUG_MODE_TAG, Timeout).
-spec
change_debug_mode(director(), debug_mode(), timeout()) ->
'ok' | {'error', term()}.
%% @doc
%% Changes debug_mode of director.
%% @end
change_debug_mode(Director, DbgMode, Timeout) ->
do_call(Director, {?CHANGE_DEBUG_MODE_TAG, DbgMode}, 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),
DbgMode = director_check:get_debug_mode(Name
,Opts
,?DEFAULT_DEBUG_MODE),
Dbg = director_debug:debug_options(Name, Opts),
erlang:process_flag(trap_exit, true),
case init_module(Mod, InitArg) of
{ok, Children, DefChildSpec} ->
Table = director_table:create(),
case start_children(Name, Children, Table, DbgMode) of
ok ->
State = #?STATE{name = Name
,module = Mod
,init_argument = InitArg
,table = Table
,default_childspec = DefChildSpec
,debug_mode = DbgMode},
proc_lib:init_ack(Starter, {ok, erlang:self()}),
%exit(element(2, (catch loop(Parent, Dbg, State))));
loop(Parent, Dbg, State);
{error, Reason}=Error ->
unregister_name(Name0),
proc_lib:init_ack(Starter, Error),
erlang:exit(Reason)
end;
ignore ->
proc_lib:init_ack(Starter, ignore),
erlang:exit(normal);
{error, Reason}=Error ->
unregister_name(Name0),
proc_lib:init_ack(Starter, Error),
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|Rest]) ->
NewState = ReplaceStateFun(State),
{ok, NewState, [NewState|Rest]}.
%% @hidden
system_code_change([#?STATE{module = Mod
,init_argument = InitArg
,table = Table}=State|Rest]
,_Module
,_OldVsn
,_Extra) ->
case init_module(Mod, InitArg) of
{ok, Children1, DefChildSpec} ->
case get_children(Children1, Table) of
{ok, ChildrenR2} ->
UpdateTable =
fun(#?CHILD{pid = Pid, extra = Extra}=LastChild
,CurrentChild) ->
director_table:delete(Table, LastChild),
NewChild =
CurrentChild#?CHILD{pid = Pid
,extra = Extra},
director_table:insert(Table, NewChild)
end,
[UpdateTable(LastChild, CurrentChild)
|| {LastChild, CurrentChild} <- ChildrenR2],
{ok
,[State#?STATE{default_childspec = DefChildSpec}
|Rest]};
{error, _Reason}=Error ->
Error
end;
ignore ->
{ok, [State|Rest]};
{error, _Reason}=Error ->
Error
end.
%% ---------------------------------------------------------------------
%% 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_debug: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_debug:debug(Dbg, Name, Msg)
,State
,Pid
,Reason);
process_message(Parent
,Dbg
,#?STATE{name = Name}=State
,{timeout, TimerRef, Id}=Msg) ->
{Dbg2, State2} = process_timeout(director_debug: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{module = Mod}=State
,{system, From, Msg}) ->
sys:handle_system_msg(Msg
,From
,Parent
,?MODULE
,Dbg
,[State, {supervisor, [{"Callback", Mod}]}]);
%% Catch clause:
process_message(Parent, Dbg, #?STATE{name = Name}=State, Msg) ->
error_logger:error_msg("Director \"~p\" received unexpected message"
": \"~p\"~n"
,[Name, Msg]),
loop(Parent, Dbg, State).
process_request(Dbg
,#?STATE{table = Table, name = Name}=State
,From
,{?GET_PID_TAG, Id}) ->
Result =
case director_table:lookup(Table, Id) 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 = Table, name = Name}=State
,From
,?GET_PIDS_TAG) ->
Pids = [{Id, Pid} || #?CHILD{pid = Pid, id = Id}
<- director_table:tab2list(Table), erlang:is_pid(Pid)],
{reply(Dbg, Name, From, Pids), State};
process_request(Dbg
,#?STATE{table = Table, name = Name}=State
,From
,?COUNT_CHILDREN_TAG) ->
Specs = director_table:count(Table),
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} = ets:foldl(Fun, {0, 0, 0}, Table),
Result = [{specs, Specs}
,{active, Actives}
,{supervisors, Sups}
,{workers, Workers}],
{reply(Dbg, Name, From, Result), State};
process_request(Dbg
,#?STATE{table = Table, name = Name}=State
,From
,{?DELETE_CHILD_TAG, Id}) ->
Result =
case director_table:lookup(Table, Id) of
not_found ->
{error, not_found};
#?CHILD{pid = Pid} when erlang:is_pid(Pid) ->
{error, running};
_Child ->
ok = director_table:delete(Table, Id)
end,
{reply(Dbg, Name, From, Result), State};
process_request(Dbg
,#?STATE{table = Table, name = Name}=State
,From
,{?GET_CHILDSPEC_TAG, Term}) ->
Result =
case director_table:lookup(Table, Term) of
not_found ->
if
erlang:is_pid(Term) ->
case director_table:lookup_by_pid(Table
,Term) of
not_found ->
{error, not_found};
Child ->
{ok, director_wrapper:c2cs(Child)}
end;
true ->
{error, not_found}
end;
Child ->
{ok, director_wrapper:c2cs(Child)}
end,
{reply(Dbg, Name, From, Result), State};
process_request(Dbg
,#?STATE{table = Table
,name = Name
,debug_mode = DbgMode}=State
,From
,{?RESTART_CHILD_TAG, Id}) ->
{reply(Dbg
,Name
,From
,do_restart_child(Name, Id, Table, DbgMode))
,State};
process_request(Dbg
,#?STATE{table = Table
,name = Name
,default_childspec = DefChildSpec
,debug_mode = DbgMode}=State
,From
,{?START_CHILD_TAG, ChildSpec}) ->
Result =
case director_check:check_childspec(ChildSpec, DefChildSpec) of
{ok, Child} ->
do_start_child(Name, Child, Table, DbgMode);
{error, _Reason}=Error ->
Error
end,
{reply(Dbg, Name, From, Result), State};
process_request(Dbg
,#?STATE{table = Table
,name = Name
,debug_mode = DbgMode}=State
,From
,{?TERMINATE_CHILD_TAG, Term}) ->
Result =
case do_terminate_child(Name, Term, Table, DbgMode) of
ok ->
ok;
not_found ->
{error, not_found}
end,
{reply(Dbg, Name, From, Result), State};
process_request(Dbg
,#?STATE{name = Name, table = Table}=State
,From
,?WHICH_CHILDREN_TAG) ->
Result = [{Id, Pid, Type, Mods} || #?CHILD{id = Id
,pid = Pid
,type = Type
,modules = Mods}
<- director_table:tab2list(Table)],
{reply(Dbg, Name, From, Result), State};
process_request(Dbg
,#?STATE{table = Table, name = Name}=State
,From
,{?GET_PLAN_TAG, Id}) ->
Result =
case director_table:lookup(Table, Id) 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}=State
,From
,{?CHANGE_PLAN_TAG, Id, Plan}) ->
Result =
case director_check:filter_plan(Plan) of
{ok, Plan2} ->
case director_table:lookup(Table, Id) of
not_found ->
{error, not_found};
Child ->
PlanElemIndex =
if
Plan =:= [] ->
0;
true ->
1
end,
PlanLen = erlang:length(Plan2),
Child2 = Child#?CHILD{plan = Plan2
,plan_element_index =
PlanElemIndex
,plan_length = PlanLen},
ok = director_table:insert(Table, Child2)
end;
{error, _Reason}=Error ->
Error
end,
{reply(Dbg, Name, From, Result), State};
process_request(Dbg
,#?STATE{table = Table, name = Name}=State
,From
,{?CHANGE_COUNT_TAG, Id, Count0}) ->
CountCheck =
case director_check:is_whole_integer(Count0) of
true ->
{ok, Count0};
false when Count0 == infinity ->
{ok, Count0};
false ->
error
end,
Result =
case CountCheck of
{ok, Count} ->
case director_table:lookup(Table, Id) of
not_found ->
{error, not_found};
Child ->
ok = director_table:insert(Table
,Child#?CHILD{count
= Count})
end;
error ->
{error, {count_format, [{count, Count0}]}}
end,
{reply(Dbg, Name, From, Result), State};
process_request(Dbg
,#?STATE{table = Table, name = Name}=State
,From
,{?GET_COUNT_TAG, Id}) ->
Result =
case director_table:lookup(Table, Id) 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_DEFAULT_CHILDSPEC) ->
{reply(Dbg, Name, From, DefChildSpec), State};
process_request(Dbg
,#?STATE{table = Table
,name = Name
,default_childspec = DefChildSpec}=State
,From
,{?CHANGE_DEFAULT_CHILDSPEC, ChildSpec}) ->
{State2, Result} =
case director_check:check_default_childspec(ChildSpec) of
{ok, DefChildSpec2} ->
director_table:separate_children(DefChildSpec, Table),
director_table:combine_children(DefChildSpec2, Table),
{State#?STATE{default_childspec = DefChildSpec2}, ok};
{error, _Reason}=Error ->
{State, Error}
end,
{reply(Dbg, Name, From, Result), State2};
process_request(Dbg
,#?STATE{name = Name
,debug_mode = DbgMode}=State
,From
,?GET_DEBUG_MODE_TAG) ->
{reply(Dbg, Name, From, DbgMode), State};
process_request(Dbg
,#?STATE{name = Name}=State
,From
,{?CHANGE_DEBUG_MODE_TAG, DbgMode}) ->
{State2, Result} =
if
erlang:is_atom(DbgMode) ->
if
DbgMode == off orelse
DbgMode == short orelse
DbgMode == long ->
{State#?STATE{debug_mode = DbgMode}, ok};
true ->
{State
,{error
,{debug_mode_value
,[{debug_mode, DbgMode}]}}}
end ;
true ->
{State
,{error
,{debug_mode_type
,[{debug_mode, DbgMode}]}}}
end,
{reply(Dbg, Name, From, Result), State2};
%% Catch clause:
process_request(Dbg, #?STATE{name = Name}=State, From, Other) ->
error_logger:error_msg("Director \"~p\" received unexpected call: \"
""~p\"~n"
,[Name, Other]),
{reply(Dbg, Name, From, {error, {unknown_call, Other}}), State}.
process_exit(Parent, Dbg, State, Parent, Reason) ->
terminate(Dbg, State, Reason);
process_exit(Parent
,Dbg
,#?STATE{table = Table
,name = Name
,debug_mode = DbgMode}=State
,Pid
,Reason) ->
case director_table:lookup_by_pid(Table, Pid) of
not_found ->
loop(Parent, Dbg, State);
Child ->
director_debug:error_report(Name
,child_terminated
,Reason
,Child
,DbgMode),
Child2 = Child#?CHILD{pid = undefined, extra = undefined},
ok = director_table:insert(Table, Child2),
{Dbg2, State2} = handle_exit(Dbg, State, Child2, Reason),
loop(Parent, Dbg2, State2)
end.
handle_exit(Dbg
,#?STATE{name = Name, debug_mode = DbgMode}=State
,#?CHILD{plan = []}=Child
,Reason) ->
director_debug:error_report(Name
,empty_plan_child_terminated
,Reason
,Child
,DbgMode),
terminate(Dbg
,State
,{empty_plan_child_terminated
,[{child, director_wrapper:c_r2p(Child, long)}
,{child_last_error_reason, Reason}]});
handle_exit(Dbg
,#?STATE{name = Name, debug_mode = DbgMode}=State
,#?CHILD{count = Count
,count2 = _Count2 = Count}=Child
,Reason) ->
director_debug:error_report(Name
,reached_max_restart_plan
,Reason
,Child
,DbgMode),
terminate(Dbg
,State
,{reached_max_restart_plan
,[{child, director_wrapper:c_r2p(Child, long)}
,{child_last_error_reason, Reason}]});
handle_exit(Dbg
,#?STATE{name = Name
,table = Table
,debug_mode = DbgMode}=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},
ok = director_table:insert(Table, Child2),
Strategy =
case PlanElem of
Fun when erlang:is_function(Fun) ->
case catch Fun(Reason, ResCount2) of
Term when erlang:is_function(Term) ->
{error
,{fun_bad_return_value
,[{'fun', Fun}
,{arguments, [Reason, ResCount2]}
,{returned_value, Term}]}};
{'EXIT', Reason2} ->
{error
,{fun_run_crash
,[{'fun', Fun}
,{arguments, [Reason, ResCount2]}
,{reason, Reason2}]}};
Term ->
case director_check:filter_plan_element(Term) of
{ok, Term2} ->
Term2;
{error, Reason2} ->
{error
,{fun_bad_return_value
,[{'fun', Fun}
,{arguments, [Reason, ResCount2]}
,{return_value, Term}
,{reason, Reason2}]}}
end
end;
_Other ->
PlanElem
end,
_ = director_debug:debug(Dbg, Name, {plan, Id, Strategy}),
case Strategy of
restart ->
case do_restart_child(Name, Id, Table, DbgMode) of
{error, _Reason3} ->
TimeRef = restart_timer(0, Id),
ok = director_table:
insert(Table
,Child2#?CHILD{pid = restarting
,timer_reference =
TimeRef});
_Other2 ->
ok
end,
{Dbg, State};
stop ->
terminate(Dbg, State, Reason);
{stop, Reason3} ->
terminate(Dbg, State, Reason3);
delete ->
ok = director_table:delete(Table, Id),
{Dbg, State};
wait ->
{Dbg, State};
{restart, PosInt} ->
TimeRef = restart_timer(PosInt, Id),
ok = director_table:insert(Table
,Child2#?CHILD{pid = restarting
,timer_reference =
TimeRef}),
{Dbg, State};
{error, Reason3} ->
terminate(Dbg
,State
,{run_plan_element
,[{child, director_wrapper:c_r2p(Child2, long)}
,{child_last_error_reason, Reason}
,{run_plan_error_reason, Reason3}]})
end.
process_timeout(Dbg
,#?STATE{name = Name
,table = Table
,debug_mode = DbgMode}=State
,TimerRef
,Id) ->
case director_table:lookup(Table, Id) of
not_found ->
{Dbg, State};
#?CHILD{timer_reference = TimerRef} ->
case do_restart_child(Name, Id, Table, DbgMode) of
%% {error, not_found} ->
%% {Dbg, State};
{error, Reason} ->
handle_exit(Dbg
,State
,director_table:lookup(Table, Id)
,Reason);
{ok, _Pid} ->
{Dbg, State}
end;
_Child ->
{Dbg, State}
end.
%% ---------------------------------------------------------------------
%% Other internal functions:
do_call(Name, Request) ->
do_call(Name, Request, ?DEFAULT_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_crash, [{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_check:check_childspecs(ChildSpecs) of
{ok, ChildSpecs2} ->
{ok, ChildSpecs2, ?DEFAULT_DEFAULT_CHILDSPEC};
{error, _Reason}=Error ->
Error
end;
{ok, ChildSpecs, DefChildSpec} ->
case director_check:check_default_childspec(DefChildSpec) of
{ok, DefChildSpec2} ->
case director_check:
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], Table, DbgMode) ->
case do_start_child(Name, Child, Table, DbgMode) of
{ok, _Pid} ->
start_children(Name, Children, Table, DbgMode);
{error, already_present} ->
{error, {repeated_id, [{id, Id}]}};
{error, _Reason}=Error ->
_ = terminate_children(Name, Table, DbgMode),
Error
end;
start_children(_Name, [], _Table, _DbgMode) ->
ok.
do_start_child(Name, #?CHILD{id = Id}=Child ,Table, DbgMode) ->
case director_table:lookup(Table, Id) of
not_found ->
start_mfa(Name, Child, Table, DbgMode);
#?CHILD{pid = Pid} when erlang:is_pid(Pid) ->
{error, {already_started, Pid}};
_Child ->
{error, already_present}
end.
do_restart_child(Name, Id, Table, DbgMode) ->
case director_table:lookup(Table, Id) 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}
,Table
,DbgMode);
Child ->
start_mfa(Name, Child, Table, DbgMode)
end.
start_mfa(Name
,#?CHILD{start = {Mod, Func, Args}}=Child
,Table
,DbgMode) ->
case catch erlang:apply(Mod, Func, Args) of
{ok, Pid} when erlang:is_pid(Pid) ->
Child2 = Child#?CHILD{pid = Pid, extra = undefined},
ok = director_table:insert(Table, Child2),
director_debug:progress_report(Name, Child2, DbgMode),
{ok, Pid};
{ok, Pid, Extra} when erlang:is_pid(Pid) ->
Child2 = Child#?CHILD{pid = Pid
,extra = {extra, Extra}},
ok = director_table:insert(Table, Child2),
director_debug:progress_report(Name, Child2, DbgMode),
{ok, Pid, Extra};
ignore ->
{ok, undefined};
{error, _Reason}=Error ->
Error;
{'EXIT', Reason} ->
{error, Reason};
Other ->
{error
,{bad_start_return_value, [{returned_value, Other}
,{module, Mod}
,{function, Func}
,{arguments, Args}]}}
end.
terminate(Dbg
,#?STATE{table = Table
,name = Name
,debug_mode = DbgMode}=State
,Reason) ->
Children = director_table:tab2list(Table),
terminate_children(Name, Table, DbgMode),
error_logger:format("** Director \"~p\" terminating \n** Reason for"
" termination == \"~p\"~n** Children == \"~p\"~"
"n** State == \"~p\"~n"
,[Name
,Reason
,[director_wrapper:c_r2p(Child, DbgMode)
|| Child <- Children]
,State]),
sys:print_log(Dbg),
erlang:exit(Reason).
terminate_children(Name, Table, DbgMode) ->
[do_terminate_child(Name, Child#?CHILD.id, Table, DbgMode)
|| Child <- director_table:tab2list(Table)],
ok.
do_terminate_child(Name, Id_or_Pid, Table, DbgMode) ->
Search =
case director_table:lookup(Table, Id_or_Pid) of
not_found ->
director_table:lookup_by_pid(Table, Id_or_Pid);
Child ->
Child
end,
_ =
case Search of
#?CHILD{pid = Pid}=Child2 when erlang:is_pid(Pid) ->
ok = do_terminate_child(Name, Child2, DbgMode);
#?CHILD{pid = restarting, timer_reference = Ref} ->
_ = erlang:cancel_timer(Ref, [{async, true}]),
ok;
Other ->
Other
end,
case Search of
not_found ->
not_found;
Child3 ->
ok = director_table:insert(Table
,Child3#?CHILD{pid = undefined
,extra = undefined
,timer_reference =
undefined})
end.
do_terminate_child(Name
,#?CHILD{pid=Pid
,terminate_timeout = TerminateTimeout}=Child
,DbgMode)
when erlang:is_pid(Pid) ->
BrutalKill =
fun() ->
erlang:exit(Pid, kill),
receive
{'DOWN', _Ref, process, Pid, killed} ->
ok;
{'DOWN', _Ref, process, Pid, Reason} ->
director_debug:error_report(Name
,shutdown_error
,Reason
,Child
,DbgMode),
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_debug:error_report(Name
,shutdown_error
,Reason2
,Child
,DbgMode),
ok
after TerminateTimeout ->
BrutalKill()
end
end;
{error, Reason3} ->
director_debug:error_report(Name
,shutdown_error
,Reason3
,Child
,DbgMode),
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_debug:debug(Dbg, Name, {out, Pid, Result}).
get_children(Children, Table) ->
get_children(Children, Table, []).
get_children([ChildR|Children], Table, Children2) ->
case director_table:lookup(Table, ChildR#?CHILD.id) of
not_found ->
{error
,{child_not_found, director_wrapper:c_r2p(ChildR, long)}};
LastChildR ->
get_children(Children
,Table
,[{LastChildR, ChildR}|Children2])
end;
get_children([], _Table, Children2) ->
{ok, Children2}.