Skip to content

Commit

Permalink
Merge pull request #1372 from pguyot/w47/improve-supervisor
Browse files Browse the repository at this point in the history
Extend support of `supervisor` module

Add `supervisor:start_child/2`
Add map syntax for supervisor init callback

These changes are made under both the "Apache 2.0" and the "GNU Lesser General
Public License 2.1 or later" license terms (dual license).

SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
  • Loading branch information
bettio committed Nov 23, 2024
2 parents 94ced13 + 5e51e33 commit eb4943a
Show file tree
Hide file tree
Showing 2 changed files with 253 additions and 40 deletions.
173 changes: 142 additions & 31 deletions libs/estdlib/src/supervisor.erl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
-behavior(gen_server).

-export([
start_link/2, start_link/3
start_link/2,
start_link/3,
start_child/2
]).

-export([
Expand All @@ -33,63 +35,138 @@
handle_info/2
]).

-record(child, {pid = undefined, id, mfargs, restart_type, shutdown, child_type, modules = []}).
-record(state, {children = []}).
-export_type([
child_spec/0,
strategy/0,
sup_flags/0
]).

-type restart() :: permanent | transient | temporary.
-type shutdown() :: brutal_kill | timeout().
-type child_type() :: worker | supervisor.

-type strategy() :: one_for_all | one_for_one.
-type sup_flags() ::
#{
strategy => strategy(),
intensity => non_neg_integer(),
period => pos_integer()
}
| {RestartStrategy :: strategy(), Intensity :: non_neg_integer(), Period :: pos_integer()}.

-type child_spec() ::
#{
id := any(),
start := {module(), atom(), [any()]},
restart => restart(),
shutdown => shutdown(),
type => child_type(),
modules => [module()] | dynamic
}
| {
Id :: any(),
StartFunc :: {module(), atom(), [any()]},
Restart :: restart(),
Shutdown :: shutdown(),
Type :: child_type(),
Modules :: [module()] | dynamic
}.

-record(child, {
pid = undefined,
id :: any(),
start :: {module(), atom(), [any()] | undefined},
restart :: restart(),
shutdown :: shutdown(),
type :: child_type
}).
-record(state, {restart_strategy :: strategy(), children = [] :: [#child{}]}).

start_link(Module, Args) ->
gen_server:start_link(?MODULE, {Module, Args}, []).
start_link(SupName, Module, Args) ->
gen_server:start_link(SupName, ?MODULE, {Module, Args}, []).

start_child(Supervisor, ChildSpec) ->
gen_server:call(Supervisor, {start_child, ChildSpec}).

init({Mod, Args}) ->
erlang:process_flag(trap_exit, true),
case Mod:init(Args) of
{ok, {{one_for_one, _Intensity, _Period}, StartSpec}} ->
State = init_state(StartSpec, #state{}),
{ok, {{Strategy, _Intensity, _Period}, StartSpec}} ->
State = init_state(StartSpec, #state{restart_strategy = Strategy}),
NewChildren = start_children(State#state.children, []),
{ok, State#state{children = NewChildren}};
{ok, {#{strategy := Strategy}, StartSpec}} ->
State = init_state(StartSpec, #state{restart_strategy = Strategy}),
NewChildren = start_children(State#state.children, []),
{ok, State#state{children = NewChildren}};
Error ->
{stop, {bad_return, {mod, init, Error}}}
end.

init_state([{ChildId, MFA, Restart, brutal_kill, Type, Modules} | T], State) ->
Child = #child{
-spec child_spec_to_record(child_spec()) -> #child{}.
child_spec_to_record({ChildId, MFA, Restart, Shutdown, Type, _Modules}) ->
#child{
id = ChildId,
start = MFA,
restart = Restart,
shutdown = Shutdown,
type = Type
};
child_spec_to_record(#{id := ChildId, start := MFA} = ChildMap) ->
Restart = maps:get(restart, ChildMap, permanent),
Type = maps:get(type, ChildMap, worker),
Shutdown = maps:get(
shutdown,
ChildMap,
case Type of
worker -> 5000;
supervisor -> infinity
end
),
#child{
id = ChildId,
mfargs = MFA,
restart_type = Restart,
shutdown = brutal_kill,
child_type = Type,
modules = Modules
},
start = MFA,
restart = Restart,
shutdown = Shutdown,
type = Type
}.

init_state([ChildSpec | T], State) ->
Child = child_spec_to_record(ChildSpec),
NewChildren = [Child | State#state.children],
init_state(T, #state{children = NewChildren});
init_state([], State) ->
State#state{children = lists:reverse(State#state.children)}.

start_children([Child | T], StartedC) ->
#child{mfargs = {M, F, Args}} = Child,
case apply(M, F, Args) of
{ok, Pid} when is_pid(Pid) ->
case try_start(Child) of
{ok, Pid, _Result} ->
start_children(T, [Child#child{pid = Pid} | StartedC])
end;
start_children([], StartedC) ->
StartedC.

restart_child(Pid, Reason, State) ->
Child = lists:keyfind(Pid, #child.pid, State#state.children),

#child{mfargs = {M, F, Args}} = Child,
case should_restart(Reason, Child#child.restart_type) of
true ->
case apply(M, F, Args) of
{ok, NewPid} when is_pid(Pid) ->
NewChild = Child#child{pid = NewPid},
Children = lists:keyreplace(Pid, #child.pid, State#state.children, NewChild),
{ok, State#state{children = Children}}
end;
case lists:keyfind(Pid, #child.pid, State#state.children) of
false ->
Children = lists:keydelete(Pid, #child.pid, State#state.children),
{ok, State#state{children = Children}}
{ok, State};
#child{} = Child ->
case should_restart(Reason, Child#child.restart) of
true ->
case try_start(Child) of
{ok, NewPid, _Result} ->
NewChild = Child#child{pid = NewPid},
Children = lists:keyreplace(
Pid, #child.pid, State#state.children, NewChild
),
{ok, State#state{children = Children}}
end;
false ->
Children = lists:keydelete(Pid, #child.pid, State#state.children),
{ok, State#state{children = Children}}
end
end.

should_restart(_Reason, permanent) ->
Expand All @@ -102,8 +179,23 @@ should_restart(Reason, transient) ->
_any -> true
end.

handle_call(_Msg, _from, State) ->
{noreply, State}.
handle_call({start_child, ChildSpec}, _From, #state{children = Children} = State) ->
Child = child_spec_to_record(ChildSpec),
#child{id = ID} = Child,
case lists:keyfind(ID, #child.id, State#state.children) of
#child{pid = undefined} ->
{reply, {error, already_present}, State};
#child{pid = Pid} ->
{reply, {error, {already_started, Pid}}, State};
false ->
case try_start(Child) of
{ok, Pid, Result} ->
UpdatedChild = Child#child{pid = Pid},
{reply, Result, State#state{children = [UpdatedChild | Children]}};
{error, _Reason} = ErrorT ->
{reply, ErrorT, State}
end
end.

handle_cast(_Msg, State) ->
{noreply, State}.
Expand All @@ -118,3 +210,22 @@ handle_info({'EXIT', Pid, Reason}, State) ->
handle_info(_Msg, State) ->
%TODO: log unexpected message
{noreply, State}.

try_start(#child{start = {M, F, Args}} = Record) ->
try
case apply(M, F, Args) of
{ok, Pid} when is_pid(Pid) ->
{ok, Pid, {ok, Pid}};
{ok, Pid, Info} when is_pid(Pid) ->
{ok, Pid, {ok, Pid, Info}};
ignore ->
{ok, undefined, {ok, undefined}};
{error, Reason} ->
{error, {Reason, Record}};
Other ->
{error, {Other, Record}}
end
catch
error:Error ->
{error, {{'EXIT', Error}, Record}}
end.
Loading

0 comments on commit eb4943a

Please sign in to comment.