diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/stdlib/doc/src/gen_statem.xml | 82 | ||||
-rw-r--r-- | lib/stdlib/src/gen_statem.erl | 208 | ||||
-rw-r--r-- | lib/stdlib/test/gen_statem_SUITE.erl | 66 |
3 files changed, 287 insertions, 69 deletions
diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 17f1526a21..a4c5438a08 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -70,6 +70,7 @@ <item>The state can be any term.</item> <item>Events can be postponed.</item> <item>Events can be self-generated.</item> + <item>Automatic state entry events can be generated.</item> <item>A reply can be sent from a later state.</item> <item>There can be multiple <c>sys</c> traceable replies.</item> </list> @@ -193,6 +194,12 @@ erlang:'!' -----> Module:StateName/3 <seealso marker="gen_fsm"><c>gen_fsm</c></seealso> to force processing an inserted event before others. </p> + <p> + The <c>gen_statem</c> engine can automatically insert + a special event whenever a new state is entered; see + <seealso marker="#type-state_entry_mode"><c>state_entry_mode()</c></seealso>. + This makes it easy to handle code common to all state entries. + </p> <note> <p>If you in <c>gen_statem</c>, for example, postpone an event in one state and then call another state function @@ -515,7 +522,7 @@ handle_event(_, _, State, Data) -> Type <c>info</c> originates from regular process messages sent to the <c>gen_statem</c>. Also, the state machine implementation can generate events of types - <c>timeout</c> and <c>internal</c> to itself. + <c>timeout</c>, <c>enter</c> and <c>internal</c> to itself. </p> </desc> </datatype> @@ -551,6 +558,34 @@ handle_event(_, _, State, Data) -> </desc> </datatype> <datatype> + <name name="state_entry_mode"/> + <desc> + <p> + The <em>state entry mode</em> is selected when starting the + <c>gen_statem</c> and after code change + using the return value from + <seealso marker="#Module:callback_mode/0"><c>Module:callback_mode/0</c></seealso>. + </p> + <p> + If + <seealso marker="#Module:callback_mode/0"><c>Module:callback_mode/0</c></seealso> + returns a list containing <c>state_entry_events</c>, + the <c>gen_statem</c> engine will, at every state change, + insert an event of type + <seealso marker="#type-event_type">enter</seealso> + with content <c>OldState</c>. This event will be inserted + before all other events such as those generated by + <seealso marker="#type-action"><c>action()</c></seealso> + <c>next_event</c>. + </p> + <p> + If + <seealso marker="#Module:callback_mode/0"><c>Module:callback_mode/0</c></seealso> + does not return such a list, no state entry events are inserted. + </p> + </desc> + </datatype> + <datatype> <name name="transition_option"/> <desc> <p> @@ -591,6 +626,16 @@ handle_event(_, _, State, Data) -> </p> </item> <item> + <p> + If the state changes or is the initial state, and the + <seealso marker="#type-state_entry_mode"><em>state entry mode</em></seealso> + is <c>state_entry_events</c>, an event of type + <seealso marker="#type-event_type">enter</seealso> + with content <c>OldState</c> is inserted + to be processed before all other events including those above. + </p> + </item> + <item> <p> If an <seealso marker="#type-event_timeout"><c>event_timeout()</c></seealso> @@ -1288,7 +1333,9 @@ handle_event(_, _, State, Data) -> <type> <v> CallbackMode = - <seealso marker="#type-callback_mode">callback_mode()</seealso> + <seealso marker="#type-callback_mode">callback_mode()</seealso> | + [ <seealso marker="#type-callback_mode">callback_mode()</seealso> + | state_entry_events ] </v> </type> <desc> @@ -1313,12 +1360,35 @@ handle_event(_, _, State, Data) -> <seealso marker="#Module:code_change/4"><c>Module:code_change/4</c></seealso> returns. </p> + <p> + The <c>CallbackMode</c> is either just + <seealso marker="#type-callback_mode">callback_mode()</seealso> + or a list containing + <seealso marker="#type-callback_mode">callback_mode()</seealso> + and possibly the atom + <seealso marker="#type-state_entry_mode"><c>state_entry_events</c></seealso>. + </p> + <p> + If the atom <c>state_entry_events</c> is present in the list, + the <c>gen_statem</c> engine will, at every state change, + insert an event of type + <seealso marker="#type-event_type">enter</seealso> + with content <c>OldState</c>. This event will be inserted + before all other events such as those generated by + <seealso marker="#type-action"><c>action()</c></seealso> + <c>next_event</c>. + </p> + <p> + No state entry event will be inserted after a + <seealso marker="#Module:code_change/4"><c>Module:code_change/4</c></seealso> + since transforming the state to a newer version is regarded + as staying in the same state even if the newer version state + should have a different name. + </p> <note> <p> - If this function's body does not consist of solely one of two - possible - <seealso marker="#type-callback_mode">atoms</seealso> - the callback module is doing something strange. + If this function's body does not return an inline constant + value the callback module is doing something strange. </p> </note> </desc> diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 46c0e92a9b..7f437404ed 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -53,7 +53,7 @@ action/0]). %% Fix problem for doc build --export_type([transition_option/0]). +-export_type([state_entry_mode/0,transition_option/0]). %%%========================================================================== %%% Interface functions. @@ -72,9 +72,10 @@ -type event_type() :: {'call',From :: from()} | 'cast' | - 'info' | 'timeout' | 'internal'. + 'info' | 'timeout' | 'enter' | 'internal'. -type callback_mode() :: 'state_functions' | 'handle_event_function'. +-type state_entry_mode() :: 'state_entry_events'. -type transition_option() :: postpone() | hibernate() | event_timeout(). @@ -183,7 +184,9 @@ %% %% It is called once after init/0 and code_change/4 but before %% the first state callback StateName/3 or handle_event/4. --callback callback_mode() -> callback_mode(). +-callback callback_mode() -> + callback_mode() | + [callback_mode() | state_entry_mode()]. %% Example state callback for StateName = 'state_name' %% when callback_mode() =:= state_functions. @@ -556,19 +559,27 @@ enter(Module, Opts, State, Data, Server, Actions, Parent) -> end, S = #{ callback_mode => undefined, + state_entry_events => false, module => Module, name => Name, - %% All fields below will be replaced according to the arguments to + %% The rest of the fields are set from to the arguments to %% loop_event_actions/10 when it finally loops back to loop/3 - state => State, - data => Data, - postponed => P, - hibernate => false, - timer => undefined}, + %% in loop_events_done/8 + %% + %% Marker for initial state, cleared immediately when used + init_state => true + }, NewDebug = sys_debug(Debug, S, State, {enter,Event,State}), - loop_event_actions( - Parent, NewDebug, S, Events, - State, Data, P, Event, State, NewActions). + case call_callback_mode(S) of + {ok,NewS} -> + loop_event_actions( + Parent, NewDebug, NewS, Events, + State, Data, P, Event, State, NewActions); + {Class,Reason,Stacktrace} -> + terminate( + Class, Reason, Stacktrace, + NewDebug, S, [Event|Events], State, Data, P) + end. %%%========================================================================== %%% gen callbacks @@ -866,13 +877,93 @@ loop_events( loop_events_done(Parent, Debug, S, Timer, State, Data, P, Hibernate) -> NewS = S#{ - state := State, - data := Data, - postponed := P, - hibernate := Hibernate, - timer := Timer}, + state => State, + data => Data, + postponed => P, + hibernate => Hibernate, + timer => Timer}, loop(Parent, Debug, NewS). + + +parse_callback_mode([], CBMode, SEntry) -> + {CBMode,SEntry}; +parse_callback_mode([H|T], CBMode, SEntry) -> + case callback_mode(H) of + true -> + parse_callback_mode(T, H, SEntry); + false -> + case H of + state_entry_events -> + parse_callback_mode(T, CBMode, true); + _ -> + {undefined,SEntry} + end + end; +parse_callback_mode(_, _CBMode, SEntry) -> + {undefined,SEntry}. + +call_callback_mode(S, CallbackMode) -> + case + parse_callback_mode( + if + is_atom(CallbackMode) -> + [CallbackMode]; + true -> + CallbackMode + end, undefined, false) + of + {undefined,_} -> + {error, + {bad_return_from_callback_mode,CallbackMode}, + ?STACKTRACE()}; + {CBMode,SEntry} -> + {ok, + S#{ + callback_mode := CBMode, + state_entry_events := SEntry}} + end. + +call_callback_mode(#{module := Module} = S) -> + try Module:callback_mode() of + CallbackMode -> + call_callback_mode(S, CallbackMode) + catch + CallbackMode -> + call_callback_mode(S, CallbackMode); + error:undef -> + %% Process undef to check for the simple mistake + %% of calling a nonexistent state function + %% to make the undef more precise + case erlang:get_stacktrace() of + [{Module,callback_mode,[]=Args,_} + |Stacktrace] -> + {error, + {undef_callback,{Module,callback_mode,Args}}, + Stacktrace}; + Stacktrace -> + {error,undef,Stacktrace} + end; + Class:Reason -> + {Class,Reason,erlang:get_stacktrace()} + end. + +loop_event( + Parent, Debug, + #{callback_mode := undefined} = S, + Events, + State, Data, P, Event, Hibernate) -> + %% This happens after code_change/4 + case call_callback_mode(S) of + {ok,NewS} -> + loop_event( + Parent, Debug, NewS, Events, + State, Data, P, Event, Hibernate); + {Class,Reason,Stacktrace} -> + terminate( + Class, Reason, Stacktrace, + Debug, S, [Event|Events], State, Data, P) + end; loop_event( Parent, Debug, #{callback_mode := CallbackMode, @@ -891,24 +982,16 @@ loop_event( %% try case CallbackMode of - undefined -> - Module:callback_mode(); state_functions -> erlang:apply(Module, State, [Type,Content,Data]); handle_event_function -> Module:handle_event(Type, Content, State, Data) end of - Result when CallbackMode =:= undefined -> - loop_event_callback_mode( - Parent, Debug, S, Events, State, Data, P, Event, Result); Result -> loop_event_result( Parent, Debug, S, Events, State, Data, P, Event, Result) catch - Result when CallbackMode =:= undefined -> - loop_event_callback_mode( - Parent, Debug, S, Events, State, Data, P, Event, Result); Result -> loop_event_result( Parent, Debug, S, Events, State, Data, P, Event, Result); @@ -936,14 +1019,6 @@ loop_event( %% of calling a nonexistent state function %% to make the undef more precise case erlang:get_stacktrace() of - [{Module,callback_mode,[]=Args,_} - |Stacktrace] - when CallbackMode =:= undefined -> - terminate( - error, - {undef_callback,{Module,callback_mode,Args}}, - Stacktrace, - Debug, S, [Event|Events], State, Data, P); [{Module,State,[Type,Content,Data]=Args,_} |Stacktrace] when CallbackMode =:= state_functions -> @@ -972,25 +1047,6 @@ loop_event( Debug, S, [Event|Events], State, Data, P) end. -%% Interpret callback_mode() result -loop_event_callback_mode( - Parent, Debug, S, Events, State, Data, P, Event, CallbackMode) -> - case callback_mode(CallbackMode) of - true -> - Hibernate = false, % We have already GC:ed recently - loop_event( - Parent, Debug, - S#{callback_mode := CallbackMode}, - Events, - State, Data, P, Event, Hibernate); - false -> - terminate( - error, - {bad_return_from_callback_mode,CallbackMode}, - ?STACKTRACE(), - Debug, S, [Event|Events], State, Data, P) - end. - %% Interpret all callback return variants loop_event_result( Parent, Debug, S, Events, State, Data, P, Event, Result) -> @@ -1174,7 +1230,7 @@ loop_event_actions( %% %% End of actions list loop_event_actions( - Parent, Debug, S, Events, + Parent, Debug, #{state_entry_events := SEEvents} = S, Events, State, NewData, P0, Event, NextState, [], Postpone, Hibernate, Timeout, NextEvents) -> %% @@ -1196,7 +1252,25 @@ loop_event_actions( {lists:reverse(P1, Events),[]} end, %% Place next events first in queue - Q = lists:reverse(NextEvents, Q2), + Q3 = lists:reverse(NextEvents, Q2), + %% State entry events + Q = + case SEEvents of + true -> + %% Generate state entry events + case + (NextState =/= State) + orelse maps:is_key(init_state, S) + of + true -> + %% State change or initial state + [{enter,State}|Q3]; + false -> + Q3 + end; + false -> + Q3 + end, %% NewDebug = sys_debug( @@ -1208,7 +1282,15 @@ loop_event_actions( {consume,Event,NextState} end), loop_events( - Parent, NewDebug, S, Q, NextState, NewData, P, Hibernate, Timeout). + Parent, NewDebug, + %% Avoid infinite loop in initial state with state entry events + case maps:is_key(init_state, S) of + true -> + maps:remove(init_state, S); + false -> + S + end, + Q, NextState, NewData, P, Hibernate, Timeout). %%--------------------------------------------------------------------------- %% Server helpers @@ -1285,7 +1367,9 @@ terminate( error_info( Class, Reason, Stacktrace, - #{name := Name, callback_mode := CallbackMode}, + #{name := Name, + callback_mode := CallbackMode, + state_entry_events := SEEvents}, Q, P, FmtData) -> {FixedReason,FixedStacktrace} = case Stacktrace of @@ -1312,6 +1396,13 @@ error_info( end; _ -> {Reason,Stacktrace} end, + CBMode = + case SEEvents of + true -> + [CallbackMode,state_entry_events]; + false -> + CallbackMode + end, error_logger:format( "** State machine ~p terminating~n" ++ case Q of @@ -1338,8 +1429,9 @@ error_info( [] -> []; [Event|_] -> [Event] end] ++ - [FmtData,Class,FixedReason, - CallbackMode] ++ + [FmtData, + Class,FixedReason, + CBMode] ++ case Q of [_|[_|_] = Events] -> [Events]; _ -> [] diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index e092940174..eef8f265c4 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -37,7 +37,7 @@ all() -> {group, stop_handle_event}, {group, abnormal}, {group, abnormal_handle_event}, - shutdown, stop_and_reply, event_order, code_change, + shutdown, stop_and_reply, enter_events, event_order, code_change, {group, sys}, hibernate, enter_loop]. @@ -556,7 +556,8 @@ stop_and_reply(_Config) -> {stop_and_reply,Reason, [R1,{reply,From2,Reply2}]} end}, - {ok,STM} = gen_statem:start_link(?MODULE, {map_statem,Machine}, []), + {ok,STM} = + gen_statem:start_link(?MODULE, {map_statem,Machine,[]}, []), Self = self(), Tag1 = make_ref(), @@ -581,6 +582,61 @@ stop_and_reply(_Config) -> +enter_events(_Config) -> + process_flag(trap_exit, true), + Self = self(), + + Machine = + %% Abusing the internal format of From... + #{init => + fun () -> + {ok,start,1} + end, + start => + fun (enter, Prev, N) -> + Self ! {enter,start,Prev,N}, + {keep_state,N + 1}; + (internal, Prev, N) -> + Self ! {internal,start,Prev,N}, + {keep_state,N + 1}; + ({call,From}, echo, N) -> + {next_state,wait,N + 1,{reply,From,{echo,start,N}}}; + ({call,From}, {stop,Reason}, N) -> + {stop_and_reply,Reason,[{reply,From,{stop,N}}],N + 1} + end, + wait => + fun (enter, Prev, N) -> + Self ! {enter,wait,Prev,N}, + {keep_state,N + 1}; + ({call,From}, echo, N) -> + {next_state,start,N + 1, + [{next_event,internal,wait}, + {reply,From,{echo,wait,N}}]} + end}, + {ok,STM} = + gen_statem:start_link( + ?MODULE, {map_statem,Machine,[state_entry_events]}, []), + + [{enter,start,start,1}] = flush(), + {echo,start,2} = gen_statem:call(STM, echo), + [{enter,wait,start,3}] = flush(), + {wait,[4|_]} = sys:get_state(STM), + {echo,wait,4} = gen_statem:call(STM, echo), + [{enter,start,wait,5},{internal,start,wait,6}] = flush(), + {stop,7} = gen_statem:call(STM, {stop,bye}), + [{'EXIT',STM,bye}] = flush(), + + {noproc,_} = + ?EXPECT_FAILURE(gen_statem:call(STM, hej), Reason), + case flush() of + [] -> + ok; + Other2 -> + ct:fail({unexpected,Other2}) + end. + + + event_order(_Config) -> process_flag(trap_exit, true), @@ -623,7 +679,7 @@ event_order(_Config) -> Result end}, - {ok,STM} = gen_statem:start_link(?MODULE, {map_statem,Machine}, []), + {ok,STM} = gen_statem:start_link(?MODULE, {map_statem,Machine,[]}, []), Self = self(), Tag1 = make_ref(), gen_statem:cast(STM, {reply,{Self,Tag1},ok1}), @@ -1315,9 +1371,9 @@ init({callback_mode,CallbackMode,Arg}) -> ets:new(?MODULE, [named_table,private]), ets:insert(?MODULE, {callback_mode,CallbackMode}), init(Arg); -init({map_statem,#{init := Init}=Machine}) -> +init({map_statem,#{init := Init}=Machine,Modes}) -> ets:new(?MODULE, [named_table,private]), - ets:insert(?MODULE, {callback_mode,handle_event_function}), + ets:insert(?MODULE, {callback_mode,[handle_event_function|Modes]}), case Init() of {ok,State,Data,Ops} -> {ok,State,[Data|Machine],Ops}; |