Current section

Files

Jump to
ides src ides_march.erl
Raw

src/ides_march.erl

-module(ides_march).
-moduledoc "Kill graph analysis and restart logic for ides.".
-export([
kill_graph/1,
should_restart/2,
affected_siblings/1,
link_info/1,
monitor_info/1,
kill_graph_detail/1
]).
-type exit_reason() :: normal | abnormal.
-export_type([exit_reason/0]).
-doc """
Return all PIDs that could cause `TargetPid` to be killed.
This is the set of ancestors unioned with siblings that trigger
cascade restarts under the parent supervisor's strategy:
- `one_for_one` / `simple_one_for_one`: only ancestors (no
sibling killers)
- `one_for_all`: all siblings are killers
- `rest_for_one`: siblings at positions before the target are
killers
""".
-spec kill_graph(TargetPid :: pid()) -> {ok, [pid()]} | {error, term()}.
kill_graph(TargetPid) ->
case ides_family:get_ancestors(TargetPid) of
{ok, Ancestors} when Ancestors =/= [] ->
AncestorPids = lists:filtermap(
fun(P) ->
try ides_family:resolve_pid(P) of
Pid -> {true, Pid}
catch
_:_ -> false
end
end,
Ancestors
),
case ides_family:parent_info(TargetPid) of
{ok, Info} ->
KillerSiblings = killer_siblings(Info),
LinkKillers = link_killers(TargetPid),
MonitorKillers = monitor_killers(TargetPid),
{ok,
lists:usort(
AncestorPids ++
KillerSiblings ++
LinkKillers ++
MonitorKillers
)};
{error, Reason} ->
{error, Reason}
end;
{ok, []} ->
{error, no_ancestors};
{error, Reason} ->
{error, Reason}
end.
-doc """
Return whether a terminated child `Pid` would be restarted by
its supervisor.
Rules:
- `permanent`: always restarted
- `transient`: restarted only on `abnormal` exit
- `temporary`: never restarted
""".
-spec should_restart(Pid :: pid(), Reason :: exit_reason()) -> boolean().
should_restart(Pid, ExitReason) ->
case ides_family:get_ancestors(Pid) of
{ok, Ancestors} when Ancestors =/= [] ->
[Parent | _] = Ancestors,
ParentPid = ides_family:resolve_pid(Parent),
ChildSpecs = supervisor:which_children(ParentPid),
{Id, _ChildPid, _Type, _Mods} = lists:keyfind(Pid, 2, ChildSpecs),
RestartType = ides_family:get_restart_type(ParentPid, Id),
should_restart_rule(RestartType, ExitReason);
_ ->
false
end.
-doc """
Return the PIDs of siblings that would be killed or restarted
if `TargetPid` dies.
Depends on the parent supervisor's strategy:
- `one_for_one`: `TargetPid` only
- `one_for_all`: all siblings
- `rest_for_one`: `TargetPid` and all siblings after it
- `simple_one_for_one`: `TargetPid` only
""".
-spec affected_siblings(TargetPid :: pid()) -> {ok, [pid()]} | {error, term()}.
affected_siblings(TargetPid) ->
case ides_family:parent_info(TargetPid) of
{ok, Info} ->
Affected = affected_siblings_rule(Info),
{ok, Affected};
{error, Reason} ->
{error, Reason}
end.
-doc """
Return link information for the given process.
Reports which processes are linked to `Pid` and whether
`Pid` traps exits. Linked processes are potential killers
if `traps_exits` is `false`.
""".
-spec link_info(Pid :: pid()) -> {ok, ides_family:link_info()} | {error, term()}.
link_info(Pid) ->
case erlang:process_info(Pid, [links, trap_exit, status]) of
[{links, Links}, {trap_exit, Traps}, {status, _}] ->
{ok, #{
links => Links -- [Pid],
traps_exits => Traps
}};
undefined ->
{error, process_not_alive};
Other ->
{error, {unexpected_process_info, Other}}
end.
-doc """
Return monitor information for the given process.
Reports which processes `Pid` is monitoring and which
processes are monitoring `Pid`. Monitored processes are
potential killers if `Pid` doesn't handle DOWN messages.
""".
-spec monitor_info(Pid :: pid()) -> {ok, ides_family:monitor_info()} | {error, term()}.
monitor_info(Pid) ->
case erlang:process_info(Pid, [monitors, monitored_by, status]) of
[{monitors, Monitors}, {monitored_by, MonitoredBy}, {status, _}] ->
MonitorPids = [MPid || {process, MPid} <- Monitors],
{ok, #{
monitors => MonitorPids,
monitored_by => MonitoredBy
}};
undefined ->
{error, process_not_alive};
Other ->
{error, {unexpected_process_info, Other}}
end.
-doc """
Return the kill graph with each entry tagged by its kill mechanism:
- `ancestor` — supervisor ancestor
- `sibling` — sibling via supervisor strategy
- `link` — linked process (relevant if target doesn't trap exits)
- `monitor` — monitored process (relevant if target doesn't handle DOWN)
""".
-spec kill_graph_detail(TargetPid :: pid()) -> {ok, [ides_family:kill_source()]} | {error, term()}.
kill_graph_detail(TargetPid) ->
case ides_family:get_ancestors(TargetPid) of
{ok, Ancestors} when Ancestors =/= [] ->
AncestorPids = lists:filtermap(
fun(P) ->
try ides_family:resolve_pid(P) of
Pid -> {true, Pid}
catch
_:_ -> false
end
end,
Ancestors
),
case ides_family:parent_info(TargetPid) of
{ok, Info} ->
SiblingKillers = killer_siblings(Info),
LinkKillers = link_killers(TargetPid),
MonitorKillers = monitor_killers(TargetPid),
Tagged =
[{ancestor, P} || P <- AncestorPids] ++
[{sibling, P} || P <- SiblingKillers] ++
[{link, P} || P <- LinkKillers] ++
[{monitor, P} || P <- MonitorKillers],
{ok, Tagged};
{error, Reason} ->
{error, Reason}
end;
{ok, []} ->
{error, no_ancestors};
{error, Reason} ->
{error, Reason}
end.
%% --- Internal ---
-spec killer_siblings(Info :: ides_family:parent_info()) -> [pid()].
killer_siblings(#{sup_strategy := one_for_all, child_pids := Children, target_position := _Pos}) ->
pids(Children);
killer_siblings(#{sup_strategy := rest_for_one, child_pids := Children, target_position := Pos}) ->
pids(child_prefix(Children, Pos - 1));
killer_siblings(#{sup_strategy := _, child_pids := _Children, target_position := _Pos}) ->
[].
-spec should_restart_rule(
RestartType :: ides_family:child_restart_type(), ExitReason :: exit_reason()
) -> boolean().
should_restart_rule(permanent, _) -> true;
should_restart_rule(transient, abnormal) -> true;
should_restart_rule(transient, normal) -> false;
should_restart_rule(temporary, _) -> false.
-spec affected_siblings_rule(Info :: ides_family:parent_info()) -> [pid()].
affected_siblings_rule(#{
sup_strategy := one_for_one, child_pids := Children, target_position := Pos
}) ->
pids(child_sublist(Children, Pos, 1));
affected_siblings_rule(#{
sup_strategy := one_for_all, child_pids := Children, target_position := _Pos
}) ->
pids(Children);
affected_siblings_rule(#{
sup_strategy := rest_for_one, child_pids := Children, target_position := Pos
}) ->
pids(child_suffix(Children, Pos - 1));
affected_siblings_rule(#{
sup_strategy := simple_one_for_one, child_pids := Children, target_position := Pos
}) ->
pids(child_sublist(Children, Pos, 1)).
-spec link_killers(Pid :: pid()) -> [pid()].
link_killers(Pid) ->
case link_info(Pid) of
{ok, #{traps_exits := true}} ->
[];
{ok, #{links := Links, traps_exits := false}} ->
Links;
_ ->
[]
end.
-spec monitor_killers(Pid :: pid()) -> [pid()].
monitor_killers(Pid) ->
case monitor_info(Pid) of
{ok, #{monitors := Monitors}} ->
Monitors;
_ ->
[]
end.
-spec pids(Children :: [{term(), pid()}]) -> [pid()].
pids(Children) ->
[Pid || {_Id, Pid} <- Children].
-spec child_sublist(Children :: [{term(), pid()}], Start :: pos_integer(), Len :: pos_integer()) ->
[{term(), pid()}].
child_sublist(Children, Start, Len) ->
child_sublist_skip(Children, Start - 1, Len).
child_sublist_skip(Children, 0, Len) ->
child_prefix(Children, Len);
child_sublist_skip([], _, _) ->
[];
child_sublist_skip([_ | T], N, Len) ->
child_sublist_skip(T, N - 1, Len).
-spec child_prefix(Children :: [{term(), pid()}], Len :: non_neg_integer()) -> [{term(), pid()}].
child_prefix(_Children, 0) -> [];
child_prefix([], _) -> [];
child_prefix([H | T], N) -> [H | child_prefix(T, N - 1)].
-spec child_suffix(Children :: [{term(), pid()}], Skip :: non_neg_integer()) -> [{term(), pid()}].
child_suffix(Children, 0) -> Children;
child_suffix([], _) -> [];
child_suffix([_ | T], N) -> child_suffix(T, N - 1).