Skip to content

Commit

Permalink
Complete supervisor behavior
Browse files Browse the repository at this point in the history
- add missing `supervisor:terminate_child/2`, `supervisor:delete_child/2`
  and `supervisor:restart_child/2`
- fix termination of children of supervisor
- add more termination strategies

Signed-off-by: Paul Guyot <[email protected]>
  • Loading branch information
pguyot committed Jan 5, 2025
1 parent 74258a1 commit 9ebce28
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 25 deletions.
40 changes: 21 additions & 19 deletions libs/estdlib/src/gen_server.erl
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ init_it(Starter, Module, Args, Options) ->
end,
case StateT of
undefined -> ok;
{State, {continue, Continue}} -> loop(State, {continue, Continue});
{State, Timeout} -> loop(State, Timeout)
{State, {continue, Continue}} -> loop(Starter, State, {continue, Continue});
{State, Timeout} -> loop(Starter, State, Timeout)
end.

init_ack(Parent, Return) ->
Expand Down Expand Up @@ -499,34 +499,34 @@ reply({Pid, Ref}, Reply) ->
%%

%% @private
loop(#state{mod = Mod, mod_state = ModState} = State, {continue, Continue}) ->
loop(Parent, #state{mod = Mod, mod_state = ModState} = State, {continue, Continue}) ->
case Mod:handle_continue(Continue, ModState) of
{noreply, NewModState} ->
loop(State#state{mod_state = NewModState}, infinity);
loop(Parent, State#state{mod_state = NewModState}, infinity);
{noreply, NewModState, {continue, NewContinue}} ->
loop(State#state{mod_state = NewModState}, {continue, NewContinue});
loop(Parent, State#state{mod_state = NewModState}, {continue, NewContinue});
{stop, Reason, NewModState} ->
do_terminate(State, Reason, NewModState)
end;
loop(#state{mod = Mod, mod_state = ModState} = State, Timeout) ->
loop(Parent, #state{mod = Mod, mod_state = ModState} = State, Timeout) ->
receive
{'$call', {_Pid, _Ref} = From, Request} ->
case Mod:handle_call(Request, From, ModState) of
{reply, Reply, NewModState} ->
ok = reply(From, Reply),
loop(State#state{mod_state = NewModState}, infinity);
loop(Parent, State#state{mod_state = NewModState}, infinity);
{reply, Reply, NewModState, {continue, Continue}} ->
ok = reply(From, Reply),
loop(State#state{mod_state = NewModState}, {continue, Continue});
loop(Parent, State#state{mod_state = NewModState}, {continue, Continue});
{reply, Reply, NewModState, NewTimeout} ->
ok = reply(From, Reply),
loop(State#state{mod_state = NewModState}, NewTimeout);
loop(Parent, State#state{mod_state = NewModState}, NewTimeout);
{noreply, NewModState} ->
loop(State#state{mod_state = NewModState}, infinity);
loop(Parent, State#state{mod_state = NewModState}, infinity);
{noreply, NewModState, {continue, Continue}} ->
loop(State#state{mod_state = NewModState}, {continue, Continue});
loop(Parent, State#state{mod_state = NewModState}, {continue, Continue});
{noreply, NewModState, NewTimeout} ->
loop(State#state{mod_state = NewModState}, NewTimeout);
loop(Parent, State#state{mod_state = NewModState}, NewTimeout);
{stop, Reason, Reply, NewModState} ->
ok = reply(From, Reply),
do_terminate(State, Reason, NewModState);
Expand All @@ -538,24 +538,26 @@ loop(#state{mod = Mod, mod_state = ModState} = State, Timeout) ->
{'$cast', Request} ->
case Mod:handle_cast(Request, ModState) of
{noreply, NewModState} ->
loop(State#state{mod_state = NewModState}, infinity);
loop(Parent, State#state{mod_state = NewModState}, infinity);
{noreply, NewModState, {continue, Continue}} ->
loop(State#state{mod_state = NewModState}, {continue, Continue});
loop(Parent, State#state{mod_state = NewModState}, {continue, Continue});
{noreply, NewModState, NewTimeout} ->
loop(State#state{mod_state = NewModState}, NewTimeout);
loop(Parent, State#state{mod_state = NewModState}, NewTimeout);
{stop, Reason, NewModState} ->
do_terminate(State, Reason, NewModState);
_ ->
do_terminate(State, {error, unexpected_reply}, ModState)
end;
{'$stop', Reason} ->
do_terminate(State, Reason, ModState);
{'EXIT', Parent, Reason} ->
do_terminate(State, Reason, ModState);
Info ->
case Mod:handle_info(Info, ModState) of
{noreply, NewModState} ->
loop(State#state{mod_state = NewModState}, infinity);
loop(Parent, State#state{mod_state = NewModState}, infinity);
{noreply, NewModState, NewTimeout} ->
loop(State#state{mod_state = NewModState}, NewTimeout);
loop(Parent, State#state{mod_state = NewModState}, NewTimeout);
{stop, Reason, NewModState} ->
do_terminate(State, Reason, NewModState);
_ ->
Expand All @@ -564,9 +566,9 @@ loop(#state{mod = Mod, mod_state = ModState} = State, Timeout) ->
after Timeout ->
case Mod:handle_info(timeout, ModState) of
{noreply, NewModState} ->
loop(State#state{mod_state = NewModState}, infinity);
loop(Parent, State#state{mod_state = NewModState}, infinity);
{noreply, NewModState, NewTimeout} ->
loop(State#state{mod_state = NewModState}, NewTimeout);
loop(Parent, State#state{mod_state = NewModState}, NewTimeout);
{stop, Reason, NewModState} ->
do_terminate(State, Reason, NewModState);
_ ->
Expand Down
118 changes: 115 additions & 3 deletions libs/estdlib/src/supervisor.erl
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,18 @@
-export([
start_link/2,
start_link/3,
start_child/2
start_child/2,
terminate_child/2,
restart_child/2,
delete_child/2
]).

-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2
handle_info/2,
terminate/2
]).

-export_type([
Expand All @@ -41,7 +45,8 @@
sup_flags/0
]).

-type restart() :: permanent | transient | temporary.
-type restart() ::
permanent | transient | temporary | {terminating, permanent | transient | temporary, gen_server:from()}.
-type shutdown() :: brutal_kill | timeout().
-type child_type() :: worker | supervisor.

Expand Down Expand Up @@ -90,6 +95,15 @@ start_link(SupName, Module, Args) ->
start_child(Supervisor, ChildSpec) ->
gen_server:call(Supervisor, {start_child, ChildSpec}).

terminate_child(Supervisor, ChildId) ->
gen_server:call(Supervisor, {terminate_child, ChildId}).

restart_child(Supervisor, ChildId) ->
gen_server:call(Supervisor, {restart_child, ChildId}).

delete_child(Supervisor, ChildId) ->
gen_server:call(Supervisor, {delete_child, ChildId}).

init({Mod, Args}) ->
erlang:process_flag(trap_exit, true),
case Mod:init(Args) of
Expand Down Expand Up @@ -152,6 +166,16 @@ restart_child(Pid, Reason, State) ->
case lists:keyfind(Pid, #child.pid, State#state.children) of
false ->
{ok, State};
#child{restart = {terminating, temporary, From}} ->
gen_server:reply(From, ok),
NewChildren = lists:keydelete(Pid, #child.pid, State#state.children),
{ok, State#state{children = NewChildren}};
#child{restart = {terminating, Restart, From}} = Child ->
gen_server:reply(From, ok),
NewChildren = lists:keyreplace(Pid, #child.pid, State#state.children, Child#child{
pid = undefined, restart = Restart
}),
{ok, State#state{children = NewChildren}};
#child{} = Child ->
case should_restart(Reason, Child#child.restart) of
true ->
Expand Down Expand Up @@ -195,6 +219,46 @@ handle_call({start_child, ChildSpec}, _From, #state{children = Children} = State
{error, _Reason} = ErrorT ->
{reply, ErrorT, State}
end
end;
handle_call({terminate_child, ID}, From, #state{children = Children} = State) ->
case lists:keyfind(ID, #child.id, Children) of
#child{pid = undefined} ->
{reply, ok, State};
#child{restart = Restart} = Child ->
do_terminate(Child),
NewChild = Child#child{restart = {terminating, Restart, From}},
NewChildren = lists:keyreplace(ID, #child.id, Children, NewChild),
{noreply, State#state{children = NewChildren}};
false ->
{reply, {error, not_found}, State}
end;
handle_call({restart_child, ID}, _From, #state{children = Children} = State) ->
case lists:keyfind(ID, #child.id, Children) of
#child{pid = undefined} = Child ->
case try_start(Child) of
{ok, NewPid, Result} ->
NewChild = Child#child{pid = NewPid},
NewChildren = lists:keyreplace(
ID, #child.id, Children, NewChild
),
{reply, Result, State#state{children = NewChildren}};
{error, _Reason} = ErrorT ->
{reply, ErrorT, State}
end;
#child{} ->
{reply, {error, running}, State};
false ->
{reply, {error, not_found}, State}
end;
handle_call({delete_child, ID}, _From, #state{children = Children} = State) ->
case lists:keyfind(ID, #child.id, Children) of
#child{pid = undefined} ->
NewChildren = lists:keydelete(ID, #child.id, Children),
{reply, ok, State#state{children = NewChildren}};
#child{} ->
{reply, {error, running}, State};
false ->
{reply, {error, not_found}, State}
end.

handle_cast(_Msg, State) ->
Expand All @@ -207,10 +271,50 @@ handle_info({'EXIT', Pid, Reason}, State) ->
{shutdown, State1} ->
{stop, shutdown, State1}
end;
handle_info({ensure_killed, Pid}, State) ->
case lists:keyfind(Pid, #child.pid, State#state.children) of
false ->
{noreply, State};
#child{} ->
exit(Pid, kill),
{noreply, State}
end;
handle_info(_Msg, State) ->
%TODO: log unexpected message
{noreply, State}.

%% @hidden
terminate(_Reason, #state{children = Children} = State) ->
RemainingChildren = loop_terminate(Children, []),
loop_wait_termination(RemainingChildren),
{ok, State}.

loop_terminate([#child{pid = undefined} | Tail], AccRemaining) ->
loop_terminate(Tail, AccRemaining);
loop_terminate([#child{pid = Pid} = Child | Tail], AccRemaining) when is_pid(Pid) ->
do_terminate(Child),
loop_terminate(Tail, [Pid | AccRemaining]);
loop_terminate([], AccRemaining) ->
AccRemaining.

loop_wait_termination([]) ->
ok;
loop_wait_termination(RemainingChildren0) ->
receive
{'EXIT', Pid, _Reason} ->
RemainingChildren1 = lists:delete(Pid, RemainingChildren0),
loop_wait_termination(RemainingChildren1);
{ensure_killed, Pid} ->
case lists:member(Pid, RemainingChildren0) of
true ->
exit(Pid, kill),
RemainingChildren1 = lists:delete(Pid, RemainingChildren0),
loop_wait_termination(RemainingChildren1);
false ->
loop_wait_termination(RemainingChildren0)
end
end.

try_start(#child{start = {M, F, Args}} = Record) ->
try
case apply(M, F, Args) of
Expand All @@ -229,3 +333,11 @@ try_start(#child{start = {M, F, Args}} = Record) ->
error:Error ->
{error, {{'EXIT', Error}, Record}}
end.

do_terminate(#child{pid = Pid, shutdown = brutal_kill}) ->
exit(Pid, kill);
do_terminate(#child{pid = Pid, shutdown = infinity}) ->
exit(Pid, shutdown);
do_terminate(#child{pid = Pid, shutdown = Timeout}) when is_integer(Timeout) ->
exit(Pid, shutdown),
erlang:send_after(Timeout, self(), {ensure_killed, Pid}).
2 changes: 2 additions & 0 deletions src/libAtomVM/defaultatoms.def
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,5 @@ X(EXTERNAL_ATOM, "\x8", "external")
X(LOCAL_ATOM, "\x5", "local")

X(REGISTERED_NAME_ATOM, "\xF", "registered_name")

X(SHUTDOWN_ATOM, "\x8", "shutdown")
4 changes: 2 additions & 2 deletions src/libAtomVM/opcodesswitch.h
Original file line number Diff line number Diff line change
Expand Up @@ -7053,8 +7053,8 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb)
}
}

// Do not print crash dump if reason is normal.
if (x_regs[0] != LOWERCASE_EXIT_ATOM || x_regs[1] != NORMAL_ATOM) {
// Do not print crash dump if reason is normal or shutdown.
if (x_regs[0] != LOWERCASE_EXIT_ATOM || (x_regs[1] != NORMAL_ATOM && x_regs[1] != SHUTDOWN_ATOM)) {
dump(ctx);
}

Expand Down
50 changes: 49 additions & 1 deletion tests/libs/estdlib/test_supervisor.erl
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ test() ->
ok = test_start_child(),
ok = test_start_child_ping_pong(),
ok = test_supervisor_order(),
ok = test_terminate_delete_child(),
ok = test_terminate_timeout(),
ok.

test_basic_supervisor() ->
Expand Down Expand Up @@ -85,6 +87,41 @@ test_start_child() ->
exit(SupPid, shutdown),
ok.

test_terminate_delete_child() ->
{ok, SupPid} = supervisor:start_link(?MODULE, {test_no_child, self()}),
{ok, Pid} = supervisor:start_child(SupPid, #{
id => child_start, start => {?MODULE, child_start, [start]}
}),
{error, not_found} = supervisor:terminate_child(SupPid, Pid),
{error, running} = supervisor:delete_child(SupPid, child_start),
ok = supervisor:terminate_child(SupPid, child_start),
ok = supervisor:delete_child(SupPid, child_start),
{error, not_found} = supervisor:delete_child(SupPid, child_start),
unlink(SupPid),
exit(SupPid, shutdown),
ok.

test_terminate_timeout() ->
{ok, SupPid} = supervisor:start_link(?MODULE, {test_no_child, self()}),
Self = self(),
{ok, Pid} = supervisor:start_child(SupPid, #{
id => child_start, start => {?MODULE, child_start, [{trap_exit, Self}]}, shutdown => 500
}),
ok = supervisor:terminate_child(SupPid, child_start),
ok = receive {Pid, {SupPid, shutdown}} -> ok after 1000 -> timeout end,
{ok, Pid2} = supervisor:restart_child(SupPid, child_start),
Pid2 ! ok,
ok = supervisor:terminate_child(SupPid, child_start),
ok = receive {Pid2, {SupPid, shutdown}} -> ok after 1000 -> timeout end,
ok = supervisor:delete_child(SupPid, child_start),
{ok, Pid3} = supervisor:start_child(SupPid, #{
id => child_start, start => {?MODULE, child_start, [{trap_exit, Self}]}, shutdown => 500
}),
unlink(SupPid),
exit(SupPid, shutdown),
ok = receive {Pid3, {SupPid, shutdown}} -> ok after 1000 -> timeout end,
ok.

child_start(ignore) ->
ignore;
child_start(start) ->
Expand All @@ -104,7 +141,18 @@ child_start(info) ->
child_start(error) ->
{error, child_error};
child_start(fail) ->
fail.
fail;
child_start({trap_exit, Parent}) ->
Pid = spawn_link(fun() ->
process_flag(trap_exit, true),
receive
{'EXIT', From, Reason} -> Parent ! {self(), {From, Reason}}
end,
receive
ok -> ok
end
end),
{ok, Pid}.

test_ping_pong(SupPid) ->
Pid1 = get_and_test_server(),
Expand Down

0 comments on commit 9ebce28

Please sign in to comment.