diff --git a/CHANGELOG.md b/CHANGELOG.md
index 120e307cbd..81f100bc25 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`onRunTrackedJs`, `onGetTrackedObjects` and `onTrackedObjectDelete` hooks, which embedders may
override to customize what tracking means
- Added `string:to_integer/1`
+- Added support for `rest_for_one` and `simple_one_for_one` supervisor restart strategies
### Changed
- `erlang:process_info/2` now accepts only pids of local processes, as Erlang/OTP does:
diff --git a/libs/estdlib/src/supervisor.erl b/libs/estdlib/src/supervisor.erl
index 6f5392e554..0019ed5d85 100644
--- a/libs/estdlib/src/supervisor.erl
+++ b/libs/estdlib/src/supervisor.erl
@@ -31,7 +31,6 @@
%% Caveats:
%%
%% - Support only for locally named procs
-%% - No support for simple_one_for_one or one_for_rest strategies
%% - No support for hibernate
%% - No support for automatic shutdown
%%
@@ -81,7 +80,7 @@
-type child_id() :: term().
-type child() :: undefined | pid().
--type strategy() :: one_for_all | one_for_one.
+-type strategy() :: one_for_all | one_for_one | rest_for_one | simple_one_for_one.
-type sup_flags() ::
#{
strategy => strategy(),
@@ -138,7 +137,8 @@
period = ?DEFAULT_PERIOD :: pos_integer(),
restart_count = 0 :: non_neg_integer(),
restarts = [] :: [integer()],
- children = [] :: [#child{}]
+ children = [] :: [#child{}],
+ template = undefined :: #child{} | undefined
}).
%% Used to trim stale restarts when the 'intensity' value is large.
@@ -156,23 +156,26 @@ start_link(Module, Args) ->
start_link(SupName, Module, Args) ->
gen_server:start_link(SupName, ?MODULE, {Module, Args}, []).
--spec start_child(Supervisor :: sup_ref(), ChildSpec :: child_spec()) -> startchild_ret().
+-spec start_child(Supervisor :: sup_ref(), ChildSpec :: child_spec() | [term()]) ->
+ startchild_ret().
start_child(Supervisor, ChildSpec) ->
gen_server:call(Supervisor, {start_child, ChildSpec}, infinity).
--spec terminate_child(Supervisor :: sup_ref(), ChildId :: any()) -> ok | {error, not_found}.
+%% ChildId is a pid() for simple_one_for_one, a child id otherwise.
+-spec terminate_child(Supervisor :: sup_ref(), ChildId :: any()) ->
+ ok | {error, not_found | simple_one_for_one}.
terminate_child(Supervisor, ChildId) ->
gen_server:call(Supervisor, {terminate_child, ChildId}, infinity).
-spec restart_child(Supervisor :: sup_ref(), ChildId :: any()) ->
{ok, Child :: child()}
| {ok, Child :: child(), Info :: term()}
- | {error, Reason :: running | restarting | not_found | term()}.
+ | {error, Reason :: running | restarting | not_found | simple_one_for_one | term()}.
restart_child(Supervisor, ChildId) ->
gen_server:call(Supervisor, {restart_child, ChildId}, infinity).
-spec delete_child(Supervisor :: sup_ref(), ChildId :: any()) ->
- ok | {error, Reason :: running | restarting | not_found}.
+ ok | {error, Reason :: running | restarting | not_found | simple_one_for_one}.
delete_child(Supervisor, ChildId) ->
gen_server:call(Supervisor, {delete_child, ChildId}, infinity).
@@ -202,32 +205,42 @@ count_children(Supervisor) ->
-spec init({Mod :: module(), Args :: [any()]}) ->
{ok, State :: #state{}}
| {stop, {bad_return, {Mod :: module(), init, Reason :: term()}}}
+ | {stop, {bad_start_spec, StartSpec :: term()}}
| {stop, {shutdown, {failed_to_start_child, ChildId :: term(), Reason :: term()}}}.
init({Mod, Args}) ->
erlang:process_flag(trap_exit, true),
case Mod:init(Args) of
{ok, {{Strategy, Intensity, Period}, StartSpec}} ->
- State = init_state(StartSpec, #state{
+ finalize_init(StartSpec, #state{
restart_strategy = Strategy,
intensity = Intensity,
period = Period
- }),
- init_start_children(State);
+ });
{ok, {#{} = SupSpec, StartSpec}} ->
Strategy = maps:get(strategy, SupSpec, one_for_one),
Intensity = maps:get(intensity, SupSpec, ?DEFAULT_INTENSITY),
Period = maps:get(period, SupSpec, ?DEFAULT_PERIOD),
- State = init_state(StartSpec, #state{
+ finalize_init(StartSpec, #state{
restart_strategy = Strategy,
intensity = Intensity,
period = Period
- }),
- init_start_children(State);
+ });
Error ->
% TODO: log supervisor init failure
{stop, {bad_return, {Mod, init, Error}}}
end.
+finalize_init(StartSpec, #state{restart_strategy = simple_one_for_one} = State) ->
+ case StartSpec of
+ [Spec] ->
+ Template = child_spec_to_record(Spec),
+ {ok, State#state{template = Template, children = []}};
+ _ ->
+ {stop, {bad_start_spec, StartSpec}}
+ end;
+finalize_init(StartSpec, State) ->
+ init_start_children(init_state(StartSpec, State)).
+
init_start_children(State) ->
case start_children(State#state.children, []) of
{ok, NewChildren} ->
@@ -300,6 +313,58 @@ start_children([], StartedC) ->
{ok, StartedC}.
% @hidden
+handle_call(
+ {start_child, ExtraArgs},
+ _From,
+ #state{restart_strategy = simple_one_for_one, template = Template, children = Children} = State
+) ->
+ #child{start = {M, F, A}} = Template,
+ NewChild = Template#child{id = make_ref(), start = {M, F, A ++ ExtraArgs}},
+ case try_start(NewChild) of
+ {ok, undefined, Result} ->
+ {reply, Result, State};
+ {ok, Pid, Result} ->
+ {reply, Result, State#state{children = [NewChild#child{pid = Pid} | Children]}};
+ {error, _Reason} = ErrorT ->
+ {reply, ErrorT, State}
+ end;
+handle_call(
+ {terminate_child, Pid}, From, #state{restart_strategy = simple_one_for_one} = State
+) when
+ is_pid(Pid)
+->
+ case lists:keyfind(Pid, #child.pid, State#state.children) of
+ #child{} = Child ->
+ do_terminate(Child),
+ %% Mark as terminating so handle_child_exit replies on the 'EXIT'.
+ NewChild = Child#child{restart = {terminating, temporary, From}},
+ NewChildren = lists:keyreplace(Pid, #child.pid, State#state.children, NewChild),
+ {noreply, State#state{children = NewChildren}};
+ false ->
+ %% A failed restart is parked as {restarting, Pid}; cancel the retry.
+ case lists:keyfind({restarting, Pid}, #child.pid, State#state.children) of
+ #child{} ->
+ NewChildren = lists:keydelete(
+ {restarting, Pid}, #child.pid, State#state.children
+ ),
+ {reply, ok, State#state{children = NewChildren}};
+ false ->
+ {reply, {error, not_found}, State}
+ end
+ end;
+handle_call({terminate_child, _Id}, _From, #state{restart_strategy = simple_one_for_one} = State) ->
+ {reply, {error, simple_one_for_one}, State};
+handle_call({restart_child, _Id}, _From, #state{restart_strategy = simple_one_for_one} = State) ->
+ {reply, {error, simple_one_for_one}, State};
+handle_call({delete_child, _Id}, _From, #state{restart_strategy = simple_one_for_one} = State) ->
+ {reply, {error, simple_one_for_one}, State};
+handle_call(
+ which_children,
+ _From,
+ #state{restart_strategy = simple_one_for_one, children = Children} = State
+) ->
+ ChildrenInfo = [setelement(1, child_to_info(C), undefined) || C <- Children],
+ {reply, ChildrenInfo, State};
handle_call({start_child, ChildSpec}, _From, #state{children = Children} = State) ->
Child = child_spec_to_record(ChildSpec),
#child{id = ID} = Child,
@@ -369,8 +434,13 @@ handle_call(which_children, _From, #state{children = Children} = State) ->
ChildrenInfo = lists:map(fun child_to_info/1, Children),
{reply, ChildrenInfo, State};
handle_call(count_children, _From, #state{children = Children} = State) ->
- {Specs, Active, Supers, Workers} =
+ {Specs0, Active, Supers, Workers} =
lists:foldl(fun count_child/2, {0, 0, 0, 0}, Children),
+ Specs =
+ case State#state.restart_strategy of
+ simple_one_for_one -> 1;
+ _ -> Specs0
+ end,
Reply = [{specs, Specs}, {active, Active}, {supervisors, Supers}, {workers, Workers}],
{reply, Reply, State}.
@@ -414,6 +484,13 @@ handle_info({try_again_restart, Id}, State) ->
case add_restart(State) of
{ok, State1} ->
case try_start(Child) of
+ {ok, undefined, _Result} when
+ State1#state.restart_strategy =:= simple_one_for_one
+ ->
+ UpdatedChildren = lists:keydelete(
+ Id, #child.id, State1#state.children
+ ),
+ {noreply, State1#state{children = UpdatedChildren}};
{ok, NewPid, _Result} ->
UpdatedChildren = lists:keyreplace(
Id, #child.id, State1#state.children, Child#child{pid = NewPid}
@@ -478,23 +555,10 @@ handle_child_exit(Pid, Reason, State) ->
end
end.
-handle_restart_strategy(
- #child{id = Id} = Child, #state{restart_strategy = one_for_one} = State
-) ->
- case try_start(Child) of
- {ok, NewPid, _Result} ->
- NewChild = Child#child{pid = NewPid},
- Children = lists:keyreplace(
- Id, #child.id, State#state.children, NewChild
- ),
- {noreply, State#state{children = Children}};
- {error, _} ->
- NewChild = Child#child{pid = {restarting, Child#child.pid}},
- Children = lists:keyreplace(
- Id, #child.id, State#state.children, NewChild
- ),
- {noreply, State#state{children = Children}, {timeout, 0, {try_again_restart, Id}}}
- end;
+handle_restart_strategy(#child{} = Child, #state{restart_strategy = one_for_one} = State) ->
+ restart_one(Child, State);
+handle_restart_strategy(#child{} = Child, #state{restart_strategy = simple_one_for_one} = State) ->
+ restart_simple_one(Child, State);
handle_restart_strategy(
#child{pid = Pid} = Child, #state{restart_strategy = one_for_all} = State
) ->
@@ -511,7 +575,63 @@ handle_restart_strategy(
{ok, NewChildren} = get_restart_children(Children),
%% NewChildren is startup order (first at head) and needs to be reversed to keep Children in correct order in #state{}
{noreply, State#state{children = lists:reverse(NewChildren)},
- {timeout, 0, {restart_many_children, NewChildren}}}.
+ {timeout, 0, {restart_many_children, NewChildren}}};
+handle_restart_strategy(
+ #child{pid = Pid} = Child, #state{restart_strategy = rest_for_one} = State
+) ->
+ {Affected0, Rest} = split_affected_rest(Pid, State#state.children),
+ Affected1 =
+ case Pid of
+ {restarting, _} ->
+ Affected0;
+ Pid when is_pid(Pid) ->
+ lists:keyreplace(Pid, #child.pid, Affected0, Child#child{
+ pid = {restarting, Pid}
+ })
+ end,
+ ok = terminate_one_for_all(Affected1),
+ {ok, NewAffected} = get_restart_children(Affected1),
+ {noreply, State#state{children = lists:reverse(NewAffected) ++ Rest},
+ {timeout, 0, {restart_many_children, NewAffected}}}.
+
+split_affected_rest(Pid, Children) ->
+ split_affected_rest(Pid, Children, []).
+
+split_affected_rest(Pid, [#child{pid = Pid} = Child | Rest], Acc) ->
+ {lists:reverse([Child | Acc]), Rest};
+split_affected_rest(Pid, [Child | Tail], Acc) ->
+ split_affected_rest(Pid, Tail, [Child | Acc]);
+split_affected_rest(_Pid, [], Acc) ->
+ {lists:reverse(Acc), []}.
+
+restart_one(#child{id = Id} = Child, State) ->
+ case try_start(Child) of
+ {ok, NewPid, _Result} ->
+ NewChild = Child#child{pid = NewPid},
+ Children = lists:keyreplace(Id, #child.id, State#state.children, NewChild),
+ {noreply, State#state{children = Children}};
+ {error, _} ->
+ NewChild = Child#child{pid = {restarting, Child#child.pid}},
+ Children = lists:keyreplace(Id, #child.id, State#state.children, NewChild),
+ {noreply, State#state{children = Children}, {timeout, 0, {try_again_restart, Id}}}
+ end.
+
+%% Unlike a static child, a dynamic one that returns ignore is dropped rather
+%% than kept with pid = undefined.
+restart_simple_one(#child{id = Id} = Child, State) ->
+ case try_start(Child) of
+ {ok, undefined, _Result} ->
+ Children = lists:keydelete(Id, #child.id, State#state.children),
+ {noreply, State#state{children = Children}};
+ {ok, NewPid, _Result} ->
+ NewChild = Child#child{pid = NewPid},
+ Children = lists:keyreplace(Id, #child.id, State#state.children, NewChild),
+ {noreply, State#state{children = Children}};
+ {error, _} ->
+ NewChild = Child#child{pid = {restarting, Child#child.pid}},
+ Children = lists:keyreplace(Id, #child.id, State#state.children, NewChild),
+ {noreply, State#state{children = Children}, {timeout, 0, {try_again_restart, Id}}}
+ end.
should_restart(_Reason, permanent) ->
true;
diff --git a/tests/libs/estdlib/test_supervisor.erl b/tests/libs/estdlib/test_supervisor.erl
index 94fa90d42a..24333ebab1 100644
--- a/tests/libs/estdlib/test_supervisor.erl
+++ b/tests/libs/estdlib/test_supervisor.erl
@@ -20,7 +20,17 @@
-module(test_supervisor).
--export([start/0, test/0, init/1, start_link/1, child_start/1]).
+-export([
+ start/0,
+ test/0,
+ init/1,
+ start_link/1,
+ child_start/1,
+ start_rest_worker/2,
+ start_simple_worker/2,
+ start_ignoring_worker/2,
+ start_unrestartable_worker/2
+]).
start() ->
ok = test().
@@ -36,6 +46,10 @@ test() ->
ok = test_which_children(),
ok = test_count_children(),
ok = test_one_for_all(),
+ ok = test_rest_for_one(),
+ ok = test_simple_one_for_one(),
+ ok = test_simple_one_for_one_ignore_on_restart(),
+ ok = test_simple_one_for_one_terminate_restarting(),
ok = test_crash_limits(),
ok = try_again_restart(),
ok = try_again_restart_shutdown(),
@@ -360,6 +374,49 @@ init({test_supervisor_order, Parent}) ->
{ok, {{one_for_one, 10000, 3600}, ChildSpecs}};
init({test_no_child, _Parent}) ->
{ok, {#{strategy => one_for_one, intensity => 10000, period => 3600}, []}};
+init({test_rest_for_one, Parent}) ->
+ ChildSpecs = [
+ #{
+ id => Id,
+ start => {?MODULE, start_rest_worker, [Id, Parent]},
+ restart => permanent,
+ shutdown => brutal_kill,
+ type => worker,
+ modules => [?MODULE]
+ }
+ || Id <- [rest_a, rest_b, rest_c]
+ ],
+ {ok, {#{strategy => rest_for_one, intensity => 10, period => 60}, ChildSpecs}};
+init({test_simple_one_for_one, Parent}) ->
+ Template = #{
+ id => dynamic,
+ start => {?MODULE, start_simple_worker, [Parent]},
+ restart => permanent,
+ shutdown => brutal_kill,
+ type => worker,
+ modules => [?MODULE]
+ },
+ {ok, {#{strategy => simple_one_for_one, intensity => 10, period => 60}, [Template]}};
+init({test_simple_ignore, Parent}) ->
+ Template = #{
+ id => dynamic,
+ start => {?MODULE, start_ignoring_worker, [Parent]},
+ restart => permanent,
+ shutdown => brutal_kill,
+ type => worker,
+ modules => [?MODULE]
+ },
+ {ok, {#{strategy => simple_one_for_one, intensity => 10, period => 60}, [Template]}};
+init({test_simple_restart_fails, Parent}) ->
+ Template = #{
+ id => dynamic,
+ start => {?MODULE, start_unrestartable_worker, [Parent]},
+ restart => permanent,
+ shutdown => brutal_kill,
+ type => worker,
+ modules => [?MODULE]
+ },
+ {ok, {#{strategy => simple_one_for_one, intensity => 10, period => 60}, [Template]}};
init({test_one_for_all, Parent}) ->
ChildSpecs = [
#{
@@ -788,6 +845,188 @@ try_again_one_for_all() ->
process_flag(trap_exit, false),
ok.
+start_rest_worker(Id, Parent) ->
+ Pid = spawn_link(fun() ->
+ Parent ! {rest_started, Id, self()},
+ rest_worker_loop()
+ end),
+ {ok, Pid}.
+
+rest_worker_loop() ->
+ receive
+ crash -> exit(crashed);
+ stop -> ok
+ end.
+
+test_rest_for_one() ->
+ {ok, SupPid} = supervisor:start_link(?MODULE, {test_rest_for_one, self()}),
+ {rest_a, PidA} = recv_rest_started(rest_a),
+ {rest_b, PidB} = recv_rest_started(rest_b),
+ {rest_c, _PidC} = recv_rest_started(rest_c),
+ %% Crash the middle child: rest_b and rest_c must restart, rest_a must not.
+ PidB ! crash,
+ {rest_b, NewPidB} = recv_rest_started(rest_b),
+ {rest_c, _NewPidC} = recv_rest_started(rest_c),
+ true = is_pid(NewPidB),
+ true = NewPidB =/= PidB,
+ %% rest_a was started before the crashed child and must be left running.
+ ok = recv_no_rest_started(rest_a),
+ true = is_process_alive(PidA),
+ unlink(SupPid),
+ exit(SupPid, shutdown),
+ ok.
+
+recv_rest_started(Id) ->
+ receive
+ {rest_started, Id, Pid} -> {Id, Pid}
+ after 2000 -> error({timeout, {rest_started, Id}})
+ end.
+
+recv_no_rest_started(Id) ->
+ receive
+ {rest_started, Id, _Pid} -> error({unexpected_restart, Id})
+ after 200 -> ok
+ end.
+
+start_simple_worker(Parent, N) ->
+ Pid = spawn_link(fun() ->
+ Parent ! {simple_started, N, self()},
+ simple_worker_loop()
+ end),
+ {ok, Pid}.
+
+simple_worker_loop() ->
+ receive
+ crash -> exit(crashed);
+ stop -> ok
+ end.
+
+test_simple_one_for_one() ->
+ {ok, SupPid} = supervisor:start_link(?MODULE, {test_simple_one_for_one, self()}),
+ {ok, Pid1} = supervisor:start_child(SupPid, [1]),
+ {simple_started, 1, Pid1} = recv_simple_started(1),
+ {ok, Pid2} = supervisor:start_child(SupPid, [2]),
+ {simple_started, 2, Pid2} = recv_simple_started(2),
+ %% which_children reports dynamic children with an undefined id
+ Children = supervisor:which_children(SupPid),
+ 2 = length(Children),
+ [{undefined, _, worker, [?MODULE]}, {undefined, _, worker, [?MODULE]}] = Children,
+ %% simple_one_for_one has a single child spec (the template), so specs is
+ %% always 1 regardless of the number of dynamic children.
+ [{specs, 1}, {active, 2}, {supervisors, 0}, {workers, 2}] = supervisor:count_children(SupPid),
+ %% id-based operations are invalid for simple_one_for_one
+ {error, simple_one_for_one} = supervisor:delete_child(SupPid, whatever),
+ {error, simple_one_for_one} = supervisor:restart_child(SupPid, whatever),
+ {error, simple_one_for_one} = supervisor:terminate_child(SupPid, not_a_pid),
+ %% terminate one dynamic child by pid
+ ok = supervisor:terminate_child(SupPid, Pid1),
+ [{undefined, _, worker, [?MODULE]}] = supervisor:which_children(SupPid),
+ %% crashing a permanent dynamic child restarts it with its own args ([2])
+ Pid2 ! crash,
+ {simple_started, 2, NewPid2} = recv_simple_started(2),
+ true = is_pid(NewPid2),
+ true = NewPid2 =/= Pid2,
+ unlink(SupPid),
+ exit(SupPid, shutdown),
+ ok.
+
+start_ignoring_worker(Parent, N) ->
+ case erlang:get({ignore_next, N}) of
+ true ->
+ Parent ! {ignored, N},
+ ignore;
+ _ ->
+ erlang:put({ignore_next, N}, true),
+ start_simple_worker(Parent, N)
+ end.
+
+start_unrestartable_worker(Parent, N) ->
+ case erlang:get({fail_next, N}) of
+ true ->
+ Parent ! {restart_attempted, N},
+ %% Slow the retry loop so the child is observably parked in
+ %% {restarting, Pid} rather than racing through its intensity.
+ receive
+ after 250 -> ok
+ end,
+ {error, always_fails};
+ _ ->
+ erlang:put({fail_next, N}, true),
+ start_simple_worker(Parent, N)
+ end.
+
+test_simple_one_for_one_ignore_on_restart() ->
+ {ok, SupPid} = supervisor:start_link(?MODULE, {test_simple_ignore, self()}),
+ {ok, Pid} = supervisor:start_child(SupPid, [1]),
+ {simple_started, 1, Pid} = recv_simple_started(1),
+ [{undefined, Pid, worker, [?MODULE]}] = supervisor:which_children(SupPid),
+ Pid ! crash,
+ ok = recv_ignored(1),
+ %% OTP kept the child with pid = undefined until OTP 27.
+ case get_otp_version() of
+ Version when Version =:= atomvm orelse Version >= 27 ->
+ ok = wait_no_children(SupPid, 50),
+ [{specs, 1}, {active, 0}, {supervisors, 0}, {workers, 0}] = supervisor:count_children(
+ SupPid
+ );
+ _ ->
+ ok
+ end,
+ unlink(SupPid),
+ exit(SupPid, shutdown),
+ ok.
+
+test_simple_one_for_one_terminate_restarting() ->
+ {ok, SupPid} = supervisor:start_link(?MODULE, {test_simple_restart_fails, self()}),
+ {ok, Pid} = supervisor:start_child(SupPid, [1]),
+ {simple_started, 1, Pid} = recv_simple_started(1),
+ Pid ! crash,
+ ok = recv_restart_attempted(1),
+ ok = supervisor:terminate_child(SupPid, Pid),
+ [] = supervisor:which_children(SupPid),
+ unlink(SupPid),
+ exit(SupPid, shutdown),
+ ok.
+
+get_otp_version() ->
+ case erlang:system_info(machine) of
+ "BEAM" ->
+ list_to_integer(erlang:system_info(otp_release));
+ _ ->
+ atomvm
+ end.
+
+recv_ignored(N) ->
+ receive
+ {ignored, N} -> ok
+ after 2000 -> error({timeout, {ignored, N}})
+ end.
+
+recv_restart_attempted(N) ->
+ receive
+ {restart_attempted, N} -> ok
+ after 2000 -> error({timeout, {restart_attempted, N}})
+ end.
+
+wait_no_children(_SupPid, 0) ->
+ error(children_not_dropped);
+wait_no_children(SupPid, N) ->
+ case supervisor:which_children(SupPid) of
+ [] ->
+ ok;
+ _ ->
+ receive
+ after 20 -> ok
+ end,
+ wait_no_children(SupPid, N - 1)
+ end.
+
+recv_simple_started(N) ->
+ receive
+ {simple_started, N, Pid} -> {simple_started, N, Pid}
+ after 2000 -> error({timeout, {simple_started, N}})
+ end.
+
wait_child_pid(Ref, Name) ->
receive
{Ref, Pid} when is_pid(Pid) ->