From 9ebce285202554945874687ce9c6652f3d75e789 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sun, 5 Jan 2025 22:06:33 +0100 Subject: [PATCH] Complete supervisor behavior - 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 --- libs/estdlib/src/gen_server.erl | 40 +++++---- libs/estdlib/src/supervisor.erl | 118 ++++++++++++++++++++++++- src/libAtomVM/defaultatoms.def | 2 + src/libAtomVM/opcodesswitch.h | 4 +- tests/libs/estdlib/test_supervisor.erl | 50 ++++++++++- 5 files changed, 189 insertions(+), 25 deletions(-) diff --git a/libs/estdlib/src/gen_server.erl b/libs/estdlib/src/gen_server.erl index 7aa1f7640..12e16882d 100644 --- a/libs/estdlib/src/gen_server.erl +++ b/libs/estdlib/src/gen_server.erl @@ -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) -> @@ -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); @@ -538,11 +538,11 @@ 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); _ -> @@ -550,12 +550,14 @@ loop(#state{mod = Mod, mod_state = ModState} = State, Timeout) -> 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); _ -> @@ -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); _ -> diff --git a/libs/estdlib/src/supervisor.erl b/libs/estdlib/src/supervisor.erl index 8239d55e9..9ab98fb8a 100644 --- a/libs/estdlib/src/supervisor.erl +++ b/libs/estdlib/src/supervisor.erl @@ -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([ @@ -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. @@ -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 @@ -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 -> @@ -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) -> @@ -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 @@ -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}). diff --git a/src/libAtomVM/defaultatoms.def b/src/libAtomVM/defaultatoms.def index 2f1d2949d..9e768a355 100644 --- a/src/libAtomVM/defaultatoms.def +++ b/src/libAtomVM/defaultatoms.def @@ -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") diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index f83dd1f60..c0ed1fe30 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -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); } diff --git a/tests/libs/estdlib/test_supervisor.erl b/tests/libs/estdlib/test_supervisor.erl index b18944113..a80df3561 100644 --- a/tests/libs/estdlib/test_supervisor.erl +++ b/tests/libs/estdlib/test_supervisor.erl @@ -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() -> @@ -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) -> @@ -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(),