From f986565050ac30075ef3c0a451bf6dad91c7c446 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 13 Sep 2016 11:15:32 +0200 Subject: Implement call/3 dirty_timeout --- lib/stdlib/doc/src/gen_statem.xml | 32 ++++++++-- lib/stdlib/src/gen_statem.erl | 110 ++++++++++++++++++++++------------- lib/stdlib/test/gen_statem_SUITE.erl | 46 ++++++++++++++- 3 files changed, 140 insertions(+), 48 deletions(-) (limited to 'lib/stdlib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 3322571b2c..17f1526a21 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -919,18 +919,40 @@ handle_event(_, _, State, Data) ->

- For Timeout =/= infinity, + For Timeout < infinity, to avoid getting a late reply in the caller's - inbox, this function spawns a proxy process that + inbox if the caller should catch exceptions, + this function spawns a proxy process that does the call. A late reply gets delivered to the dead proxy process, hence gets discarded. This is less efficient than using - Timeout =:= infinity. + Timeout == infinity.

- The call can fail, for example, if the gen_statem dies - before or during this function call. + Timeout can also be a tuple + {clean_timeout,T} or + {dirty_timeout,T}, where + T is the timeout time. + {clean_timeout,T} works like + just T described in the note above + and uses a proxy process for T < infinity, + while {dirty_timeout,T} + bypasses the proxy process which is more lightweight. +

+ +

+ If you combine catching exceptions from this function + with {dirty_timeout,T} + to avoid that the calling process dies when the call + times out, you will have to be prepared to handle + a late reply. + So why not just allow the calling process to die? +

+
+

+ The call can also fail, for example, if the gen_statem + dies before or during this function call.

diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 3b3477b282..46c0e92a9b 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -385,53 +385,79 @@ call(ServerRef, Request) -> -spec call( ServerRef :: server_ref(), Request :: term(), - Timeout :: timeout()) -> + Timeout :: + timeout() | + {'clean_timeout',T :: timeout()} | + {'dirty_timeout',T :: timeout()}) -> Reply :: term(). -call(ServerRef, Request, infinity) -> - try gen:call(ServerRef, '$gen_call', Request, infinity) of - {ok,Reply} -> - Reply - catch - Class:Reason -> - erlang:raise( - Class, - {Reason,{?MODULE,call,[ServerRef,Request,infinity]}}, - erlang:get_stacktrace()) - end; call(ServerRef, Request, Timeout) -> - %% Call server through proxy process to dodge any late reply - Ref = make_ref(), - Self = self(), - Pid = spawn( - fun () -> - Self ! - try gen:call( - ServerRef, '$gen_call', Request, Timeout) of - Result -> - {Ref,Result} - catch Class:Reason -> - {Ref,Class,Reason,erlang:get_stacktrace()} - end - end), - Mref = monitor(process, Pid), - receive - {Ref,Result} -> - demonitor(Mref, [flush]), - case Result of + case parse_timeout(Timeout) of + {dirty_timeout,T} -> + try gen:call(ServerRef, '$gen_call', Request, T) of {ok,Reply} -> Reply + catch + Class:Reason -> + erlang:raise( + Class, + {Reason,{?MODULE,call,[ServerRef,Request,Timeout]}}, + erlang:get_stacktrace()) + end; + {clean_timeout,T} -> + %% Call server through proxy process to dodge any late reply + Ref = make_ref(), + Self = self(), + Pid = spawn( + fun () -> + Self ! + try gen:call( + ServerRef, '$gen_call', Request, T) of + Result -> + {Ref,Result} + catch Class:Reason -> + {Ref,Class,Reason, + erlang:get_stacktrace()} + end + end), + Mref = monitor(process, Pid), + receive + {Ref,Result} -> + demonitor(Mref, [flush]), + case Result of + {ok,Reply} -> + Reply + end; + {Ref,Class,Reason,Stacktrace} -> + demonitor(Mref, [flush]), + erlang:raise( + Class, + {Reason,{?MODULE,call,[ServerRef,Request,Timeout]}}, + Stacktrace); + {'DOWN',Mref,_,_,Reason} -> + %% There is a theoretical possibility that the + %% proxy process gets killed between try--of and ! + %% so this clause is in case of that + exit(Reason) end; - {Ref,Class,Reason,Stacktrace} -> - demonitor(Mref, [flush]), - erlang:raise( - Class, - {Reason,{?MODULE,call,[ServerRef,Request,Timeout]}}, - Stacktrace); - {'DOWN',Mref,_,_,Reason} -> - %% There is a theoretical possibility that the - %% proxy process gets killed between try--of and ! - %% so this clause is in case of that - exit(Reason) + Error when is_atom(Error) -> + erlang:error(Error, [ServerRef,Request,Timeout]) + end. + +parse_timeout(Timeout) -> + case Timeout of + {clean_timeout,infinity} -> + {dirty_timeout,infinity}; + {clean_timeout,_} -> + Timeout; + {dirty_timeout,_} -> + Timeout; + {_,_} -> + %% Be nice and throw a badarg for speling errors + badarg; + infinity -> + {dirty_timeout,infinity}; + T -> + {clean_timeout,T} end. %% Reply from a state machine callback to whom awaits in call/2 diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 1d1417c2e6..e092940174 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -57,7 +57,7 @@ tcs(start) -> tcs(stop) -> [stop1, stop2, stop3, stop4, stop5, stop6, stop7, stop8, stop9, stop10]; tcs(abnormal) -> - [abnormal1, abnormal2]; + [abnormal1, abnormal1clean, abnormal1dirty, abnormal2]; tcs(sys) -> [sys1, call_format_status, error_format_status, terminate_crash_format, @@ -451,8 +451,52 @@ abnormal1(Config) -> gen_statem:call(Name, {delayed_answer,1000}, 10), Reason), ok = gen_statem:stop(Name), + ?t:sleep(1100), ok = verify_empty_msgq(). +%% Check that time outs in calls work +abnormal1clean(Config) -> + Name = abnormal1clean, + LocalSTM = {local,Name}, + + {ok, _Pid} = + gen_statem:start(LocalSTM, ?MODULE, start_arg(Config, []), []), + + %% timeout call. + delayed = + gen_statem:call(Name, {delayed_answer,1}, {clean_timeout,100}), + {timeout,_} = + ?EXPECT_FAILURE( + gen_statem:call( + Name, {delayed_answer,1000}, {clean_timeout,10}), + Reason), + ok = gen_statem:stop(Name), + ?t:sleep(1100), + ok = verify_empty_msgq(). + +%% Check that time outs in calls work +abnormal1dirty(Config) -> + Name = abnormal1dirty, + LocalSTM = {local,Name}, + + {ok, _Pid} = + gen_statem:start(LocalSTM, ?MODULE, start_arg(Config, []), []), + + %% timeout call. + delayed = + gen_statem:call(Name, {delayed_answer,1}, {dirty_timeout,100}), + {timeout,_} = + ?EXPECT_FAILURE( + gen_statem:call( + Name, {delayed_answer,1000}, {dirty_timeout,10}), + Reason), + ok = gen_statem:stop(Name), + ?t:sleep(1100), + case flush() of + [{Ref,delayed}] when is_reference(Ref) -> + ok + end. + %% Check that bad return values makes the stm crash. Note that we must %% trap exit since we must link to get the real bad_return_ error abnormal2(Config) -> -- cgit v1.2.3 From 6ee0aefd8a0ea9c165211c42d5244182b5aa9210 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 13 Sep 2016 14:00:04 +0200 Subject: Implement state entry events --- lib/stdlib/doc/src/gen_statem.xml | 82 +++++++++++++- lib/stdlib/src/gen_statem.erl | 208 +++++++++++++++++++++++++---------- lib/stdlib/test/gen_statem_SUITE.erl | 66 ++++++++++- 3 files changed, 287 insertions(+), 69 deletions(-) (limited to 'lib/stdlib') 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 @@ The state can be any term. Events can be postponed. Events can be self-generated. + Automatic state entry events can be generated. A reply can be sent from a later state. There can be multiple sys traceable replies. @@ -193,6 +194,12 @@ erlang:'!' -----> Module:StateName/3 gen_fsm to force processing an inserted event before others.

+

+ The gen_statem engine can automatically insert + a special event whenever a new state is entered; see + state_entry_mode(). + This makes it easy to handle code common to all state entries. +

If you in gen_statem, for example, postpone an event in one state and then call another state function @@ -515,7 +522,7 @@ handle_event(_, _, State, Data) -> Type info originates from regular process messages sent to the gen_statem. Also, the state machine implementation can generate events of types - timeout and internal to itself. + timeout, enter and internal to itself.

@@ -550,6 +557,34 @@ handle_event(_, _, State, Data) -> + + + +

+ The state entry mode is selected when starting the + gen_statem and after code change + using the return value from + Module:callback_mode/0. +

+

+ If + Module:callback_mode/0 + returns a list containing state_entry_events, + the gen_statem engine will, at every state change, + insert an event of type + enter + with content OldState. This event will be inserted + before all other events such as those generated by + action() + next_event. +

+

+ If + Module:callback_mode/0 + does not return such a list, no state entry events are inserted. +

+
+
@@ -590,6 +625,16 @@ handle_event(_, _, State, Data) -> all other events.

+ +

+ If the state changes or is the initial state, and the + state entry mode + is state_entry_events, an event of type + enter + with content OldState is inserted + to be processed before all other events including those above. +

+

If an @@ -1288,7 +1333,9 @@ handle_event(_, _, State, Data) -> CallbackMode = - callback_mode() + callback_mode() | + [ callback_mode() + | state_entry_events ] @@ -1313,12 +1360,35 @@ handle_event(_, _, State, Data) -> Module:code_change/4 returns.

+

+ The CallbackMode is either just + callback_mode() + or a list containing + callback_mode() + and possibly the atom + state_entry_events. +

+

+ If the atom state_entry_events is present in the list, + the gen_statem engine will, at every state change, + insert an event of type + enter + with content OldState. This event will be inserted + before all other events such as those generated by + action() + next_event. +

+

+ No state entry event will be inserted after a + Module:code_change/4 + 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. +

- If this function's body does not consist of solely one of two - possible - atoms - 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.

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}; -- cgit v1.2.3 From 4ebdabdca2c964887115f21405993f3916843d10 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 16 Sep 2016 10:15:22 +0200 Subject: Improve docs --- lib/stdlib/doc/src/gen_statem.xml | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) (limited to 'lib/stdlib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index a4c5438a08..944e9ab13b 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -583,6 +583,20 @@ handle_event(_, _, State, Data) -> Module:callback_mode/0 does not return such a list, no state entry events are inserted.

+

+ No state entry event will be inserted after a + Module:code_change/4 + 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. +

+

+ Note that a state entry event will be inserted + when entering the initial state even though this formally + is not a state change. In this case OldState + will be the same as State, which can not happen + for an actual state change. +

@@ -1335,7 +1349,7 @@ handle_event(_, _, State, Data) -> CallbackMode = callback_mode() | [ callback_mode() - | state_entry_events ] + | state_entry_events ] @@ -1368,23 +1382,6 @@ handle_event(_, _, State, Data) -> and possibly the atom state_entry_events.

-

- If the atom state_entry_events is present in the list, - the gen_statem engine will, at every state change, - insert an event of type - enter - with content OldState. This event will be inserted - before all other events such as those generated by - action() - next_event. -

-

- No state entry event will be inserted after a - Module:code_change/4 - 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. -

If this function's body does not return an inline constant -- cgit v1.2.3 From 04d40c5cd18aca449606c19608e8044f593ee99e Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 22 Sep 2016 17:40:47 +0200 Subject: Change state entry events into state enter calls --- lib/stdlib/doc/src/gen_statem.xml | 305 +++++++++++++----- lib/stdlib/src/gen_statem.erl | 582 +++++++++++++++++++---------------- lib/stdlib/test/gen_statem_SUITE.erl | 6 +- 3 files changed, 543 insertions(+), 350 deletions(-) (limited to 'lib/stdlib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 944e9ab13b..aa34f53d29 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -54,7 +54,8 @@

This is a new behavior in Erlang/OTP 19.0. It has been thoroughly reviewed, is stable enough - to be used by at least two heavy OTP applications, and is here to stay. + to be used by at least two heavy OTP applications, + and is here to stay. Depending on user feedback, we do not expect but can find it necessary to make minor not backward compatible changes into Erlang/OTP 20.0. @@ -70,7 +71,7 @@ The state can be any term. Events can be postponed. Events can be self-generated. - Automatic state entry events can be generated. + Automatic state enter code can be called. A reply can be sent from a later state. There can be multiple sys traceable replies. @@ -195,10 +196,14 @@ erlang:'!' -----> Module:StateName/3 to force processing an inserted event before others.

- The gen_statem engine can automatically insert - a special event whenever a new state is entered; see - state_entry_mode(). - This makes it easy to handle code common to all state entries. + The gen_statem engine can automatically + make a specialized call to the + state function + whenever a new state is entered; see + state_enter(). + This is for writing code common to all state entries. + Another way to do it is to insert events at state transitions, + but you have to do so everywhere it is needed.

If you in gen_statem, for example, postpone @@ -526,6 +531,20 @@ handle_event(_, _, State, Data) ->

+ + + +

+ This is the return type from + Module:callback_mode/0 + and selects + callback mode + and whether to do + state enter calls, + or not. +

+
+
@@ -558,44 +577,48 @@ handle_event(_, _, State, Data) -> - +

- The state entry mode is selected when starting the - gen_statem and after code change - using the return value from + If the state machine should use state enter calls + is selected when starting the gen_statem + and after code change using the return value from Module:callback_mode/0.

If Module:callback_mode/0 - returns a list containing state_entry_events, + returns a list containing state_enter, the gen_statem engine will, at every state change, - insert an event of type - enter - with content OldState. This event will be inserted - before all other events such as those generated by - action() - next_event. + call the + state function + with arguments (enter, OldState, Data). + This may look like an event but is really a call + performed after the previous state function returned + and before any event is delivered to the new state function. + See + Module:StateName/3 + and + Module:handle_event/4.

If Module:callback_mode/0 - does not return such a list, no state entry events are inserted. + does not return such a list, no state enter calls are done.

- No state entry event will be inserted after a + If Module:code_change/4 - 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. + should transform the state to a state with a different + name it is still regarded as the same state so this + does not cause a state enter call.

- Note that a state entry event will be inserted - when entering the initial state even though this formally - is not a state change. In this case OldState - will be the same as State, which can not happen - for an actual state change. + Note that a state enter call will be done + right before entering the initial state even though this + formally is not a state change. + In this case OldState will be the same as State, + which can not happen for a subsequent state change.

@@ -605,8 +628,7 @@ handle_event(_, _, State, Data) ->

Transition options can be set by actions - and they modify the following in how - the state transition is done: + and they modify how the state transition is done:

@@ -636,17 +658,22 @@ handle_event(_, _, State, Data) -> action() next_event are inserted in the queue to be processed before - all other events. + other events.

- If the state changes or is the initial state, and the - state entry mode - is state_entry_events, an event of type - enter - with content OldState is inserted - to be processed before all other events including those above. + If the state changes or is the initial state, and + state enter calls + are used, the gen_statem calls + the new state function with arguments + (enter, OldState, Data). + If this call returns any + actions + that sets transition options + they are merged with the current + That is: hibernate and timeout overrides + the current and reply sends a reply.

@@ -656,8 +683,9 @@ handle_event(_, _, State, Data) -> is set through action() timeout, - an event timer can be started or a time-out zero event - can be enqueued. + an event timer is started if the value is less than + infinity or a time-out zero event + is enqueued if the value is zero.

@@ -732,7 +760,7 @@ handle_event(_, _, State, Data) -> is processed before any not yet received external event.

- Notice that it is not possible or needed to cancel this time-out, + Note that it is not possible or needed to cancel this time-out, as it is cancelled automatically by any other event.

@@ -743,7 +771,10 @@ handle_event(_, _, State, Data) ->

These state transition actions can be invoked by returning them from the - state function, from + state function + when it is called with an + event, + from Module:init/1 or by giving them to enter_loop/5,6. @@ -757,8 +788,8 @@ handle_event(_, _, State, Data) -> override any previous of the same type, so the last in the containing list wins. For example, the last - event_timeout() - overrides any other event_timeout() in the list. + postpone() + overrides any previous postpone() in the list.

postpone @@ -775,6 +806,53 @@ handle_event(_, _, State, Data) -> as there is no event to postpone in those cases.

+ next_event + +

+ Stores the specified EventType + and EventContent for insertion after all + actions have been executed. +

+

+ The stored events are inserted in the queue as the next to process + before any already queued events. The order of these stored events + is preserved, so the first next_event in the containing + list becomes the first to process. +

+

+ An event of type + internal + is to be used when you want to reliably distinguish + an event inserted this way from any external event. +

+
+ + + + + + +

+ These state transition actions can be invoked by + returning them from the + state function, from + Module:init/1 + or by giving them to + enter_loop/5,6. +

+

+ Actions are executed in the containing list order. +

+

+ Actions that set + transition options + override any previous of the same type, + so the last in the containing list wins. + For example, the last + event_timeout() + overrides any previous event_timeout() in the list. +

+ hibernate

@@ -805,32 +883,6 @@ handle_event(_, _, State, Data) -> to Time with EventContent.

- reply_action() - -

- Replies to a caller. -

-
- next_event - -

- Stores the specified EventType - and EventContent for insertion after all - actions have been executed. -

-

- The stored events are inserted in the queue as the next to process - before any already queued events. The order of these stored events - is preserved, so the first next_event in the containing - list becomes the first to process. -

-

- An event of type - internal - is to be used when you want to reliably distinguish - an event inserted this way from any external event. -

-
@@ -838,13 +890,31 @@ handle_event(_, _, State, Data) ->

- Replies to a caller waiting for a reply in + This state transition action can be invoked by + returning it from the + state function, from + Module:init/1 + or by giving it to + enter_loop/5,6. +

+

+ It replies to a caller waiting for a reply in call/2. From must be the term from argument {call,From} - to the + in a call to a state function.

+

+ Note that using this action from + Module:init/1 + or + enter_loop/5,6 + would be weird on the border of whichcraft + since there has been no earlier call to a + state function + in this server. +

@@ -868,6 +938,27 @@ handle_event(_, _, State, Data) ->

+ + + + + next_state + +

+ The gen_statem does a state transition to + NextStateName + (which can be the same as the current state), + sets NewData, + and executes all Actions. +

+
+
+

+ All these terms are tuples or atoms and this property + will hold in any future version of gen_statem. +

+
+
@@ -889,6 +980,27 @@ handle_event(_, _, State, Data) ->

+ + + + + next_state + +

+ The gen_statem does a state transition to + NextState + (which can be the same as the current state), + sets NewData, + and executes all Actions. +

+
+
+

+ All these terms are tuples or atoms and this property + will hold in any future version of gen_statem. +

+
+
@@ -1148,10 +1260,11 @@ handle_event(_, _, State, Data) -> {call,From} to the state function. - From and Reply - can also be specified using a - reply_action() - and multiple replies with a list of them. + A reply or multiple replies canalso be sent + using one or several + reply_action()s + from a + state function.

@@ -1362,7 +1475,8 @@ handle_event(_, _, State, Data) -> once after server start and after code change, but before the first state function - is called. More occasions may be added in future versions + in the current code version is called. + More occasions may be added in future versions of gen_statem.

@@ -1380,7 +1494,7 @@ handle_event(_, _, State, Data) -> or a list containing callback_mode() and possibly the atom - state_entry_events. + state_enter.

@@ -1601,7 +1715,8 @@ handle_event(_, _, State, Data) ->

The function is to return Status, a term that - changes the details of the current state and status of + contains the appropriate details + of the current state and status of the gen_statem. There are no restrictions on the form Status can take, but for the sys:get_status/1,2 @@ -1625,11 +1740,17 @@ handle_event(_, _, State, Data) -> + Module:StateName(enter, OldState, Data) -> + StateFunctionEnterResult + Module:StateName(EventType, EventContent, Data) -> StateFunctionResult - Module:handle_event(EventType, EventContent, - State, Data) -> HandleEventResult + Module:handle_event(enter, OldState, State, Data) -> + HandleEventResult + + Module:handle_event(EventType, EventContent, State, Data) -> + HandleEventResult Handle an event. @@ -1650,10 +1771,18 @@ handle_event(_, _, State, Data) -> StateFunctionResult = state_function_result() + + StateFunctionEnterResult = + state_function_enter_result() + HandleEventResult = handle_event_result() + + HandleEventEnterResult = + handle_event_enter_result() +

@@ -1694,6 +1823,24 @@ handle_event(_, _, State, Data) -> by gen_statem after returning from this function, see action().

+

+ When the gen_statem runs with + state enter calls, + these functions are also called with arguments + (enter, OldState, ...) whenever the state changes. + In this case there are some restrictions on the + actions + that may be returned: + postpone() + and + {next_event,_,_} + are not allowed. + You may also not change states from this call. + Should you return {next_state,NextState, ...} + with NextState =/= State the gen_statem crashes. + You are advised to use {keep_state,...} or + keep_state_and_data. +

Note the fact that you can use throw diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 7f437404ed..aedcfc932f 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -47,13 +47,16 @@ %% Type exports for templates -export_type( [event_type/0, - callback_mode/0, + state_name/0, + callback_mode_result/0, state_function_result/0, + state_function_enter_result/0, handle_event_result/0, + handle_event_enter_result/0, action/0]). %% Fix problem for doc build --export_type([state_entry_mode/0,transition_option/0]). +-export_type([transition_option/0]). %%%========================================================================== %%% Interface functions. @@ -72,10 +75,12 @@ -type event_type() :: {'call',From :: from()} | 'cast' | - 'info' | 'timeout' | 'enter' | 'internal'. + 'info' | 'timeout' | 'internal'. +-type callback_mode_result() :: + callback_mode() | [callback_mode() | state_enter()]. -type callback_mode() :: 'state_functions' | 'handle_event_function'. --type state_entry_mode() :: 'state_entry_events'. +-type state_enter() :: 'state_enter'. -type transition_option() :: postpone() | hibernate() | event_timeout(). @@ -109,6 +114,14 @@ 'postpone' | % Set the postpone option {'postpone', Postpone :: postpone()} | %% + %% All 'next_event' events are kept in a list and then + %% inserted at state changes so the first in the + %% action() list is the first to be delivered. + {'next_event', % Insert event as the next to handle + EventType :: event_type(), + EventContent :: term()} | + enter_action(). +-type enter_action() :: 'hibernate' | % Set the hibernate option {'hibernate', Hibernate :: hibernate()} | %% @@ -116,14 +129,7 @@ {'timeout', % Set the event timeout option Time :: event_timeout(), EventContent :: term()} | %% - reply_action() | - %% - %% All 'next_event' events are kept in a list and then - %% inserted at state changes so the first in the - %% action() list is the first to be delivered. - {'next_event', % Insert event as the next to handle - EventType :: event_type(), - EventContent :: term()}. + reply_action(). -type reply_action() :: {'reply', % Reply to a caller From :: from(), Reply :: term()}. @@ -137,6 +143,16 @@ NewData :: data(), Actions :: [action()] | action()} | common_state_callback_result(). +-type state_function_enter_result() :: + {'next_state', % {next_state,NextStateName,NewData,[]} + NextStateName :: state_name(), + NewData :: data()} | + {'next_state', % State transition, maybe to the same state + NextStateName :: state_name(), + NewData :: data(), + Actions :: [enter_action()] | enter_action()} | + common_state_callback_result(). + -type handle_event_result() :: {'next_state', % {next_state,NextState,NewData,[]} NextState :: state(), @@ -146,6 +162,16 @@ NewData :: data(), Actions :: [action()] | action()} | common_state_callback_result(). +-type handle_event_enter_result() :: + {'next_state', % {next_state,NextState,NewData,[]} + NextState :: state(), + NewData :: data()} | + {'next_state', % State transition, maybe to the same state + NextState :: state(), + NewData :: data(), + Actions :: [enter_action()] | enter_action()} | + common_state_callback_result(). + -type common_state_callback_result() :: 'stop' | % {stop,normal} {'stop', % Stop the server @@ -164,10 +190,10 @@ NewData :: data()} | {'keep_state', % Keep state, change data NewData :: data(), - Actions :: [action()] | action()} | + Actions :: [ActionType] | ActionType} | 'keep_state_and_data' | % {keep_state_and_data,[]} {'keep_state_and_data', % Keep state and data -> only actions - Actions :: [action()] | action()}. + Actions :: [ActionType] | ActionType}. %% The state machine init function. It is called only once and @@ -184,9 +210,7 @@ %% %% 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_mode() | state_entry_mode()]. +-callback callback_mode() -> callback_mode_result(). %% Example state callback for StateName = 'state_name' %% when callback_mode() =:= state_functions. @@ -197,7 +221,11 @@ %% StateName/3 callbacks and terminate/3, so the state name %% 'terminate' is unusable in this mode. -callback state_name( - event_type(), + 'enter', + OldStateName :: state_name(), + Data :: data()) -> + state_function_enter_result(); + (event_type(), EventContent :: term(), Data :: data()) -> state_function_result(). @@ -205,7 +233,12 @@ %% State callback for all states %% when callback_mode() =:= handle_event_function. -callback handle_event( - event_type(), + 'enter', + OldState :: state(), + State :: state(), % Current state + Data :: data()) -> + handle_event_enter_result(); + (event_type(), EventContent :: term(), State :: state(), % Current state Data :: data()) -> @@ -547,7 +580,7 @@ enter(Module, Opts, State, Data, Server, Actions, Parent) -> Name = gen:get_proc_name(Server), Debug = gen:debug_options(Name, Opts), P = Events = [], - Event = {internal,initial_state}, + Event = {internal,init_state}, %% We enforce {postpone,false} to ensure that %% our fake Event gets discarded, thought it might get logged NewActions = @@ -559,7 +592,7 @@ enter(Module, Opts, State, Data, Server, Actions, Parent) -> end, S = #{ callback_mode => undefined, - state_entry_events => false, + state_enter => false, module => Module, name => Name, %% The rest of the fields are set from to the arguments to @@ -886,51 +919,13 @@ loop_events_done(Parent, Debug, S, Timer, State, Data, P, Hibernate) -> -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) + call_callback_mode_result(S, CallbackMode) catch CallbackMode -> - call_callback_mode(S, CallbackMode); + call_callback_mode_result(S, CallbackMode); error:undef -> %% Process undef to check for the simple mistake %% of calling a nonexistent state function @@ -948,38 +943,57 @@ call_callback_mode(#{module := Module} = S) -> {Class,Reason,erlang:get_stacktrace()} end. -loop_event( - Parent, Debug, +call_callback_mode_result(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,StateEnter} -> + {ok, + S#{ + callback_mode := CBMode, + state_enter := StateEnter}} + end. + +parse_callback_mode([], CBMode, StateEnter) -> + {CBMode,StateEnter}; +parse_callback_mode([H|T], CBMode, StateEnter) -> + case callback_mode(H) of + true -> + parse_callback_mode(T, H, StateEnter); + false -> + case H of + state_enter -> + parse_callback_mode(T, CBMode, true); + _ -> + {undefined,StateEnter} + end + end; +parse_callback_mode(_, _CBMode, StateEnter) -> + {undefined,StateEnter}. + +call_state_function( #{callback_mode := undefined} = S, - Events, - State, Data, P, Event, Hibernate) -> - %% This happens after code_change/4 + Type, Content, State, Data) -> 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) + call_state_function(NewS, Type, Content, State, Data); + Error -> + Error end; -loop_event( - Parent, Debug, +call_state_function( #{callback_mode := CallbackMode, module := Module} = S, - Events, - State, Data, P, {Type,Content} = Event, Hibernate) -> - %% - %% If Hibernate is true here it can only be - %% because it was set from an event action - %% and we did not go into hibernation since there - %% were events in queue, so we do what the user - %% might depend on i.e collect garbage which - %% would have happened if we actually hibernated - %% and immediately was awakened - Hibernate andalso garbage_collect(), - %% + Type, Content, State, Data) -> try case CallbackMode of state_functions -> @@ -989,12 +1003,10 @@ loop_event( end of Result -> - loop_event_result( - Parent, Debug, S, Events, State, Data, P, Event, Result) + {ok,Result,S} catch Result -> - loop_event_result( - Parent, Debug, S, Events, State, Data, P, Event, Result); + {ok,Result,S}; error:badarg -> case erlang:get_stacktrace() of [{erlang,apply, @@ -1004,15 +1016,11 @@ loop_event( when CallbackMode =:= state_functions -> %% We get here e.g if apply fails %% due to State not being an atom - terminate( - error, - {undef_state_function,{Module,State,Args}}, - Stacktrace, - Debug, S, [Event|Events], State, Data, P); + {error, + {undef_state_function,{Module,State,Args}}, + Stacktrace}; Stacktrace -> - terminate( - error, badarg, Stacktrace, - Debug, S, [Event|Events], State, Data, P) + {error,badarg,Stacktrace} end; error:undef -> %% Process undef to check for the simple mistake @@ -1022,34 +1030,54 @@ loop_event( [{Module,State,[Type,Content,Data]=Args,_} |Stacktrace] when CallbackMode =:= state_functions -> - terminate( - error, - {undef_state_function,{Module,State,Args}}, - Stacktrace, - Debug, S, [Event|Events], State, Data, P); + {error, + {undef_state_function,{Module,State,Args}}, + Stacktrace}; [{Module,handle_event,[Type,Content,State,Data]=Args,_} |Stacktrace] when CallbackMode =:= handle_event_function -> - terminate( - error, - {undef_state_function,{Module,handle_event,Args}}, - Stacktrace, - Debug, S, [Event|Events], State, Data, P); + {error, + {undef_state_function,{Module,handle_event,Args}}, + Stacktrace}; Stacktrace -> - terminate( - error, undef, Stacktrace, - Debug, S, [Event|Events], State, Data, P) + {error,undef,Stacktrace} end; Class:Reason -> - Stacktrace = erlang:get_stacktrace(), + {Class,Reason,erlang:get_stacktrace()} + end. + +loop_event( + Parent, Debug, S, Events, + State, Data, P, {Type,Content} = Event, Hibernate) -> + %% + %% If Hibernate is true here it can only be + %% because it was set from an event action + %% and we did not go into hibernation since there + %% were events in queue, so we do what the user + %% might depend on i.e collect garbage which + %% would have happened if we actually hibernated + %% and immediately was awakened + Hibernate andalso garbage_collect(), + case call_state_function(S, Type, Content, State, Data) of + {ok,Result,NewS} -> + {NewData,NextState,Actions} = + parse_event_result( + Parent, Debug, NewS, Events, + State, Data, P, Event, + Result, true), + loop_event_actions( + Parent, Debug, S, Events, + State, NewData, P, Event, NextState, Actions); + {Class,Reason,Stacktrace} -> terminate( Class, Reason, 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) -> +parse_event_result( + _Parent, Debug, S, Events, State, Data, P, Event, + Result, AllowStateChange) -> case Result of stop -> terminate( @@ -1073,30 +1101,22 @@ loop_event_result( reply_then_terminate( exit, Reason, ?STACKTRACE(), Debug, S, Q, State, NewData, P, Replies); - {next_state,NextState,NewData} -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, []); - {next_state,NextState,NewData,Actions} -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions); + {next_state,State,NewData} -> + {NewData,State,[]}; + {next_state,NextState,NewData} when AllowStateChange -> + {NewData,NextState,[]}; + {next_state,State,NewData,Actions} -> + {NewData,State,Actions}; + {next_state,NextState,NewData,Actions} when AllowStateChange -> + {NewData,NextState,Actions}; {keep_state,NewData} -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, State, []); + {NewData,State,[]}; {keep_state,NewData,Actions} -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, State, Actions); + {NewData,State,Actions}; keep_state_and_data -> - loop_event_actions( - Parent, Debug, S, Events, - State, Data, P, Event, State, []); + {Data,State,[]}; {keep_state_and_data,Actions} -> - loop_event_actions( - Parent, Debug, S, Events, - State, Data, P, Event, State, Actions); + {Data,State,Actions}; _ -> terminate( error, @@ -1105,134 +1125,178 @@ loop_event_result( Debug, S, [Event|Events], State, Data, P) end. -loop_event_actions( - Parent, Debug, S, Events, State, NewData, P, Event, NextState, Actions) -> - Postpone = false, % Shall we postpone this event; boolean() +parse_enter_actions(Debug, S, State, Actions, Hibernate, Timeout) -> + Postpone = forbidden, + NextEvents = forbidden, + parse_actions( + Debug, S, State, listify(Actions), + Hibernate, Timeout, Postpone, NextEvents). + +parse_actions(Debug, S, State, Actions) -> + Postpone = false, Hibernate = false, Timeout = undefined, NextEvents = [], - loop_event_actions( - Parent, Debug, S, Events, State, NewData, P, Event, NextState, - if - is_list(Actions) -> - Actions; - true -> - [Actions] - end, - Postpone, Hibernate, Timeout, NextEvents). + parse_actions( + Debug, S, State, listify(Actions), + Hibernate, Timeout, Postpone, NextEvents). %% -%% Process all actions -loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, [Action|Actions], - Postpone, Hibernate, Timeout, NextEvents) -> +parse_actions( + Debug, _S, _State, [], Hibernate, Timeout, Postpone, NextEvents) -> + {ok,Debug,Hibernate,Timeout,Postpone,NextEvents}; +parse_actions( + Debug, S, State, [Action|Actions], + Hibernate, Timeout, Postpone, NextEvents) -> case Action of %% Actual actions {reply,From,Reply} -> case from(From) of true -> NewDebug = do_reply(Debug, S, State, From, Reply), - loop_event_actions( - Parent, NewDebug, S, Events, - State, NewData, P, Event, NextState, Actions, - Postpone, Hibernate, Timeout, NextEvents); + parse_actions( + NewDebug, S, State, Actions, + Hibernate, Timeout, Postpone, NextEvents); false -> - terminate( - error, - {bad_action_from_state_function,Action}, - ?STACKTRACE(), - Debug, S, [Event|Events], State, NewData, P) - end; - {next_event,Type,Content} -> - case event_type(Type) of - true -> - NewDebug = - sys_debug(Debug, S, State, {in,{Type,Content}}), - loop_event_actions( - Parent, NewDebug, S, Events, - State, NewData, P, Event, NextState, Actions, - Postpone, Hibernate, Timeout, - [{Type,Content}|NextEvents]); - false -> - terminate( - error, - {bad_action_from_state_function,Action}, - ?STACKTRACE(), - Debug, S, [Event|Events], State, NewData, P) + {error, + {bad_action_from_state_function,Action}, + ?STACKTRACE()} end; %% Actions that set options - {postpone,NewPostpone} when is_boolean(NewPostpone) -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions, - NewPostpone, Hibernate, Timeout, NextEvents); - {postpone,_} -> - terminate( - error, - {bad_action_from_state_function,Action}, - ?STACKTRACE(), - Debug, S, [Event|Events], State, NewData, P); - postpone -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions, - true, Hibernate, Timeout, NextEvents); {hibernate,NewHibernate} when is_boolean(NewHibernate) -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions, - Postpone, NewHibernate, Timeout, NextEvents); + parse_actions( + Debug, S, State, Actions, + NewHibernate, Timeout, Postpone, NextEvents); {hibernate,_} -> - terminate( - error, - {bad_action_from_state_function,Action}, - ?STACKTRACE(), - Debug, S, [Event|Events], State, NewData, P); + {error, + {bad_action_from_state_function,Action}, + ?STACKTRACE()}; hibernate -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions, - Postpone, true, Timeout, NextEvents); + parse_actions( + Debug, S, State, Actions, + true, Timeout, Postpone, NextEvents); {timeout,infinity,_} -> % Clear timer - it will never trigger - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions, - Postpone, Hibernate, undefined, NextEvents); + parse_actions( + Debug, S, State, Actions, + Hibernate, undefined, Postpone, NextEvents); {timeout,Time,_} = NewTimeout when is_integer(Time), Time >= 0 -> - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions, - Postpone, Hibernate, NewTimeout, NextEvents); + parse_actions( + Debug, S, State, Actions, + Hibernate, NewTimeout, Postpone, NextEvents); {timeout,_,_} -> - terminate( - error, - {bad_action_from_state_function,Action}, - ?STACKTRACE(), - Debug, S, [Event|Events], State, NewData, P); + {error, + {bad_action_from_state_function,Action}, + ?STACKTRACE()}; infinity -> % Clear timer - it will never trigger - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions, - Postpone, Hibernate, undefined, NextEvents); + parse_actions( + Debug, S, State, Actions, + Hibernate, undefined, Postpone, NextEvents); Time when is_integer(Time), Time >= 0 -> NewTimeout = {timeout,Time,Time}, - loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions, - Postpone, Hibernate, NewTimeout, NextEvents); + parse_actions( + Debug, S, State, Actions, + Hibernate, NewTimeout, Postpone, NextEvents); + {postpone,NewPostpone} + when is_boolean(NewPostpone), Postpone =/= forbidden -> + parse_actions( + Debug, S, State, Actions, + Hibernate, Timeout, NewPostpone, NextEvents); + {postpone,_} -> + {error, + {bad_action_from_state_function,Action}, + ?STACKTRACE()}; + postpone when Postpone =/= forbidden -> + parse_actions( + Debug, S, State, Actions, + Hibernate, Timeout, true, NextEvents); + {next_event,Type,Content} -> + case event_type(Type) of + true when NextEvents =/= forbidden -> + NewDebug = + sys_debug(Debug, S, State, {in,{Type,Content}}), + parse_actions( + NewDebug, S, State, Actions, + Hibernate, Timeout, Postpone, + [{Type,Content}|NextEvents]); + _ -> + {error, + {bad_action_from_state_function,Action}, + ?STACKTRACE()} + end; _ -> + {error, + {bad_action_from_state_function,Action}, + ?STACKTRACE()} + end. + +loop_event_actions( + Parent, Debug, #{state_enter := StateEnter} = S, Events, + State, NewData, P, Event, NextState, Actions) -> + case parse_actions(Debug, S, State, Actions) of + {ok,NewDebug,Hibernate,Timeout,Postpone,NextEvents} -> + case + StateEnter andalso + ((NextState =/= State) + orelse maps:is_key(init_state, S)) of + true -> + loop_event_enter( + Parent, NewDebug, S, Events, + State, NewData, P, Event, NextState, + Hibernate, Timeout, Postpone, NextEvents); + false -> + loop_event_result( + Parent, NewDebug, S, Events, + State, NewData, P, Event, NextState, + Hibernate, Timeout, Postpone, NextEvents) + end; + {Class,Reason,Stacktrace} -> terminate( - error, - {bad_action_from_state_function,Action}, - ?STACKTRACE(), + Class, Reason, Stacktrace, Debug, S, [Event|Events], State, NewData, P) - end; -%% -%% End of actions list -loop_event_actions( - Parent, Debug, #{state_entry_events := SEEvents} = S, Events, - State, NewData, P0, Event, NextState, [], - Postpone, Hibernate, Timeout, NextEvents) -> + end. + +loop_event_enter( + Parent, Debug, S, Events, + State, NewData, P, Event, NextState, + Hibernate, Timeout, Postpone, NextEvents) -> + case call_state_function(S, enter, State, NextState, NewData) of + {ok,Result,NewS} -> + {NewerData,_,Actions} = + parse_event_result( + Parent, Debug, NewS, Events, + NextState, NewData, P, Event, + Result, false), + loop_event_enter_actions( + Parent, Debug, NewS, Events, + State, NewerData, P, Event, NextState, + Hibernate, Timeout, Postpone, NextEvents, Actions); + {Class,Reason,Stacktrace} -> + terminate( + Class, Reason, Stacktrace, + Debug, S, [Event|Events], NextState, NewData, P) + end. + +loop_event_enter_actions( + Parent, Debug, S, Events, + State, NewData, P, Event, NextState, + Hibernate, Timeout, Postpone, NextEvents, Actions) -> + case + parse_enter_actions(Debug, S, NextState, Actions, Hibernate, Timeout) + of + {ok,NewDebug,NewHibernate,NewTimeout,_,_} -> + loop_event_result( + Parent, NewDebug, S, Events, + State, NewData, P, Event, NextState, + NewHibernate, NewTimeout, Postpone, NextEvents); + {Class,Reason,Stacktrace} -> + terminate( + Class, Reason, Stacktrace, + Debug, S, [Event|Events], NextState, NewData, P) + end. + +loop_event_result( + Parent, Debug, S, Events, + State, NewData, P0, Event, NextState, + Hibernate, Timeout, Postpone, NextEvents) -> %% %% All options have been collected and next_events are buffered. %% Do the actual state transition. @@ -1252,44 +1316,21 @@ loop_event_actions( {lists:reverse(P1, Events),[]} end, %% Place next events first in queue - 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, + Q = lists:reverse(NextEvents, Q2), %% NewDebug = sys_debug( Debug, S, State, case Postpone of true -> - {postpone,Event,NextState}; + {postpone,Event,State}; false -> - {consume,Event,NextState} + {consume,Event,State} end), loop_events( 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, + maps:remove(init_state, S), Q, NextState, NewData, P, Hibernate, Timeout). %%--------------------------------------------------------------------------- @@ -1369,7 +1410,7 @@ error_info( Class, Reason, Stacktrace, #{name := Name, callback_mode := CallbackMode, - state_entry_events := SEEvents}, + state_enter := StateEnter}, Q, P, FmtData) -> {FixedReason,FixedStacktrace} = case Stacktrace of @@ -1397,9 +1438,9 @@ error_info( _ -> {Reason,Stacktrace} end, CBMode = - case SEEvents of + case StateEnter of true -> - [CallbackMode,state_entry_events]; + [CallbackMode,state_enter]; false -> CallbackMode end, @@ -1471,3 +1512,8 @@ format_status_default(Opt, State, Data) -> _ -> [{data,[{"State",StateData}]}] end. + +listify(Item) when is_list(Item) -> + Item; +listify(Item) -> + [Item]. diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index eef8f265c4..48f93b1de7 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, enter_events, event_order, code_change, + shutdown, stop_and_reply, state_enter, event_order, code_change, {group, sys}, hibernate, enter_loop]. @@ -582,7 +582,7 @@ stop_and_reply(_Config) -> -enter_events(_Config) -> +state_enter(_Config) -> process_flag(trap_exit, true), Self = self(), @@ -615,7 +615,7 @@ enter_events(_Config) -> end}, {ok,STM} = gen_statem:start_link( - ?MODULE, {map_statem,Machine,[state_entry_events]}, []), + ?MODULE, {map_statem,Machine,[state_enter]}, []), [{enter,start,start,1}] = flush(), {echo,start,2} = gen_statem:call(STM, echo), -- cgit v1.2.3 From 800265f49f912dcf66846b13aa8032bf2f380caf Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 30 Sep 2016 11:17:22 +0200 Subject: Improve docs and types --- lib/stdlib/doc/src/gen_statem.xml | 77 +++++++++++++++++++++++++++++++-------- lib/stdlib/src/gen_statem.erl | 38 +++++++++++++------ 2 files changed, 88 insertions(+), 27 deletions(-) (limited to 'lib/stdlib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index aa34f53d29..bba2de5e77 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -674,6 +674,9 @@ handle_event(_, _, State, Data) -> they are merged with the current That is: hibernate and timeout overrides the current and reply sends a reply. + This has the same effect as if you would have appended + the actions from this state enter call to the actions + returned by the state function that changed states.

@@ -1002,28 +1005,42 @@ handle_event(_, _, State, Data) ->
- + - stop + keep_state

- Terminates the gen_statem by calling - Module:terminate/3 - with Reason and - NewData, if specified. + The gen_statem keeps the current state, or + does a state transition to the current state if you like, + sets NewData, + and executes all Actions. + This is the same as + {next_state,CurrentState,NewData,Actions}.

- stop_and_reply + keep_state_and_data

- Sends all Replies, - then terminates the gen_statem by calling - Module:terminate/3 - with Reason and - NewData, if specified. + The gen_statem keeps the current state or + does a state transition to the current state if you like, + keeps the current server data, + and executes all Actions. + This is the same as + {next_state,CurrentState,CurrentData,Actions}.

+
+

+ All these terms are tuples or atoms and this property + will hold in any future version of gen_statem. +

+
+
+ + + + keep_state

@@ -1053,6 +1070,36 @@ handle_event(_, _, State, Data) ->

+ + + + + stop + +

+ Terminates the gen_statem by calling + Module:terminate/3 + with Reason and + NewData, if specified. +

+
+ stop_and_reply + +

+ Sends all Replies, + then terminates the gen_statem by calling + Module:terminate/3 + with Reason and + NewData, if specified. +

+
+
+

+ All these terms are tuples or atoms and this property + will hold in any future version of gen_statem. +

+
+
@@ -1462,7 +1509,7 @@ handle_event(_, _, State, Data) -> CallbackMode = callback_mode() | [ callback_mode() - | state_entry_events ] + | state_enter() ] @@ -1490,9 +1537,9 @@ handle_event(_, _, State, Data) ->

The CallbackMode is either just - callback_mode() + callback_mode() or a list containing - callback_mode() + callback_mode() and possibly the atom state_enter.

diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index aedcfc932f..9f5573af86 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -142,7 +142,7 @@ NextStateName :: state_name(), NewData :: data(), Actions :: [action()] | action()} | - common_state_callback_result(). + keep_state_callback_result(). -type state_function_enter_result() :: {'next_state', % {next_state,NextStateName,NewData,[]} NextStateName :: state_name(), @@ -151,7 +151,7 @@ NextStateName :: state_name(), NewData :: data(), Actions :: [enter_action()] | enter_action()} | - common_state_callback_result(). + keep_state_callback_enter_result(). -type handle_event_result() :: {'next_state', % {next_state,NextState,NewData,[]} @@ -161,7 +161,7 @@ NextState :: state(), NewData :: data(), Actions :: [action()] | action()} | - common_state_callback_result(). + keep_state_callback_result(). -type handle_event_enter_result() :: {'next_state', % {next_state,NextState,NewData,[]} NextState :: state(), @@ -170,6 +170,28 @@ NextState :: state(), NewData :: data(), Actions :: [enter_action()] | enter_action()} | + keep_state_callback_enter_result(). + +-type keep_state_callback_result() :: + {'keep_state', % {keep_state,NewData,[]} + NewData :: data()} | + {'keep_state', % Keep state, change data + NewData :: data(), + Actions :: [action()] | action()} | + 'keep_state_and_data' | % {keep_state_and_data,[]} + {'keep_state_and_data', % Keep state and data -> only actions + Actions :: [action()] | action()} | + common_state_callback_result(). + +-type keep_state_callback_enter_result() :: + {'keep_state', % {keep_state,NewData,[]} + NewData :: data()} | + {'keep_state', % Keep state, change data + NewData :: data(), + Actions :: [enter_action()] | enter_action()} | + 'keep_state_and_data' | % {keep_state_and_data,[]} + {'keep_state_and_data', % Keep state and data -> only actions + Actions :: [enter_action()] | enter_action()} | common_state_callback_result(). -type common_state_callback_result() :: @@ -185,15 +207,7 @@ {'stop_and_reply', % Reply then stop the server Reason :: term(), Replies :: [reply_action()] | reply_action(), - NewData :: data()} | - {'keep_state', % {keep_state,NewData,[]} - NewData :: data()} | - {'keep_state', % Keep state, change data - NewData :: data(), - Actions :: [ActionType] | ActionType} | - 'keep_state_and_data' | % {keep_state_and_data,[]} - {'keep_state_and_data', % Keep state and data -> only actions - Actions :: [ActionType] | ActionType}. + NewData :: data()}. %% The state machine init function. It is called only once and -- cgit v1.2.3 From 77e175589b0ee3c1a4c94aef3cdcdf54cd84c53c Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 30 Sep 2016 18:00:38 +0200 Subject: Implement state timeouts --- lib/stdlib/doc/src/gen_statem.xml | 105 +++++--- lib/stdlib/src/gen_statem.erl | 464 ++++++++++++++++++++++------------- lib/stdlib/test/gen_statem_SUITE.erl | 80 +++++- 3 files changed, 440 insertions(+), 209 deletions(-) (limited to 'lib/stdlib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index bba2de5e77..c0631c8448 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -527,7 +527,8 @@ handle_event(_, _, State, Data) -> Type info originates from regular process messages sent to the gen_statem. Also, the state machine implementation can generate events of types - timeout, enter and internal to itself. + timeout, state_timeout, enter, + and internal to itself.

@@ -657,8 +658,7 @@ handle_event(_, _, State, Data) -> All events stored with action() next_event - are inserted in the queue to be processed before - other events. + are inserted to be processed before the other queued events.

@@ -668,35 +668,36 @@ handle_event(_, _, State, Data) -> are used, the gen_statem calls the new state function with arguments (enter, OldState, Data). - If this call returns any + Any actions - that sets transition options - they are merged with the current - That is: hibernate and timeout overrides - the current and reply sends a reply. - This has the same effect as if you would have appended - the actions from this state enter call to the actions + returned from this call are handled as if they were + appended to the actions returned by the state function that changed states.

- If an - event_timeout() - is set through - action() - timeout, - an event timer is started if the value is less than - infinity or a time-out zero event - is enqueued if the value is zero. + If there are enqueued events the (possibly new) + state function + is called with the oldest enqueued event, + and we start again from the top of this list.

- The (possibly new) + Timeout timers + state_timeout() + and + event_timeout() + are handled. This may lead to a time-out zero event + being generated to the state function - is called with the oldest enqueued event if there is any, - otherwise the gen_statem goes into receive + and we start again from the top of this list. +

+
+ +

+ Otherwise the gen_statem goes into receive or hibernation (if hibernate() @@ -704,8 +705,11 @@ handle_event(_, _, State, Data) -> to wait for the next message. In hibernation the next non-system event awakens the gen_statem, or rather the next incoming message awakens the gen_statem, - but if it is a system event - it goes right back into hibernation. + but if it is a system event it goes right back into hibernation. + When a new message arrives the + state function + is called with the corresponding event, + and we start again from the top of this list.

@@ -747,20 +751,20 @@ handle_event(_, _, State, Data) -> event_type() timeout after this time (in milliseconds) unless another - event arrives in which case this time-out is cancelled. - Notice that a retried or inserted event - counts like a new in this respect. + event arrives or has arrived + in which case this time-out is cancelled. + Note that a retried, inserted or state time-out zero + events counts as arrived.

If the value is infinity, no timer is started, as - it never triggers anyway. + it never would trigger anyway.

- If the value is 0, the time-out event is immediately enqueued - unless there already are enqueued events, as the - time-out is then immediately cancelled. - This is a feature ensuring that a time-out 0 event - is processed before any not yet received external event. + If the value is 0 no timer is actually started, + instead the the time-out event is enqueued to ensure + that it gets processed before any not yet + received external event.

Note that it is not possible or needed to cancel this time-out, @@ -768,6 +772,34 @@ handle_event(_, _, State, Data) ->

+ + + +

+ Generates an event of + event_type() + state_timeout + after this time (in milliseconds) unless the gen_statem + changes states (NewState =/= OldState) + which case this time-out is cancelled. +

+

+ If the value is infinity, no timer is started, as + it never would trigger anyway. +

+

+ If the value is 0 no timer is actually started, + instead the the time-out event is enqueued to ensure + that it gets processed before any not yet + received external event. +

+

+ Setting this timer while it is running will restart it with + the new time-out value. Therefore it is possible to cancel + this timeout by setting it to infinity. +

+
+
@@ -886,6 +918,15 @@ handle_event(_, _, State, Data) -> to Time with EventContent.

+ state_timeout + +

+ Sets the + transition_option() + state_timeout() + to Time with EventContent. +

+
diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 9f5573af86..bc33be22a2 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -75,7 +75,7 @@ -type event_type() :: {'call',From :: from()} | 'cast' | - 'info' | 'timeout' | 'internal'. + 'info' | 'timeout' | 'state_timeout' | 'internal'. -type callback_mode_result() :: callback_mode() | [callback_mode() | state_enter()]. @@ -95,6 +95,10 @@ %% Generate a ('timeout', EventContent, ...) event after Time %% unless some other event is delivered Time :: timeout(). +-type state_timeout() :: + %% Generate a ('state_timeout', EventContent, ...) event after Time + %% unless the state is changed + Time :: timeout(). -type action() :: %% During a state change: @@ -126,8 +130,10 @@ {'hibernate', Hibernate :: hibernate()} | %% (Timeout :: event_timeout()) | % {timeout,Timeout} - {'timeout', % Set the event timeout option + {'timeout', % Set the event_timeout option Time :: event_timeout(), EventContent :: term()} | + {'state_timeout', % Set the state_timeout option + Time :: state_timeout(), EventContent :: term()} | %% reply_action(). -type reply_action() :: @@ -593,7 +599,8 @@ enter(Module, Opts, State, Data, Server, Actions, Parent) -> %% The values should already have been type checked Name = gen:get_proc_name(Server), Debug = gen:debug_options(Name, Opts), - P = Events = [], + Events = [], + P = [], Event = {internal,init_state}, %% We enforce {postpone,false} to ensure that %% our fake Event gets discarded, thought it might get logged @@ -609,9 +616,12 @@ enter(Module, Opts, State, Data, Server, Actions, Parent) -> state_enter => false, module => Module, name => Name, + state => State, + data => Data, + postponed => P, %% The rest of the fields are set from to the arguments to - %% loop_event_actions/10 when it finally loops back to loop/3 - %% in loop_events_done/8 + %% loop_event_actions/9 when it finally loops back to loop/3 + %% in loop_events_done/9 %% %% Marker for initial state, cleared immediately when used init_state => true @@ -619,13 +629,14 @@ enter(Module, Opts, State, Data, Server, Actions, Parent) -> NewDebug = sys_debug(Debug, S, State, {enter,Event,State}), case call_callback_mode(S) of {ok,NewS} -> + StateTimer = undefined, loop_event_actions( - Parent, NewDebug, NewS, Events, - State, Data, P, Event, State, NewActions); + Parent, NewDebug, NewS, StateTimer, + Events, Event, State, Data, NewActions); {Class,Reason,Stacktrace} -> terminate( Class, Reason, Stacktrace, - NewDebug, S, [Event|Events], State, Data, P) + NewDebug, S, [Event|Events]) end. %%%========================================================================== @@ -647,7 +658,9 @@ init_it(Starter, Parent, ServerRef, Module, Args, Opts) -> proc_lib:init_ack(Starter, {error,Reason}), error_info( Class, Reason, Stacktrace, - #{name => Name, callback_mode => undefined}, + #{name => Name, + callback_mode => undefined, + state_enter => false}, [], [], undefined), erlang:raise(Class, Reason, Stacktrace) end. @@ -678,7 +691,9 @@ init_result(Starter, Parent, ServerRef, Module, Result, Opts) -> proc_lib:init_ack(Starter, {error,Error}), error_info( error, Error, ?STACKTRACE(), - #{name => Name, callback_mode => undefined}, + #{name => Name, + callback_mode => undefined, + state_enter => false}, [], [], undefined), exit(Error) end. @@ -689,12 +704,10 @@ init_result(Starter, Parent, ServerRef, Module, Result, Opts) -> system_continue(Parent, Debug, S) -> loop(Parent, Debug, S). -system_terminate( - Reason, _Parent, Debug, - #{state := State, data := Data, postponed := P} = S) -> +system_terminate(Reason, _Parent, Debug, S) -> terminate( exit, Reason, ?STACKTRACE(), - Debug, S, [], State, Data, P). + Debug, S, []). system_code_change( #{module := Module, @@ -731,7 +744,7 @@ system_replace_state( format_status( Opt, [PDict,SysState,Parent,Debug, - #{name := Name, postponed := P, state := State, data := Data} = S]) -> + #{name := Name, postponed := P} = S]) -> Header = gen:format_status_header("Status for state machine", Name), Log = sys:get_debug(log, Debug, []), [{header,Header}, @@ -740,7 +753,7 @@ format_status( {"Parent",Parent}, {"Logged Events",Log}, {"Postponed",P}]} | - case format_status(Opt, PDict, S, State, Data) of + case format_status(Opt, PDict, S) of L when is_list(L) -> L; T -> [T] end]. @@ -816,7 +829,8 @@ loop(Parent, Debug, #{hibernate := Hibernate} = S) -> end. %% Entry point for wakeup_from_hibernate/3 -loop_receive(Parent, Debug, #{timer := Timer} = S) -> +loop_receive( + Parent, Debug, #{timer := Timer, state_timer := StateTimer} = S) -> receive Msg -> case Msg of @@ -827,34 +841,23 @@ loop_receive(Parent, Debug, #{timer := Timer} = S) -> sys:handle_system_msg( Req, Pid, Parent, ?MODULE, Debug, S, Hibernate); {'EXIT',Parent,Reason} = EXIT -> - #{state := State, data := Data, postponed := P} = S, %% EXIT is not a 2-tuple and therefore %% not an event and has no event_type(), %% but this will stand out in the crash report... terminate( - exit, Reason, ?STACKTRACE(), - Debug, S, [EXIT], State, Data, P); - {timeout,Timer,Content} when Timer =/= undefined -> + exit, Reason, ?STACKTRACE(), Debug, S, [EXIT]); + {timeout,Timer,Content} + when Timer =/= undefined -> loop_receive_result( - Parent, Debug, S, {timeout,Content}); + Parent, Debug, S, StateTimer, + {timeout,Content}); + {timeout,StateTimer,Content} + when StateTimer =/= undefined -> + loop_receive_result( + Parent, Debug, S, undefined, + {state_timeout,Content}); _ -> - %% Cancel Timer if running - case Timer of - undefined -> - ok; - _ -> - case erlang:cancel_timer(Timer) of - TimeLeft when is_integer(TimeLeft) -> - ok; - false -> - receive - {timeout,Timer,_} -> - ok - after 0 -> - ok - end - end - end, + cancel_timer(Timer), Event = case Msg of {'$gen_call',From,Request} -> @@ -864,71 +867,93 @@ loop_receive(Parent, Debug, #{timer := Timer} = S) -> _ -> {info,Msg} end, - loop_receive_result(Parent, Debug, S, Event) + loop_receive_result( + Parent, Debug, S, StateTimer, Event) end end. -loop_receive_result( - Parent, Debug, - #{state := State, - data := Data, - postponed := P} = S, - Event) -> - %% The engine state map S is now dismantled - %% and will not be restored until we return to loop/3. - %% - %% The fields 'callback_mode', 'module', and 'name' are still valid. - %% The fields 'state', 'data', and 'postponed' are held in arguments. - %% The fields 'timer' and 'hibernate' will be recalculated. +loop_receive_result(Parent, Debug, #{state := State} = S, StateTimer, Event) -> + %% The fields 'timer', 'state_timer' and 'hibernate' + %% are now invalid in state map S - they will be recalculated + %% and restored when we return to loop/3 %% NewDebug = sys_debug(Debug, S, State, {in,Event}), %% Here the queue of not yet handled events is created Events = [], Hibernate = false, - loop_event( - Parent, NewDebug, S, Events, State, Data, P, Event, Hibernate). + loop_event(Parent, NewDebug, S, StateTimer, Events, Event, Hibernate). %% Process the event queue, or if it is empty %% loop back to loop/3 to receive a new event loop_events( - Parent, Debug, S, [Event|Events], - State, Data, P, Hibernate, _Timeout) -> + Parent, Debug, S, StateTimeout, + [Event|Events], _Timeout, State, Data, P, Hibernate) -> %% - %% If there was a state timer requested we just ignore that + %% If there was an event timer requested we just ignore that %% since we have events to handle which cancels the timer loop_event( - Parent, Debug, S, Events, State, Data, P, Event, Hibernate); + Parent, Debug, S, StateTimeout, + Events, Event, State, Data, P, Hibernate); +loop_events( + Parent, Debug, S, {state_timeout,Time,EventContent}, + [] = Events, Timeout, State, Data, P, Hibernate) -> + if + Time =:= 0 -> + %% Simulate an immediate timeout + %% so we do not get the timeout message + %% after any received event + %% + %% This faked event will cancel + %& any not yet started event timer + Event = {state_timeout,EventContent}, + StateTimer = undefined, + loop_event( + Parent, Debug, S, StateTimer, + Events, Event, State, Data, P, Hibernate); + true -> + StateTimer = erlang:start_timer(Time, self(), EventContent), + loop_events( + Parent, Debug, S, StateTimer, + Events, Timeout, State, Data, P, Hibernate) + end; loop_events( - Parent, Debug, S, [], - State, Data, P, Hibernate, Timeout) -> + Parent, Debug, S, StateTimer, + [] = Events, Timeout, State, Data, P, Hibernate) -> case Timeout of {timeout,0,EventContent} -> - %% Immediate timeout - simulate it + %% Simulate an immediate timeout %% so we do not get the timeout message %% after any received event + %% + Event = {timeout,EventContent}, loop_event( - Parent, Debug, S, [], - State, Data, P, {timeout,EventContent}, Hibernate); + Parent, Debug, S, StateTimer, + Events, Event, State, Data, P, Hibernate); {timeout,Time,EventContent} -> - %% Actually start a timer Timer = erlang:start_timer(Time, self(), EventContent), loop_events_done( - Parent, Debug, S, Timer, State, Data, P, Hibernate); + Parent, Debug, S, StateTimer, + State, Data, P, Hibernate, Timer); undefined -> - %% No state timeout has been requested + %% No event timeout has been requested Timer = undefined, loop_events_done( - Parent, Debug, S, Timer, State, Data, P, Hibernate) + Parent, Debug, S, StateTimer, + State, Data, P, Hibernate, Timer) end. -%% -loop_events_done(Parent, Debug, S, Timer, State, Data, P, Hibernate) -> + +%% Back to the top +loop_events_done( + Parent, Debug, S, StateTimer, + State, Data, P, Hibernate, Timer) -> NewS = S#{ - state => State, - data => Data, - postponed => P, + state := State, + data := Data, + postponed := P, hibernate => Hibernate, - timer => Timer}, + timer => Timer, + state_timer => StateTimer}, loop(Parent, Debug, NewS). @@ -936,10 +961,10 @@ loop_events_done(Parent, Debug, S, Timer, State, Data, P, Hibernate) -> call_callback_mode(#{module := Module} = S) -> try Module:callback_mode() of CallbackMode -> - call_callback_mode_result(S, CallbackMode) + callback_mode_result(S, CallbackMode) catch CallbackMode -> - call_callback_mode_result(S, CallbackMode); + callback_mode_result(S, CallbackMode); error:undef -> %% Process undef to check for the simple mistake %% of calling a nonexistent state function @@ -957,7 +982,7 @@ call_callback_mode(#{module := Module} = S) -> {Class,Reason,erlang:get_stacktrace()} end. -call_callback_mode_result(S, CallbackMode) -> +callback_mode_result(S, CallbackMode) -> case parse_callback_mode( if @@ -1060,15 +1085,26 @@ call_state_function( {Class,Reason,erlang:get_stacktrace()} end. +%% Update S and continue loop_event( - Parent, Debug, S, Events, - State, Data, P, {Type,Content} = Event, Hibernate) -> + Parent, Debug, S, StateTimer, + Events, Event, State, Data, P, Hibernate) -> + NewS = + S#{ + state := State, + data := Data, + postponed := P}, + loop_event(Parent, Debug, NewS, StateTimer, Events, Event, Hibernate). + +loop_event( + Parent, Debug, #{state := State, data := Data} = S, StateTimer, + Events, {Type,Content} = Event, Hibernate) -> %% %% If Hibernate is true here it can only be %% because it was set from an event action %% and we did not go into hibernation since there %% were events in queue, so we do what the user - %% might depend on i.e collect garbage which + %% might rely on i.e collect garbage which %% would have happened if we actually hibernated %% and immediately was awakened Hibernate andalso garbage_collect(), @@ -1076,45 +1112,40 @@ loop_event( {ok,Result,NewS} -> {NewData,NextState,Actions} = parse_event_result( - Parent, Debug, NewS, Events, - State, Data, P, Event, - Result, true), + true, Debug, NewS, Result, + Events, Event, State, Data), loop_event_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, Actions); + Parent, Debug, S, StateTimer, + Events, Event, NextState, NewData, Actions); {Class,Reason,Stacktrace} -> terminate( - Class, Reason, Stacktrace, - Debug, S, [Event|Events], State, Data, P) + Class, Reason, Stacktrace, Debug, S, [Event|Events]) end. %% Interpret all callback return variants parse_event_result( - _Parent, Debug, S, Events, State, Data, P, Event, - Result, AllowStateChange) -> + AllowStateChange, Debug, S, Result, Events, Event, State, Data) -> case Result of stop -> terminate( - exit, normal, ?STACKTRACE(), - Debug, S, [Event|Events], State, Data, P); + exit, normal, ?STACKTRACE(), Debug, S, [Event|Events]); {stop,Reason} -> terminate( - exit, Reason, ?STACKTRACE(), - Debug, S, [Event|Events], State, Data, P); + exit, Reason, ?STACKTRACE(), Debug, S, [Event|Events]); {stop,Reason,NewData} -> terminate( exit, Reason, ?STACKTRACE(), - Debug, S, [Event|Events], State, NewData, P); + Debug, S#{data := NewData}, [Event|Events]); {stop_and_reply,Reason,Replies} -> Q = [Event|Events], reply_then_terminate( exit, Reason, ?STACKTRACE(), - Debug, S, Q, State, Data, P, Replies); + Debug, S, Q, Replies); {stop_and_reply,Reason,Replies,NewData} -> Q = [Event|Events], reply_then_terminate( exit, Reason, ?STACKTRACE(), - Debug, S, Q, State, NewData, P, Replies); + Debug, S#{data := NewData}, Q, Replies); {next_state,State,NewData} -> {NewData,State,[]}; {next_state,NextState,NewData} when AllowStateChange -> @@ -1136,31 +1167,35 @@ parse_event_result( error, {bad_return_from_state_function,Result}, ?STACKTRACE(), - Debug, S, [Event|Events], State, Data, P) + Debug, S, [Event|Events]) end. -parse_enter_actions(Debug, S, State, Actions, Hibernate, Timeout) -> +parse_enter_actions( + Debug, S, State, Actions, + Hibernate, Timeout, StateTimeout) -> Postpone = forbidden, NextEvents = forbidden, parse_actions( Debug, S, State, listify(Actions), - Hibernate, Timeout, Postpone, NextEvents). + Hibernate, Timeout, StateTimeout, Postpone, NextEvents). parse_actions(Debug, S, State, Actions) -> - Postpone = false, Hibernate = false, Timeout = undefined, + StateTimeout = undefined, + Postpone = false, NextEvents = [], parse_actions( Debug, S, State, listify(Actions), - Hibernate, Timeout, Postpone, NextEvents). + Hibernate, Timeout, StateTimeout, Postpone, NextEvents). %% parse_actions( - Debug, _S, _State, [], Hibernate, Timeout, Postpone, NextEvents) -> - {ok,Debug,Hibernate,Timeout,Postpone,NextEvents}; + Debug, _S, _State, [], + Hibernate, Timeout, StateTimeout, Postpone, NextEvents) -> + {ok,Debug,Hibernate,Timeout,StateTimeout,Postpone,NextEvents}; parse_actions( Debug, S, State, [Action|Actions], - Hibernate, Timeout, Postpone, NextEvents) -> + Hibernate, Timeout, StateTimeout, Postpone, NextEvents) -> case Action of %% Actual actions {reply,From,Reply} -> @@ -1169,7 +1204,8 @@ parse_actions( NewDebug = do_reply(Debug, S, State, From, Reply), parse_actions( NewDebug, S, State, Actions, - Hibernate, Timeout, Postpone, NextEvents); + Hibernate, Timeout, StateTimeout, + Postpone, NextEvents); false -> {error, {bad_action_from_state_function,Action}, @@ -1179,7 +1215,7 @@ parse_actions( {hibernate,NewHibernate} when is_boolean(NewHibernate) -> parse_actions( Debug, S, State, Actions, - NewHibernate, Timeout, Postpone, NextEvents); + NewHibernate, Timeout, StateTimeout, Postpone, NextEvents); {hibernate,_} -> {error, {bad_action_from_state_function,Action}, @@ -1187,15 +1223,25 @@ parse_actions( hibernate -> parse_actions( Debug, S, State, Actions, - true, Timeout, Postpone, NextEvents); + true, Timeout, StateTimeout, Postpone, NextEvents); + {state_timeout,Time,_} = NewStateTimeout + when is_integer(Time), Time >= 0; + Time =:= infinity -> + parse_actions( + Debug, S, State, Actions, + Hibernate, Timeout, NewStateTimeout, Postpone, NextEvents); + {state_timeout,_,_} -> + {error, + {bad_action_from_state_function,Action}, + ?STACKTRACE()}; {timeout,infinity,_} -> % Clear timer - it will never trigger parse_actions( Debug, S, State, Actions, - Hibernate, undefined, Postpone, NextEvents); + Hibernate, undefined, StateTimeout, Postpone, NextEvents); {timeout,Time,_} = NewTimeout when is_integer(Time), Time >= 0 -> parse_actions( Debug, S, State, Actions, - Hibernate, NewTimeout, Postpone, NextEvents); + Hibernate, NewTimeout, StateTimeout, Postpone, NextEvents); {timeout,_,_} -> {error, {bad_action_from_state_function,Action}, @@ -1203,17 +1249,17 @@ parse_actions( infinity -> % Clear timer - it will never trigger parse_actions( Debug, S, State, Actions, - Hibernate, undefined, Postpone, NextEvents); + Hibernate, undefined, StateTimeout, Postpone, NextEvents); Time when is_integer(Time), Time >= 0 -> NewTimeout = {timeout,Time,Time}, parse_actions( Debug, S, State, Actions, - Hibernate, NewTimeout, Postpone, NextEvents); + Hibernate, NewTimeout, StateTimeout, Postpone, NextEvents); {postpone,NewPostpone} when is_boolean(NewPostpone), Postpone =/= forbidden -> parse_actions( Debug, S, State, Actions, - Hibernate, Timeout, NewPostpone, NextEvents); + Hibernate, Timeout, StateTimeout, NewPostpone, NextEvents); {postpone,_} -> {error, {bad_action_from_state_function,Action}, @@ -1221,7 +1267,7 @@ parse_actions( postpone when Postpone =/= forbidden -> parse_actions( Debug, S, State, Actions, - Hibernate, Timeout, true, NextEvents); + Hibernate, Timeout, StateTimeout, true, NextEvents); {next_event,Type,Content} -> case event_type(Type) of true when NextEvents =/= forbidden -> @@ -1229,8 +1275,8 @@ parse_actions( sys_debug(Debug, S, State, {in,{Type,Content}}), parse_actions( NewDebug, S, State, Actions, - Hibernate, Timeout, Postpone, - [{Type,Content}|NextEvents]); + Hibernate, Timeout, StateTimeout, + Postpone, [{Type,Content}|NextEvents]); _ -> {error, {bad_action_from_state_function,Action}, @@ -1243,94 +1289,143 @@ parse_actions( end. loop_event_actions( - Parent, Debug, #{state_enter := StateEnter} = S, Events, - State, NewData, P, Event, NextState, Actions) -> + Parent, Debug, + #{state := State, state_enter := StateEnter} = S, StateTimer, + Events, Event, NextState, NewData, Actions) -> case parse_actions(Debug, S, State, Actions) of - {ok,NewDebug,Hibernate,Timeout,Postpone,NextEvents} -> - case - StateEnter andalso - ((NextState =/= State) - orelse maps:is_key(init_state, S)) of - true -> + {ok,NewDebug,Hibernate,Timeout,StateTimeout,Postpone,NextEvents} -> + if + StateEnter, NextState =/= State -> loop_event_enter( - Parent, NewDebug, S, Events, - State, NewData, P, Event, NextState, - Hibernate, Timeout, Postpone, NextEvents); - false -> + Parent, NewDebug, S, StateTimer, + Events, Event, NextState, NewData, + Hibernate, Timeout, StateTimeout, Postpone, NextEvents); + StateEnter -> + case maps:is_key(init_state, S) of + true -> + %% Avoid infinite loop in initial state + %% with state entry events + NewS = maps:remove(init_state, S), + loop_event_enter( + Parent, NewDebug, NewS, StateTimer, + Events, Event, NextState, NewData, + Hibernate, Timeout, StateTimeout, + Postpone, NextEvents); + false -> + loop_event_result( + Parent, NewDebug, S, StateTimer, + Events, Event, NextState, NewData, + Hibernate, Timeout, StateTimeout, + Postpone, NextEvents) + end; + true -> loop_event_result( - Parent, NewDebug, S, Events, - State, NewData, P, Event, NextState, - Hibernate, Timeout, Postpone, NextEvents) + Parent, NewDebug, S, StateTimer, + Events, Event, NextState, NewData, + Hibernate, Timeout, StateTimeout, Postpone, NextEvents) end; {Class,Reason,Stacktrace} -> terminate( Class, Reason, Stacktrace, - Debug, S, [Event|Events], State, NewData, P) + Debug, S#{data := NewData}, [Event|Events]) end. loop_event_enter( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, - Hibernate, Timeout, Postpone, NextEvents) -> + Parent, Debug, #{state := State} = S, StateTimer, + Events, Event, NextState, NewData, + Hibernate, Timeout, StateTimeout, Postpone, NextEvents) -> case call_state_function(S, enter, State, NextState, NewData) of {ok,Result,NewS} -> {NewerData,_,Actions} = parse_event_result( - Parent, Debug, NewS, Events, - NextState, NewData, P, Event, - Result, false), + false, Debug, NewS, Result, + Events, Event, NextState, NewData), loop_event_enter_actions( - Parent, Debug, NewS, Events, - State, NewerData, P, Event, NextState, - Hibernate, Timeout, Postpone, NextEvents, Actions); + Parent, Debug, NewS, StateTimer, + Events, Event, NextState, NewerData, + Hibernate, Timeout, StateTimeout, Postpone, NextEvents, Actions); {Class,Reason,Stacktrace} -> terminate( Class, Reason, Stacktrace, - Debug, S, [Event|Events], NextState, NewData, P) + Debug, S#{state := NextState, data := NewData}, + [Event|Events]) end. loop_event_enter_actions( - Parent, Debug, S, Events, - State, NewData, P, Event, NextState, - Hibernate, Timeout, Postpone, NextEvents, Actions) -> + Parent, Debug, S, StateTimer, + Events, Event, NextState, NewData, + Hibernate, Timeout, StateTimeout, Postpone, NextEvents, Actions) -> case - parse_enter_actions(Debug, S, NextState, Actions, Hibernate, Timeout) + parse_enter_actions( + Debug, S, NextState, Actions, + Hibernate, Timeout, StateTimeout) of - {ok,NewDebug,NewHibernate,NewTimeout,_,_} -> + {ok,NewDebug,NewHibernate,NewTimeout,NewStateTimeout,_,_} -> loop_event_result( - Parent, NewDebug, S, Events, - State, NewData, P, Event, NextState, - NewHibernate, NewTimeout, Postpone, NextEvents); + Parent, NewDebug, S, StateTimer, + Events, Event, NextState, NewData, + NewHibernate, NewTimeout, NewStateTimeout, Postpone, NextEvents); {Class,Reason,Stacktrace} -> terminate( Class, Reason, Stacktrace, - Debug, S, [Event|Events], NextState, NewData, P) + Debug, S#{state := NextState, data := NewData}, + [Event|Events]) end. loop_event_result( - Parent, Debug, S, Events, - State, NewData, P0, Event, NextState, - Hibernate, Timeout, Postpone, NextEvents) -> + Parent, Debug, + #{state := State, postponed := P_0} = S, StateTimer, + Events, Event, NextState, NewData, + Hibernate, Timeout, StateTimeout, Postpone, NextEvents) -> %% %% All options have been collected and next_events are buffered. %% Do the actual state transition. %% - P1 = % Move current event to postponed if Postpone + NewStateTimeout = + case StateTimeout of + {state_timeout,Time,_} -> + %% New timeout -> cancel timer + case StateTimer of + {state_timeout,_,_} -> + ok; + _ -> + cancel_timer(StateTimer) + end, + case Time of + infinity -> + undefined; + _ -> + StateTimeout + end; + undefined when NextState =/= State -> + %% State change -> cancel timer + case StateTimer of + {state_timeout,_,_} -> + ok; + _ -> + cancel_timer(StateTimer) + end, + undefined; + undefined -> + StateTimer + end, + %% + P_1 = % Move current event to postponed if Postpone case Postpone of true -> - [Event|P0]; + [Event|P_0]; false -> - P0 + P_0 end, - {Q2,P} = % Move all postponed events to queue if state change + {Events_1,NewP} = % Move all postponed events to queue if state change if NextState =:= State -> - {Events,P1}; + {Events,P_1}; true -> - {lists:reverse(P1, Events),[]} + {lists:reverse(P_1, Events),[]} end, %% Place next events first in queue - Q = lists:reverse(NextEvents, Q2), + NewEvents = lists:reverse(NextEvents, Events_1), %% NewDebug = sys_debug( @@ -1341,46 +1436,44 @@ loop_event_result( false -> {consume,Event,State} end), + %% loop_events( - Parent, NewDebug, - %% Avoid infinite loop in initial state with state entry events - maps:remove(init_state, S), - Q, NextState, NewData, P, Hibernate, Timeout). + Parent, NewDebug, S, NewStateTimeout, + NewEvents, Timeout, NextState, NewData, NewP, Hibernate). %%--------------------------------------------------------------------------- %% Server helpers reply_then_terminate( Class, Reason, Stacktrace, - Debug, S, Q, State, Data, P, Replies) -> + Debug, #{state := State} = S, Q, Replies) -> if is_list(Replies) -> do_reply_then_terminate( Class, Reason, Stacktrace, - Debug, S, Q, State, Data, P, Replies); + Debug, S, Q, Replies, State); true -> do_reply_then_terminate( Class, Reason, Stacktrace, - Debug, S, Q, State, Data, P, [Replies]) + Debug, S, Q, [Replies], State) end. %% do_reply_then_terminate( - Class, Reason, Stacktrace, Debug, S, Q, State, Data, P, []) -> - terminate(Class, Reason, Stacktrace, Debug, S, Q, State, Data, P); + Class, Reason, Stacktrace, Debug, S, Q, [], _State) -> + terminate(Class, Reason, Stacktrace, Debug, S, Q); do_reply_then_terminate( - Class, Reason, Stacktrace, Debug, S, Q, State, Data, P, [R|Rs]) -> + Class, Reason, Stacktrace, Debug, S, Q, [R|Rs], State) -> case R of {reply,{_To,_Tag}=From,Reply} -> NewDebug = do_reply(Debug, S, State, From, Reply), do_reply_then_terminate( - Class, Reason, Stacktrace, - NewDebug, S, Q, State, Data, P, Rs); + Class, Reason, Stacktrace, NewDebug, S, Q, Rs, State); _ -> terminate( error, {bad_reply_action_from_state_function,R}, ?STACKTRACE(), - Debug, S, Q, State, Data, P) + Debug, S, Q) end. do_reply(Debug, S, State, From, Reply) -> @@ -1390,7 +1483,9 @@ do_reply(Debug, S, State, From, Reply) -> terminate( Class, Reason, Stacktrace, - Debug, #{module := Module} = S, Q, State, Data, P) -> + Debug, + #{module := Module, state := State, data := Data, postponed := P} = S, + Q) -> try Module:terminate(Reason, State, Data) of _ -> ok catch @@ -1399,7 +1494,7 @@ terminate( ST = erlang:get_stacktrace(), error_info( C, R, ST, S, Q, P, - format_status(terminate, get(), S, State, Data)), + format_status(terminate, get(), S)), sys:print_log(Debug), erlang:raise(C, R, ST) end, @@ -1410,7 +1505,7 @@ terminate( _ -> error_info( Class, Reason, Stacktrace, S, Q, P, - format_status(terminate, get(), S, State, Data)), + format_status(terminate, get(), S)), sys:print_log(Debug) end, case Stacktrace of @@ -1502,7 +1597,9 @@ error_info( %% Call Module:format_status/2 or return a default value -format_status(Opt, PDict, #{module := Module}, State, Data) -> +format_status( + Opt, PDict, + #{module := Module, state := State, data := Data}) -> case erlang:function_exported(Module, format_status, 2) of true -> try Module:format_status(Opt, [PDict,State,Data]) @@ -1531,3 +1628,18 @@ listify(Item) when is_list(Item) -> Item; listify(Item) -> [Item]. + +cancel_timer(undefined) -> + ok; +cancel_timer(TRef) -> + case erlang:cancel_timer(TRef) of + TimeLeft when is_integer(TimeLeft) -> + ok; + false -> + receive + {timeout,TRef,_} -> + ok + after 0 -> + ok + end + end. diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 48f93b1de7..28f9ab81fe 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -37,7 +37,8 @@ all() -> {group, stop_handle_event}, {group, abnormal}, {group, abnormal_handle_event}, - shutdown, stop_and_reply, state_enter, event_order, code_change, + shutdown, stop_and_reply, state_enter, event_order, + state_timeout, code_change, {group, sys}, hibernate, enter_loop]. @@ -709,6 +710,83 @@ event_order(_Config) -> +state_timeout(_Config) -> + process_flag(trap_exit, true), + + Machine = + #{init => + fun () -> + {ok,start,0} + end, + start => + fun + ({call,From}, {go,Time}, 0) -> + self() ! message_to_self, + {next_state, state1, {Time,From}, + %% Verify that internal events goes before external + [{state_timeout,Time,1}, + {next_event,internal,1}]} + end, + state1 => + fun + (internal, 1, Data) -> + %% Verify that a state change cancels timeout 1 + {next_state, state2, Data, + [{timeout,0,2}, + {state_timeout,0,2}, + {next_event,internal,2}]} + end, + state2 => + fun + (internal, 2, Data) -> + %% Verify that {state_timeout,0,_} + %% comes after next_event and that + %% {timeout,0,_} is cancelled by + %% {state_timeout,0,_} + {keep_state, {ok,2,Data}, + [{timeout,0,3}]}; + (state_timeout, 2, {ok,2,{Time,From}}) -> + {next_state, state3, 3, + [{reply,From,ok}, + {state_timeout,Time,3}]} + end, + state3 => + fun + (info, message_to_self, 3) -> + {keep_state, '3'}; + ({call,From}, check, '3') -> + {keep_state, From}; + (state_timeout, 3, From) -> + {stop_and_reply, normal, + {reply,From,ok}} + end}, + + {ok,STM} = gen_statem:start_link(?MODULE, {map_statem,Machine,[]}, []), + TRef = erlang:start_timer(1000, self(), kull), + ok = gen_statem:call(STM, {go,500}), + ok = gen_statem:call(STM, check), + receive + {timeout,TRef,kull} -> + ct:fail(late_timeout) + after 0 -> + receive + {timeout,TRef,kull} -> + ok + after 1000 -> + ct:fail(no_check_timeout) + end + end, + receive + {'EXIT',STM,normal} -> + ok + after 500 -> + ct:fail(did_not_stop) + end, + + verify_empty_msgq(). + + + sys1(Config) -> {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), {status, Pid, {module,gen_statem}, _} = sys:get_status(Pid), -- cgit v1.2.3 From f4de3f5887be010db178a178e1f20027f3e5d22b Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Wed, 12 Oct 2016 17:49:26 +0200 Subject: Use parameterized types --- lib/stdlib/doc/src/gen_statem.xml | 216 ++++++++++++++------------------------ lib/stdlib/src/gen_statem.erl | 94 ++++++----------- 2 files changed, 112 insertions(+), 198 deletions(-) (limited to 'lib/stdlib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index c0631c8448..64267c2af5 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -127,9 +127,9 @@ erlang:'!' -----> Module:StateName/3 is not regarded as an error but as a valid return from all callback functions.

- +

- The "state function" for a specific + The "state callback" for a specific state in a gen_statem is the callback function that is called for all events in this state. It is selected depending on which @@ -141,7 +141,7 @@ erlang:'!' -----> Module:StateName/3 When the callback mode is state_functions, the state must be an atom and - is used as the state function name; see + is used as the state callback name; see Module:StateName/3. This gathers all code for a specific state in one function as the gen_statem engine @@ -154,7 +154,7 @@ erlang:'!' -----> Module:StateName/3 When the callback mode is handle_event_function, the state can be any term - and the state function name is + and the state callback name is Module:handle_event/4. This makes it easy to branch depending on state or event as you desire. Be careful about which events you handle in which @@ -164,8 +164,8 @@ erlang:'!' -----> Module:StateName/3

The gen_statem enqueues incoming events in order of arrival and presents these to the - state function - in that order. The state function can postpone an event + state callback + in that order. The state callback can postpone an event so it is not retried in the current state. After a state change the queue restarts with the postponed events.

@@ -177,12 +177,12 @@ erlang:'!' -----> Module:StateName/3 to entering a new receive statement.

- The state function + The state callback can insert events using the action() next_event and such an event is inserted as the next to present - to the state function. That is, as if it is + to the state callback. That is, as if it is the oldest incoming event. A dedicated event_type() internal can be used for such events making them impossible @@ -198,7 +198,7 @@ erlang:'!' -----> Module:StateName/3

The gen_statem engine can automatically make a specialized call to the - state function + state callback whenever a new state is entered; see state_enter(). This is for writing code common to all state entries. @@ -207,7 +207,7 @@ erlang:'!' -----> Module:StateName/3

If you in gen_statem, for example, postpone - an event in one state and then call another state function + an event in one state and then call another state callback of yours, you have not changed states and hence the postponed event is not retried, which is logical but can be confusing.

@@ -236,7 +236,7 @@ erlang:'!' -----> Module:StateName/3 The gen_statem process can go into hibernation; see proc_lib:hibernate/3. It is done when a - state function or + state callback or Module:init/1 specifies hibernate in the returned Actions @@ -294,7 +294,7 @@ init([]) -> {ok,State,Data}. callback_mode() -> state_functions. -%%% State function(s) +%%% state callback(s) off({call,From}, push, Data) -> %% Go to 'on', increment count and reply @@ -348,7 +348,7 @@ ok callback_mode() -> handle_event_function. -%%% State function(s) +%%% state callback(s) handle_event({call,From}, push, off, Data) -> %% Go to 'on', increment count and reply @@ -482,6 +482,10 @@ handle_event(_, _, State, Data) ->

+ If the + callback mode + is handle_event_function, + the state can be any term. After a state change (NextState =/= State), all postponed events are retried.

@@ -495,6 +499,8 @@ handle_event(_, _, State, Data) -> callback mode is state_functions, the state must be of this type. + After a state change (NextState =/= State), + all postponed events are retried.

@@ -592,11 +598,11 @@ handle_event(_, _, State, Data) -> returns a list containing state_enter, the gen_statem engine will, at every state change, call the - state function + state callback with arguments (enter, OldState, Data). This may look like an event but is really a call - performed after the previous state function returned - and before any event is delivered to the new state function. + performed after the previous state callback returned + and before any event is delivered to the new state callback. See Module:StateName/3 and @@ -666,19 +672,19 @@ handle_event(_, _, State, Data) -> If the state changes or is the initial state, and state enter calls are used, the gen_statem calls - the new state function with arguments + the new state callback with arguments (enter, OldState, Data). Any actions returned from this call are handled as if they were appended to the actions - returned by the state function that changed states. + returned by the state callback that changed states.

If there are enqueued events the (possibly new) - state function + state callback is called with the oldest enqueued event, and we start again from the top of this list.

@@ -691,7 +697,7 @@ handle_event(_, _, State, Data) -> event_timeout() are handled. This may lead to a time-out zero event being generated to the - state function + state callback and we start again from the top of this list.

@@ -707,7 +713,7 @@ handle_event(_, _, State, Data) -> the next incoming message awakens the gen_statem, but if it is a system event it goes right back into hibernation. When a new message arrives the - state function + state callback is called with the corresponding event, and we start again from the top of this list.

@@ -806,7 +812,7 @@ handle_event(_, _, State, Data) ->

These state transition actions can be invoked by returning them from the - state function + state callback when it is called with an event, from @@ -870,7 +876,7 @@ handle_event(_, _, State, Data) ->

These state transition actions can be invoked by returning them from the - state function, from + state callback, from Module:init/1 or by giving them to enter_loop/5,6. @@ -903,7 +909,7 @@ handle_event(_, _, State, Data) -> Short for {timeout,Timeout,Timeout}, that is, the time-out message is the time-out time. This form exists to make the - state function + state callback return value {next_state,NextState,NewData,Timeout} allowed like for gen_fsm's Module:StateName/2. @@ -936,7 +942,7 @@ handle_event(_, _, State, Data) ->

This state transition action can be invoked by returning it from the - state function, from + state callback, from Module:init/1 or by giving it to enter_loop/5,6. @@ -947,7 +953,7 @@ handle_event(_, _, State, Data) -> From must be the term from argument {call,From} in a call to a - state function. + state callback.

Note that using this action from @@ -956,77 +962,48 @@ handle_event(_, _, State, Data) -> enter_loop/5,6 would be weird on the border of whichcraft since there has been no earlier call to a - state function + state callback in this server.

- + - - next_state - -

- The gen_statem does a state transition to - NextStateName - (which can be the same as the current state), - sets NewData, - and executes all Actions. -

-
-

- All these terms are tuples or atoms and this property - will hold in any future version of gen_statem. + State is the current state + and it can not be changed since the state callback + was called with a + state enter call.

-
-
- - - next_state

The gen_statem does a state transition to - NextStateName - (which can be the same as the current state), + State, which has to be + the current state, sets NewData, and executes all Actions.

-

- All these terms are tuples or atoms and this property - will hold in any future version of gen_statem. -

- + - - next_state - -

- The gen_statem does a state transition to - NextState - (which can be the same as the current state), - sets NewData, - and executes all Actions. -

-
-

- All these terms are tuples or atoms and this property - will hold in any future version of gen_statem. + StateType is + state_name() + if + callback mode + is state_functions, or + state() + if + callback mode + is handle_event_function.

-
-
- - - next_state @@ -1039,48 +1016,20 @@ handle_event(_, _, State, Data) ->

-

- All these terms are tuples or atoms and this property - will hold in any future version of gen_statem. -

- + - - keep_state - -

- The gen_statem keeps the current state, or - does a state transition to the current state if you like, - sets NewData, - and executes all Actions. - This is the same as - {next_state,CurrentState,NewData,Actions}. -

-
- keep_state_and_data - -

- The gen_statem keeps the current state or - does a state transition to the current state if you like, - keeps the current server data, - and executes all Actions. - This is the same as - {next_state,CurrentState,CurrentData,Actions}. -

-
-

- All these terms are tuples or atoms and this property - will hold in any future version of gen_statem. + ActionType is + enter_action() + if the state callback was called with a + state enter call + and + action() + if the state callback was called with an event.

-
-
- - - keep_state @@ -1104,17 +1053,6 @@ handle_event(_, _, State, Data) -> {next_state,CurrentState,CurrentData,Actions}.

-
-

- All these terms are tuples or atoms and this property - will hold in any future version of gen_statem. -

-
-
- - - - stop

@@ -1155,14 +1093,14 @@ handle_event(_, _, State, Data) -> by sending a request and waiting until its reply arrives. The gen_statem calls the - state function with + state callback with event_type() {call,From} and event content Request.

A Reply is generated when a - state function + state callback returns with {reply,From,Reply} as one action(), @@ -1227,7 +1165,7 @@ handle_event(_, _, State, Data) -> ignoring if the destination node or gen_statem does not exist. The gen_statem calls the - state function with + state callback with event_type() cast and event content Msg. @@ -1341,18 +1279,18 @@ handle_event(_, _, State, Data) -> call/2 when the reply cannot be defined in the return value of a - state function. + state callback.

From must be the term from argument {call,From} to the - state function. + state callback. A reply or multiple replies canalso be sent using one or several reply_action()s from a - state function. + state callback.

@@ -1562,7 +1500,7 @@ handle_event(_, _, State, Data) -> for efficiency reasons, so this function is only called once after server start and after code change, but before the first - state function + state callback in the current code version is called. More occasions may be added in future versions of gen_statem. @@ -1707,7 +1645,7 @@ handle_event(_, _, State, Data) -> The Actions are executed when entering the first state just as for a - state function. + state callback.

If the initialization fails, @@ -1829,13 +1767,13 @@ handle_event(_, _, State, Data) -> Module:StateName(enter, OldState, Data) -> - StateFunctionEnterResult + StateEnterResult(StateName) Module:StateName(EventType, EventContent, Data) -> StateFunctionResult Module:handle_event(enter, OldState, State, Data) -> - HandleEventResult + StateEnterResult Module:handle_event(EventType, EventContent, State, Data) -> HandleEventResult @@ -1856,20 +1794,20 @@ handle_event(_, _, State, Data) -> data() - StateFunctionResult = - state_function_result() + StateEnterResult(StateName) = + state_enter_result(StateName) - StateFunctionEnterResult = - state_function_enter_result() + StateFunctionResult = + event_handler_result(state_name()) - HandleEventResult = - handle_event_result() + StateEnterResult = + state_enter_result(state()) - HandleEventEnterResult = - handle_event_enter_result() + HandleEventResult = + event_handler_result(state()) @@ -1888,7 +1826,7 @@ handle_event(_, _, State, Data) -> {call,From}, the caller waits for a reply. The reply can be sent from this or from any other - state function + state callback by returning with {reply,From,Reply} in Actions, in Replies, diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index bc33be22a2..5c750cb93d 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -44,18 +44,20 @@ -export( [wakeup_from_hibernate/3]). -%% Type exports for templates +%% Type exports for templates and callback modules -export_type( [event_type/0, - state_name/0, + init_result/0, callback_mode_result/0, state_function_result/0, - state_function_enter_result/0, handle_event_result/0, - handle_event_enter_result/0, + state_enter_result/1, + event_handler_result/1, + reply_action/0, + enter_action/0, action/0]). -%% Fix problem for doc build +%% Type that is exported just to be documented -export_type([transition_option/0]). %%%========================================================================== @@ -66,7 +68,7 @@ {To :: pid(), Tag :: term()}. % Reply-to specifier for call -type state() :: - state_name() | % For StateName/3 callback functios + state_name() | % For StateName/3 callback functions term(). % For handle_event/4 callback function -type state_name() :: atom(). @@ -140,67 +142,45 @@ {'reply', % Reply to a caller From :: from(), Reply :: term()}. --type state_function_result() :: - {'next_state', % {next_state,NextStateName,NewData,[]} - NextStateName :: state_name(), - NewData :: data()} | - {'next_state', % State transition, maybe to the same state - NextStateName :: state_name(), - NewData :: data(), - Actions :: [action()] | action()} | - keep_state_callback_result(). --type state_function_enter_result() :: - {'next_state', % {next_state,NextStateName,NewData,[]} - NextStateName :: state_name(), - NewData :: data()} | - {'next_state', % State transition, maybe to the same state - NextStateName :: state_name(), - NewData :: data(), - Actions :: [enter_action()] | enter_action()} | - keep_state_callback_enter_result(). +-type init_result() :: + {ok, state(), data()} | + {ok, state(), data(), [action()] | action()} | + 'ignore' | + {'stop', Reason :: term()}. +%% Old, not advertised +-type state_function_result() :: + event_handler_result(state_name()). -type handle_event_result() :: + event_handler_result(state()). +%% +-type state_enter_result(StateType) :: {'next_state', % {next_state,NextState,NewData,[]} - NextState :: state(), + State :: StateType, NewData :: data()} | {'next_state', % State transition, maybe to the same state - NextState :: state(), + State :: StateType, NewData :: data(), - Actions :: [action()] | action()} | - keep_state_callback_result(). --type handle_event_enter_result() :: + Actions :: [enter_action()] | enter_action()} | + state_callback_result(enter_action()). +-type event_handler_result(StateType) :: {'next_state', % {next_state,NextState,NewData,[]} - NextState :: state(), + NextState :: StateType, NewData :: data()} | {'next_state', % State transition, maybe to the same state - NextState :: state(), + NextState :: StateType, NewData :: data(), - Actions :: [enter_action()] | enter_action()} | - keep_state_callback_enter_result(). - --type keep_state_callback_result() :: - {'keep_state', % {keep_state,NewData,[]} - NewData :: data()} | - {'keep_state', % Keep state, change data - NewData :: data(), - Actions :: [action()] | action()} | - 'keep_state_and_data' | % {keep_state_and_data,[]} - {'keep_state_and_data', % Keep state and data -> only actions Actions :: [action()] | action()} | - common_state_callback_result(). - --type keep_state_callback_enter_result() :: + state_callback_result(action()). +-type state_callback_result(ActionType) :: {'keep_state', % {keep_state,NewData,[]} NewData :: data()} | {'keep_state', % Keep state, change data NewData :: data(), - Actions :: [enter_action()] | enter_action()} | + Actions :: [ActionType] | ActionType} | 'keep_state_and_data' | % {keep_state_and_data,[]} {'keep_state_and_data', % Keep state and data -> only actions - Actions :: [enter_action()] | enter_action()} | - common_state_callback_result(). - --type common_state_callback_result() :: + Actions :: [ActionType] | ActionType} | 'stop' | % {stop,normal} {'stop', % Stop the server Reason :: term()} | @@ -220,11 +200,7 @@ %% the server is not running until this function has returned %% an {ok, ...} tuple. Thereafter the state callbacks are called %% for all events to this server. --callback init(Args :: term()) -> - {ok, state(), data()} | - {ok, state(), data(), [action()] | action()} | - 'ignore' | - {'stop', Reason :: term()}. +-callback init(Args :: term()) -> init_result(). %% This callback shall return the callback mode of the callback module. %% @@ -244,11 +220,11 @@ 'enter', OldStateName :: state_name(), Data :: data()) -> - state_function_enter_result(); + state_enter_result('state_name'); (event_type(), EventContent :: term(), Data :: data()) -> - state_function_result(). + event_handler_result(state_name()). %% %% State callback for all states %% when callback_mode() =:= handle_event_function. @@ -257,12 +233,12 @@ OldState :: state(), State :: state(), % Current state Data :: data()) -> - handle_event_enter_result(); + state_enter_result(state()); (event_type(), EventContent :: term(), State :: state(), % Current state Data :: data()) -> - handle_event_result(). + event_handler_result(state()). %% Clean up before the server terminates. -callback terminate( -- cgit v1.2.3 From 5e2d802e29f0a8f81de297f9a3e3922f2d6cd6c0 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 14 Oct 2016 10:08:49 +0200 Subject: Fix race condition in cancel_timer/1 --- lib/stdlib/src/gen_statem.erl | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'lib/stdlib') diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 5c750cb93d..17d1ebecec 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -1609,13 +1609,14 @@ cancel_timer(undefined) -> ok; cancel_timer(TRef) -> case erlang:cancel_timer(TRef) of - TimeLeft when is_integer(TimeLeft) -> - ok; false -> + %% We have to assume that TRef is the ref of a running timer + %% and if so the timer has expired + %% hence we must wait for the timeout message receive {timeout,TRef,_} -> ok - after 0 -> - ok - end + end; + _TimeLeft -> + ok end. -- cgit v1.2.3