diff options
author | Raimo Niskanen <raimo@erlang.org> | 2020-03-06 14:15:01 +0100 |
---|---|---|
committer | Raimo Niskanen <raimo@erlang.org> | 2020-03-06 14:15:01 +0100 |
commit | 89f85ed97a5aee83ecf79685c26abbd501d44508 (patch) | |
tree | ec2e46512d113ed09b0ca9de4f9ed48781e5eba3 | |
parent | d030be503fe23bb4d61db07d436146cdc84ccc3b (diff) | |
parent | f0a1c8d9e3ea410caa0ce00f2c2e9e1bebaba914 (diff) | |
download | erlang-89f85ed97a5aee83ecf79685c26abbd501d44508.tar.gz |
Merge branch 'raimo/stdlib/gen_statem-change_cb' into maint
* raimo/stdlib/gen_statem-change_cb:
Improve function name
Document change|push|pop _callback_module
Implement push and pop of callback module
-rw-r--r-- | lib/stdlib/doc/src/gen_statem.xml | 81 | ||||
-rw-r--r-- | lib/stdlib/src/gen_statem.erl | 229 | ||||
-rw-r--r-- | lib/stdlib/test/gen_statem_SUITE.erl | 33 | ||||
-rw-r--r-- | lib/stdlib/test/gen_statem_SUITE_data/oc_statem.erl | 14 | ||||
-rw-r--r-- | system/doc/design_principles/statem.xml | 45 |
5 files changed, 281 insertions, 121 deletions
diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 39fc565ff8..c12ad2deef 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -101,10 +101,10 @@ </item> <item> In OTP 22.3 the possibility to change the callback module - with action - <seealso marker="#type-action"> - <c>change_callback_module</c> - </seealso> + with actions + <seealso marker="#type-action"><c>change_callback_module</c></seealso>, + <seealso marker="#type-action"><c>push_callback_module</c></seealso> and + <seealso marker="#type-action"><c>pop_callback_module</c></seealso>, was added. </item> </list> @@ -750,8 +750,10 @@ handle_event(_, _, State, Data) -> <seealso marker="#Module:callback_mode/0"><c>Module:callback_mode/0</c></seealso> is called when starting the <c>gen_statem</c>, after code change - and after changing the callback module with action - <seealso marker="#type-action"><c>change_callback_module</c></seealso>. + and after changing the callback module with any of the actions + <seealso marker="#type-action"><c>change_callback_module</c></seealso>, + <seealso marker="#type-action"><c>push_callback_module</c></seealso> or + <seealso marker="#type-action"><c>pop_callback_module</c></seealso>. The result is cached for subsequent calls to <seealso marker="#state callback">state callbacks</seealso>. </p> @@ -1191,7 +1193,9 @@ handle_event(_, _, State, Data) -> an event inserted this way from any external event. </p> </item> - <tag><c>change_callback_module</c></tag> + <tag> + <c>change_callback_module</c> + </tag> <item> <p> Changes the callback module to @@ -1231,6 +1235,32 @@ handle_event(_, _, State, Data) -> from the old module. </p> </item> + <tag> + <c>push_callback_module</c><br /> + </tag> + <item> + <p> + Pushes the current callback module + to the top of an internal stack of callback modules + and changes the callback module to + <c><anno>NewModule</anno></c>. + Otherwise like + <c>{change_callback_module, NewModule}</c> + above. + </p> + </item> + <tag> + <c>pop_callback_module</c> + </tag> + <item> + Pops the top module from the internal stack of + callback modules and changes the callback module + to be the popped module. + If the stack is empty the server fails. + Otherwise like + <c>{change_callback_module, NewModule}</c> + above. + </item> </taglist> </desc> </datatype> @@ -2017,8 +2047,10 @@ handle_event(_, _, State, Data) -> returns. A change of the callback module happens when a <seealso marker="#state callback"><em>state callback</em></seealso> - returns the action - <seealso marker="#type-action"><c>change_callback_module</c></seealso>. + returns any of the actions + <seealso marker="#type-action"><c>change_callback_module</c></seealso>, + <seealso marker="#type-action"><c>push_callback_module</c></seealso> or + <seealso marker="#type-action"><c>pop_callback_module</c></seealso>. </p> <p> The <c>CallbackMode</c> is either just @@ -2125,6 +2157,37 @@ handle_event(_, _, State, Data) -> will not be honoured, most probably causing a server crash. </p> + <p> + If the server changes callback module using any of the actions + <seealso marker="#type-action"><c>change_callback_module</c></seealso>, + <seealso marker="#type-action"><c>push_callback_module</c></seealso> or + <seealso marker="#type-action"><c>pop_callback_module</c></seealso>, + be aware that it is always the current callback module that + will get this callback call. That the current callback module + handles the current state and data update should be no surprise, + but it must be able to handle even parts of + the state and data that it is not familiar with, + somehow. + </p> + <p> + In the supervisor + <seealso marker="doc/design_principles:sup_princ#child-specification">child specification</seealso> + there is a list of modules which is recommended to contain + only the callback module. + For a <c>gen_statem</c> with multiple callback modules + there is no real need to list all of them, + it may not even be possible since the list could change + after code upgrade. + If this list would contain only the start callback module, + as recommended, what is important is to upgrade <em>that</em> module + whenever a <em>synchronized code replacement</em> is done. + Then the release handler concludes that an upgrade + that upgrades <em>that</em> module needs to suspend, + code change, and resume any server whose child specification + declares that it is using <em>that</em> module. + And again; the <em>current</em> callback module will get the + <c>Module:code_change/4</c> call. + </p> </desc> </func> diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 885c6ef031..2cf30e10ec 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -150,6 +150,8 @@ EventType :: event_type(), EventContent :: term()} | {'change_callback_module', NewModule :: module()} | + {'push_callback_module', NewModule :: module()} | + 'pop_callback_module' | enter_action(). -type enter_action() :: 'hibernate' | % Set the hibernate option @@ -423,7 +425,7 @@ timeout_event_type(Type) -> {callback_mode = undefined :: callback_mode() | undefined, state_enter = false :: boolean(), parent :: pid(), - module :: atom(), + modules :: [module()], name :: atom() | pid(), hibernate_after = infinity :: timeout() }). @@ -691,7 +693,7 @@ enter( P = #params{ parent = Parent, - module = Module, + modules = [Module], name = Name, hibernate_after = HibernateAfterTimeout}, S = #state{state_data = {State,Data}}, @@ -728,7 +730,7 @@ init_it(Starter, Parent, ServerRef, Module, Args, Opts) -> proc_lib:init_ack(Starter, {error,Reason}), error_info( Class, Reason, Stacktrace, Debug, - #params{parent = Parent, name = Name, module = Module}, + #params{parent = Parent, name = Name, modules = [Module]}, #state{}, []), erlang:raise(Class, Reason, Stacktrace) end. @@ -764,7 +766,7 @@ init_result( proc_lib:init_ack(Starter, {error,Error}), error_info( error, Error, ?STACKTRACE(), Debug, - #params{parent = Parent, name = Name, module = Module}, + #params{parent = Parent, name = Name, modules = [Module]}, #state{}, []), exit(Error) end. @@ -785,7 +787,7 @@ system_terminate(Reason, Parent, Debug, {P,S}) -> update_parent(P, Parent), Debug, S, []). system_code_change( - {#params{module = Module} = P, + {#params{modules = [Module | _]} = P, #state{state_data = {State,Data}} = S}, _Mod, OldVsn, Extra) -> case @@ -816,7 +818,7 @@ system_replace_state( format_status( Opt, [PDict,SysState,Parent,Debug, - {#params{name = Name} = P, + {#params{name = Name, modules = Modules} = P, #state{postponed = Postponed, timers = Timers} = S}]) -> Header = gen:format_status_header("Status for state machine", Name), Log = sys:get_log(Debug), @@ -824,6 +826,7 @@ format_status( {data, [{"Status",SysState}, {"Parent",Parent}, + {"Modules",Modules}, {"Time-outs",list_timeouts(Timers)}, {"Logged Events",Log}, {"Postponed",Postponed}]} | @@ -1079,7 +1082,7 @@ loop_state_callback(P, Debug, S, Q, State_Data, CallbackEvent) -> StateCall, CallbackEvent). %% loop_state_callback( - #params{callback_mode = undefined, module = Module} = P, + #params{callback_mode = undefined, modules = [Module | _]} = P, Debug, S, Q, State_Data, NextEventsR, Hibernate, TimeoutsR, Postpone, StateCall, CallbackEvent) -> @@ -1105,7 +1108,7 @@ loop_state_callback( Class, Reason, Stacktrace, P, Debug, S, Q) end; loop_state_callback( - #params{callback_mode = CallbackMode, module = Module} = P, + #params{callback_mode = CallbackMode, modules = [Module | _]} = P, Debug, S, Q, {State,Data} = State_Data, NextEventsR, Hibernate, TimeoutsR, Postpone, StateCall, {Type,Content}) -> @@ -1446,13 +1449,21 @@ loop_actions_list( NextEventsR, Hibernate, TimeoutsR, Postpone, CallEnter, StateCall, Actions, Type, Content); %% - {change_callback_module, NewModule} - when is_atom(NewModule) -> + {Tag, NewModule} + when Tag =:= change_callback_module, is_atom(NewModule); + Tag =:= push_callback_module, is_atom(NewModule) -> if StateCall -> + NewModules = + case Tag of + change_callback_module -> + [NewModule | tl(P#params.modules)]; + push_callback_module -> + [NewModule | P#params.modules] + end, P_1 = P#params{ - callback_mode = undefined, module = NewModule}, + callback_mode = undefined, modules = NewModules}, loop_actions_list( P_1, Debug, S, Q, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, Postpone, @@ -1467,91 +1478,40 @@ loop_actions_list( hibernate = Hibernate}, Q) end; - %% - Timeout -> - loop_actions_timeout( - P, Debug, S, Q, NextState_NewData, - NextEventsR, Hibernate, TimeoutsR, Postpone, - CallEnter, StateCall, Actions, Timeout) - end. - -%% Process a reply action -%% -loop_actions_reply( - P, Debug, S, Q, NextState_NewData, - NextEventsR, Hibernate, TimeoutsR, Postpone, - CallEnter, StateCall, Actions, - From, Reply) -> - %% - case from(From) of - true -> - %% No need for a separate ?not_sys_debug clause here - %% since the external call to erlang:'!'/2 in reply/2 - %% will cause swap out of all live registers anyway - reply(From, Reply), - Debug_1 = ?sys_debug(Debug, P#params.name, {out,Reply,From}), - loop_actions_list( - P, Debug_1, S, Q, NextState_NewData, - NextEventsR, Hibernate, TimeoutsR, Postpone, - CallEnter, StateCall, Actions); - false -> - terminate( - error, - {bad_action_from_state_function,{reply,From,Reply}}, - ?STACKTRACE(), P, Debug, - S#state{ - state_data = NextState_NewData, - hibernate = Hibernate}, - Q) - end. - -%% Process a next_event action -%% -loop_actions_next_event( - P, Debug, S, Q, NextState_NewData, - NextEventsR, Hibernate, TimeoutsR, Postpone, - CallEnter, StateCall, Actions, Type, Content) -> - case event_type(Type) of - true when StateCall -> - NextEvent = {Type,Content}, - case Debug of - ?not_sys_debug -> + pop_callback_module when tl(P#params.modules) =/= [] -> + if + StateCall -> + NewModules = tl(P#params.modules), + P_1 = + P#params{ + callback_mode = undefined, + modules = NewModules}, loop_actions_list( - P, Debug, S, Q, NextState_NewData, - [NextEvent|NextEventsR], - Hibernate, TimeoutsR, Postpone, + P_1, Debug, S, Q, NextState_NewData, + NextEventsR, Hibernate, TimeoutsR, Postpone, CallEnter, StateCall, Actions); - _ -> - Name = P#params.name, - {State,_Data} = S#state.state_data, - Debug_1 = - sys_debug(Debug, Name, {in,{Type,Content},State}), - loop_actions_list( - P, Debug_1, S, Q, NextState_NewData, - [NextEvent|NextEventsR], - Hibernate, TimeoutsR, Postpone, - CallEnter, StateCall, Actions) - end; + true -> + terminate( + error, + {bad_state_enter_action_from_state_function,Action}, + ?STACKTRACE(), P, Debug, + S#state{ + state_data = NextState_NewData, + hibernate = Hibernate}, + Q) + end; + %% _ -> - terminate( - error, - {if - StateCall -> - bad_action_from_state_function; - true -> - bad_state_enter_action_from_state_function - end, - {next_event,Type,Content}}, - ?STACKTRACE(), P, Debug, - S#state{ - state_data = NextState_NewData, - hibernate = Hibernate}, - Q) + loop_actions_list( + P, Debug, S, Q, NextState_NewData, + NextEventsR, Hibernate, TimeoutsR, Postpone, + CallEnter, StateCall, Actions, Action) end. -%% Process a timeout action, or also any unrecognized action +%% Process all other actions, i.e timeout actions, +%% all others are unrecognized %% -loop_actions_timeout( +loop_actions_list( P, Debug, S, Q, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, Postpone, CallEnter, StateCall, Actions, @@ -1618,7 +1578,7 @@ loop_actions_timeout( hibernate = Hibernate}, Q) end; -loop_actions_timeout( +loop_actions_list( P, Debug, S, Q, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, Postpone, CallEnter, StateCall, Actions, @@ -1643,7 +1603,7 @@ loop_actions_timeout( hibernate = Hibernate}, Q) end; -loop_actions_timeout( +loop_actions_list( P, Debug, S, Q, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, Postpone, CallEnter, StateCall, Actions, @@ -1667,7 +1627,7 @@ loop_actions_timeout( hibernate = Hibernate}, Q) end; -loop_actions_timeout( +loop_actions_list( P, Debug, S, Q, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, Postpone, CallEnter, StateCall, Actions, @@ -1692,6 +1652,80 @@ loop_actions_timeout( Q) end. +%% Process a reply action +%% +loop_actions_reply( + P, Debug, S, Q, NextState_NewData, + NextEventsR, Hibernate, TimeoutsR, Postpone, + CallEnter, StateCall, Actions, + From, Reply) -> + %% + case from(From) of + true -> + %% No need for a separate ?not_sys_debug clause here + %% since the external call to erlang:'!'/2 in reply/2 + %% will cause swap out of all live registers anyway + reply(From, Reply), + Debug_1 = ?sys_debug(Debug, P#params.name, {out,Reply,From}), + loop_actions_list( + P, Debug_1, S, Q, NextState_NewData, + NextEventsR, Hibernate, TimeoutsR, Postpone, + CallEnter, StateCall, Actions); + false -> + terminate( + error, + {bad_action_from_state_function,{reply,From,Reply}}, + ?STACKTRACE(), P, Debug, + S#state{ + state_data = NextState_NewData, + hibernate = Hibernate}, + Q) + end. + +%% Process a next_event action +%% +loop_actions_next_event( + P, Debug, S, Q, NextState_NewData, + NextEventsR, Hibernate, TimeoutsR, Postpone, + CallEnter, StateCall, Actions, Type, Content) -> + case event_type(Type) of + true when StateCall -> + NextEvent = {Type,Content}, + case Debug of + ?not_sys_debug -> + loop_actions_list( + P, Debug, S, Q, NextState_NewData, + [NextEvent|NextEventsR], + Hibernate, TimeoutsR, Postpone, + CallEnter, StateCall, Actions); + _ -> + Name = P#params.name, + {State,_Data} = S#state.state_data, + Debug_1 = + sys_debug(Debug, Name, {in,{Type,Content},State}), + loop_actions_list( + P, Debug_1, S, Q, NextState_NewData, + [NextEvent|NextEventsR], + Hibernate, TimeoutsR, Postpone, + CallEnter, StateCall, Actions) + end; + _ -> + terminate( + error, + {if + StateCall -> + bad_action_from_state_function; + true -> + bad_state_enter_action_from_state_function + end, + {next_event,Type,Content}}, + ?STACKTRACE(), P, Debug, + S#state{ + state_data = NextState_NewData, + hibernate = Hibernate}, + Q) + end. + %% Do the state transition %% loop_state_transition( @@ -2269,7 +2303,7 @@ do_reply_then_terminate( terminate( Class, Reason, Stacktrace, - #params{module = Module} = P, Debug, + #params{modules = [Module | _]} = P, Debug, #state{state_data = {State,Data}} = S, Q) -> case erlang:function_exported(Module, terminate, 3) of true -> @@ -2310,6 +2344,7 @@ error_info( Class, Reason, Stacktrace, Debug, #params{ name = Name, + modules = Modules, callback_mode = CallbackMode, state_enter = StateEnter} = P, #state{ @@ -2321,6 +2356,7 @@ error_info( name=>Name, queue=>Q, postponed=>Postponed, + modules=>Modules, callback_mode=>CallbackMode, state_enter=>StateEnter, state=>format_status(terminate, get(), P, S), @@ -2360,6 +2396,7 @@ format_log(#{label:={gen_statem,terminate}, name:=Name, queue:=Q, postponed:=Postponed, + modules:=Modules, callback_mode:=CallbackMode, state_enter:=StateEnter, state:=FmtData, @@ -2406,6 +2443,7 @@ format_log(#{label:={gen_statem,terminate}, end ++ "** When server state = ~tp~n" ++ "** Reason for termination = ~w:~tp~n" ++ + "** Callback modules = ~p~n" ++ "** Callback mode = ~p~n" ++ case Q of [_,_|_] -> "** Queued = ~tp~n"; @@ -2434,6 +2472,7 @@ format_log(#{label:={gen_statem,terminate}, end] ++ [error_logger:limit_term(FmtData), Class,error_logger:limit_term(FixedReason), + error_logger:limit_term(Modules), CBMode] ++ case Q of [_|[_|_] = Events] -> [error_logger:limit_term(Events)]; @@ -2471,7 +2510,7 @@ format_client_log({_Pid,{Name,Stacktrace}}) -> %% Call Module:format_status/2 or return a default value format_status( Opt, PDict, - #params{module = Module}, + #params{modules = [Module | _]}, #state{state_data = {State,Data} = State_Data}) -> case erlang:function_exported(Module, format_status, 2) of true -> diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 0586575736..296973370c 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -69,7 +69,7 @@ tcs(sys) -> get_state, replace_state]; tcs(undef_callbacks) -> [undef_code_change, undef_terminate1, undef_terminate2, - function_clause_after_change_callback_module]. + pop_too_many]. init_per_suite(Config) -> Config. @@ -1778,7 +1778,7 @@ verify_down(Statem, MRef, Reason) -> end. -function_clause_after_change_callback_module(_Config) -> +pop_too_many(_Config) -> _ = process_flag(trap_exit, true), Machine = @@ -1790,8 +1790,17 @@ function_clause_after_change_callback_module(_Config) -> fun ({call, From}, {change_callback_module, _Module} = Action, undefined = _Data) -> {keep_state_and_data, - [{reply,From,ok}, - Action]} + [Action, + {reply,From,ok}]}; + ({call, From}, {verify, ?MODULE}, + undefined = _Data) -> + {keep_state_and_data, + [{reply,From,ok}]}; + ({call, From}, pop_callback_module = Action, + undefined = _Data) -> + {keep_state_and_data, + [Action, + {reply,From,ok}]} end}, {ok, STM} = gen_statem:start_link( @@ -1800,16 +1809,16 @@ function_clause_after_change_callback_module(_Config) -> [{debug, [trace]}]), ok = gen_statem:call(STM, {change_callback_module, oc_statem}), + ok = gen_statem:call(STM, {push_callback_module, ?MODULE}), + ok = gen_statem:call(STM, {verify, ?MODULE}), + ok = gen_statem:call(STM, pop_callback_module), + BadAction = {bad_action_from_state_function, pop_callback_module}, + {{BadAction, _}, + {gen_statem,call,[STM,pop_callback_module,infinity]}} = + ?EXPECT_FAILURE(gen_statem:call(STM, pop_callback_module), Reason), - Call = unhandled_call, - {{function_clause, - [{oc_statem, handle_event, - [{call, _From}, Call, start, _Data], _Line} - | _RestStacktrace]} = Undef, - {gen_statem, call, [STM,Call,_Timeout]}} = - ?EXPECT_FAILURE(gen_statem:call(STM, Call), Reason), receive - {'EXIT', STM, Undef} -> + {'EXIT', STM, {BadAction, _}} -> ok; Other -> ct:fail({surprise, Other}) diff --git a/lib/stdlib/test/gen_statem_SUITE_data/oc_statem.erl b/lib/stdlib/test/gen_statem_SUITE_data/oc_statem.erl index 1493cfb192..1bcd08867f 100644 --- a/lib/stdlib/test/gen_statem_SUITE_data/oc_statem.erl +++ b/lib/stdlib/test/gen_statem_SUITE_data/oc_statem.erl @@ -37,4 +37,16 @@ callback_mode() -> [handle_event_function, state_enter]. handle_event(enter, start, start, _Data) -> - keep_state_and_data. + keep_state_and_data; +handle_event( + {call,From}, {push_callback_module,NewModule} = Action, + start, _Data) -> + {keep_state_and_data, + [Action, + {reply,From,ok}]}; +handle_event( + {call,From}, pop_callback_module = Action, + start, _Data) -> + {keep_state_and_data, + [Action, + {reply,From,ok}]}. diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index f657e8cb00..d6d907de45 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -185,15 +185,17 @@ State(S) x Event(E) -> Actions(A), State(S')</pre> </p> <p> The <em>callback module</em> can be changed for a running server - using the - <seealso marker="#Transition Actions">transition action</seealso> - <seealso marker="stdlib:gen_statem#type-action"><c>{change_callback_module, NewModule}</c></seealso>. + using any of the + <seealso marker="#Transition Actions">transition actions</seealso> + <seealso marker="stdlib:gen_statem#type-action"><c>{change_callback_module, NewModule}</c></seealso>, + <seealso marker="stdlib:gen_statem#type-action"><c>{push_callback_module, NewModule}</c></seealso> or + <seealso marker="stdlib:gen_statem#type-action"><c>pop_callback_module</c></seealso>. Note that this is a pretty esotheric thing to do... The origin for this feature is a protocol that after version negotiation branches off into quite different state machines depending on the protocol version. There <i>might</i> be other use cases. - <i>Beware</i> that the <c>NewModule</c> + <i>Beware</i> that the new callback module completely replaces the previous behaviour module, so all relevant callback functions has to handle the state and data from the previous callback module. @@ -664,6 +666,41 @@ State(S) x Event(E) -> Actions(A), State(S')</pre> but it can <i>not</i> be done from a <seealso marker="#State Enter Calls"><em>state enter call</em></seealso>. </item> + <tag> + <seealso marker="stdlib:gen_statem#type-action"> + <c>{push_callback_module, NewModule}</c> + </seealso> + </tag> + <item> + Push the current <em>callback module</em> + to the top of an internal stack of callback modules + and set the new + <seealso marker="#Callback Module"> + <em>callback module</em> + </seealso> + for the running server. + Otherwise like + <c>{change_callback_module, NewModule}</c> + above. + </item> + <tag> + <seealso marker="stdlib:gen_statem#type-action"> + <c>pop_callback_module</c> + </seealso> + </tag> + <item> + Pop the top module from + the internal stack of callback modules + and set it to be the new + <seealso marker="#Callback Module"> + <em>callback module</em> + </seealso> + for the running server. + If the stack is empty the server fails. + Otherwise like + <c>{change_callback_module, NewModule}</c> + above. + </item> </taglist> <p> For details, see the <c>gen_statem(3)</c> |