From 63a942a0a8fd497c62f605b6223dc9068f4e27f3 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Fri, 3 Jul 2026 23:51:44 +0200 Subject: [PATCH] estdlib: add rest_for_one and simple_one_for_one supervisor strategies Signed-off-by: Paul Guyot --- CHANGELOG.md | 1 + libs/estdlib/src/supervisor.erl | 147 ++++++++++++++++++++----- tests/libs/estdlib/test_supervisor.erl | 124 ++++++++++++++++++++- 3 files changed, 242 insertions(+), 30 deletions(-) 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: %% @@ -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) ->