diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd6fbef48c..74a147fec8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added support for configuring pins and width for sdmmc on ESP32
- Added support for map comprehensions
- Added USB CDC port drivers for ESP32, RP2, and STM32 platforms
+- Added support for `rest_for_one` and `simple_one_for_one` supervisor restart strategies
### Changed
- Updated network type db() to dbm() to reflect the actual representation of the type
diff --git a/libs/estdlib/src/supervisor.erl b/libs/estdlib/src/supervisor.erl
index 6f5392e554..c3d81fb938 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,7 +156,8 @@ 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).
@@ -202,32 +203,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 +311,53 @@ 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} ->
+ %% The child returned ignore: it is dropped, as in OTP.
+ {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),
+ %% Reply only once the child has actually terminated, like OTP.
+ %% Mark it as 'terminating, temporary' so handle_child_exit removes
+ %% the dynamic child from the list on its 'EXIT'.
+ NewChild = Child#child{restart = {terminating, temporary, From}},
+ NewChildren = lists:keyreplace(Pid, #child.pid, State#state.children, NewChild),
+ {noreply, State#state{children = NewChildren}};
+ false ->
+ {reply, {error, not_found}, State}
+ 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
+) ->
+ %% Dynamic children have no id.
+ 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 +427,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}.
@@ -478,23 +541,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_one(Child, State);
handle_restart_strategy(
#child{pid = Pid} = Child, #state{restart_strategy = one_for_all} = State
) ->
@@ -511,7 +561,46 @@ 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.
should_restart(_Reason, permanent) ->
true;
diff --git a/tests/libs/estdlib/test_supervisor.erl b/tests/libs/estdlib/test_supervisor.erl
index 94fa90d42a..7a9d74c241 100644
--- a/tests/libs/estdlib/test_supervisor.erl
+++ b/tests/libs/estdlib/test_supervisor.erl
@@ -20,7 +20,9 @@
-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() ->
ok = test().
@@ -36,6 +38,8 @@ 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_crash_limits(),
ok = try_again_restart(),
ok = try_again_restart_shutdown(),
@@ -360,6 +364,29 @@ 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_one_for_all, Parent}) ->
ChildSpecs = [
#{
@@ -788,6 +815,101 @@ try_again_one_for_all() ->
process_flag(trap_exit, false),
ok.
+%% rest_for_one: when a child crashes, it and every child started *after* it
+%% are restarted, while children started *before* it are left untouched.
+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.
+
+%% simple_one_for_one: dynamic children created from a single template, each
+%% with extra args appended; restarted with their own stored args.
+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.
+
+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) ->