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(-) 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 +++++++++- system/doc/design_principles/statem.xml | 152 +++++++++++++++++------ 4 files changed, 400 insertions(+), 108 deletions(-) diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 17f1526a21..a4c5438a08 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -70,6 +70,7 @@ 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}; diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 57e47431b8..8090016b54 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -109,7 +109,7 @@ State(S) x Event(E) -> Actions(A), State(S')

 StateName(EventType, EventContent, Data) ->
-    .. code for actions here ...
+    ... code for actions here ...
     {next_state, NewStateName, NewData}.
@@ -120,7 +120,7 @@ StateName(EventType, EventContent, Data) ->

 handle_event(EventType, EventContent, State, Data) ->
-    .. code for actions here ...
+    ... code for actions here ...
     {next_state, NewState, NewData}
@@ -192,13 +192,42 @@ handle_event(EventType, EventContent, State, Data) -> + + +
+ + State Entry Events +

+ The gen_statem behavior can regardless of callback mode + automatically generate an + + event whenever the state changes + + so you can write state entry code + near the rest of the state transition rules. + It typically looks like this: +

+
+StateName(enter, _OldState, Data) ->
+    ... code for state entry here ...
+    {keep_state, NewData};
+StateName(EventType, EventContent, Data) ->
+    ... code for actions here ...
+    {next_state, NewStateName, NewData}.
+

+ Depending on how your state machine is specified, + this can be a very useful feature, but if you use it + you will have to handle the state entry events in all states. +

+
+
Example

This example starts off as equivalent to the example in section - gen_fsm Behavior. + gen_fsm-Behavior. In later sections, additions and tweaks are made using features in gen_statem that gen_fsm does not have. The end of this chapter provides the example again @@ -722,6 +751,12 @@ stop() -> Generated by any regular process message sent to the gen_statem process. + enter + + Generated by a state transition with + OldState =/= NewState when running with + state entry events. + timeout Generated by state transition action @@ -981,7 +1016,7 @@ init(Code) -> callback_mode() -> state_functions. -locked(internal, enter, _Data) -> +locked(internal, enter, Data) -> do_lock(), {keep_state,Data#{remaining => Code}}; locked( @@ -992,7 +1027,7 @@ locked( enter(next_state, open, Data); ... -open(internal, enter, _Data) -> +open(internal, enter, Data) -> Tref = erlang:start_timer(10000, self(), lock), do_unlock(), {keep_state,Data#{timer => Tref}}; @@ -1005,6 +1040,49 @@ enter(Tag, State, Data) -> ]]>

+ + +
+ Using State Entry Events +

+ Here is the same example as the previous but instead using + the built in + state entry events. + You will have to handle the state entry events in every state. + If you want state entry code in just a few states the previous + example may be more suitable. +

+ + process_flag(trap_exit, true), + Data = #{code => Code}, + {ok, locked, Data}. + +callback_mode() -> + [state_functions,state_entry_events]. + +locked(enter, _OldState, Data) -> + do_lock(), + {keep_state,Data#{remaining => Code}}; +locked( + cast, {button,Digit}, + #{code := Code, remaining := Remaining} = Data) -> + case Remaining of + [Digit] -> + {next_state, open, Data}; +... + +open(enter, _OldState, Data) -> + Tref = erlang:start_timer(10000, self(), lock), + do_unlock(), + {keep_state,Data#{timer => Tref}}; +open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> + {next_state, locked, Data}; +... + ]]> +
+
@@ -1054,12 +1132,12 @@ code_length() -> init(Code) -> process_flag(trap_exit, true), Data = #{code => Code}, - enter(ok, locked, Data). + {ok, locked, Data}. callback_mode() -> - state_functions. + [state_functions,state_entry_events]. -locked(internal, enter, #{code := Code} = Data) -> +locked(enter, _OldState, #{code := Code} = Data) -> do_lock(), {keep_state,Data#{remaining => Code}}; locked( @@ -1067,7 +1145,7 @@ locked( #{code := Code, remaining := Remaining} = Data) -> case Remaining of [Digit] -> % Complete - enter(next_state, open, Data); + {next_state, open, Data}; [Digit|Rest] -> % Incomplete {keep_state,Data#{remaining := Rest}}; [_|_] -> % Wrong @@ -1076,12 +1154,12 @@ locked( locked(EventType, EventContent, Data) -> handle_event(EventType, EventContent, Data). -open(internal, enter, Data) -> +open(enter, _OldState, Data) -> Tref = erlang:start_timer(10000, self(), lock), do_unlock(), {keep_state,Data#{timer => Tref}}; open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> - enter(next_state, locked, Data); + {next_state, locked, Data}; open(cast, {button,_}, _) -> {keep_state_and_data,[postpone]}; open(EventType, EventContent, Data) -> @@ -1089,8 +1167,6 @@ open(EventType, EventContent, Data) -> handle_event({call,From}, code_length, #{code := Code}) -> {keep_state_and_data,[{reply,From,length(Code)}]}. -enter(Tag, State, Data) -> - {Tag,State,Data,[{next_event,internal,enter}]}. do_lock() -> io:format("Locked~n", []). @@ -1111,8 +1187,9 @@ code_change(_Vsn, State, Data, _Extra) -> This section describes what to change in the example to use one handle_event/4 function. The previously used approach to first branch depending on event - does not work that well here because of the generated - entry actions, so this example first branches depending on state: + does not work that well here because of + the state entry events, + so this example first branches depending on state:

... callback_mode() -> - handle_event_function. + [handle_event_function,state_entry_events]. %% State: locked -handle_event(internal, enter, locked, #{code := Code} = Data) -> +handle_event(enter, _OldState, locked, #{code := Code} = Data) -> do_lock(), {keep_state,Data#{remaining => Code}}; handle_event( @@ -1131,7 +1208,7 @@ handle_event( #{code := Code, remaining := Remaining} = Data) -> case Remaining of [Digit] -> % Complete - enter(next_state, open, Data); + {next_state, open, Data}; [Digit|Rest] -> % Incomplete {keep_state,Data#{remaining := Rest}}; [_|_] -> % Wrong @@ -1139,12 +1216,12 @@ handle_event( end; %% %% State: open -handle_event(internal, enter, open, Data) -> +handle_event(enter, _OldState, open, Data) -> Tref = erlang:start_timer(10000, self(), lock), do_unlock(), {keep_state,Data#{timer => Tref}}; handle_event(info, {timeout,Tref,lock}, open, #{timer := Tref} = Data) -> - enter(next_state, locked, Data); + {next_state, locked, Data}; handle_event(cast, {button,_}, open, _) -> {keep_state_and_data,[postpone]}; %% @@ -1305,10 +1382,10 @@ set_lock_button(LockButton) -> init({Code,LockButton}) -> process_flag(trap_exit, true), Data = #{code => Code, remaining => undefined, timer => undefined}, - enter(ok, {locked,LockButton}, Data, []). + {ok, {locked,LockButton}, Data}. callback_mode() -> - handle_event_function. + [handle_event_function,state_entry_events]. handle_event( {call,From}, {set_lock_button,NewLockButton}, @@ -1320,55 +1397,52 @@ handle_event( {_StateName,_LockButton}, #{code := Code}) -> {keep_state_and_data, [{reply,From,length(Code)}]}; +%% +%% State: locked handle_event( EventType, EventContent, {locked,LockButton}, #{code := Code, remaining := Remaining} = Data) -> case {EventType,EventContent} of - {internal,enter} -> + {enter,_OldState} -> do_lock(), {keep_state,Data#{remaining := Code}}; {{call,From},{button,Digit}} -> case Remaining of [Digit] -> % Complete - next_state( - {open,LockButton}, Data, - [{reply,From,ok}]); + {next_state, {open,LockButton}, Data, + [{reply,From,ok}]}; [Digit|Rest] -> % Incomplete - {keep_state,Data#{remaining := Rest}, + {keep_state, Data#{remaining := Rest}, [{reply,From,ok}]}; [_|_] -> % Wrong - {keep_state,Data#{remaining := Code}, + {keep_state, Data#{remaining := Code}, [{reply,From,ok}]} end end; +%% +%% State: open handle_event( EventType, EventContent, {open,LockButton}, #{timer := Timer} = Data) -> case {EventType,EventContent} of - {internal,enter} -> + {enter,_OldState} -> Tref = erlang:start_timer(10000, self(), lock), do_unlock(), {keep_state,Data#{timer := Tref}}; {info,{timeout,Timer,lock}} -> - next_state({locked,LockButton}, Data, []); + {next_state, {locked,LockButton}, Data}; {{call,From},{button,Digit}} -> if Digit =:= LockButton -> erlang:cancel_timer(Timer), - next_state( - {locked,LockButton}, Data, - [{reply,From,locked}]); + {next_state, {locked,LockButton}, Data, + [{reply,From,locked}]); true -> {keep_state_and_data, [postpone]} end end. -next_state(State, Data, Actions) -> - enter(next_state, State, Data, Actions). -enter(Tag, State, Data, Actions) -> - {Tag,State,Data,[{next_event,internal,enter}|Actions]}. - do_lock() -> io:format("Locked~n", []). do_unlock() -> @@ -1434,7 +1508,7 @@ handle_event( EventType, EventContent, {open,LockButton}, #{timer := Timer} = Data) -> case {EventType,EventContent} of - {internal,enter} -> + {enter,_OldState} -> Tref = erlang:start_timer(10000, self(), lock), do_unlock(), {keep_state,Data#{timer := Tref},[hibernate]}; -- 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 +++++++++++++++------------------ system/doc/design_principles/statem.xml | 22 ++++++++++++++++++++-- 2 files changed, 35 insertions(+), 20 deletions(-) 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 diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 8090016b54..43359829b2 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -989,7 +989,17 @@ do_unlock() -> from your state machine to itself.

- One example of using self-generated events can be when you have + One example for this is to pre-process incoming data, for example + decrypting chunks or collecting characters up to a line break. + This could be modelled with a separate state machine that sends + the pre-processed events to the main state machine, or to decrease + overhead the small pre-processing state machine can be implemented + in the common state event handling of the main state machine + using a few state data variables and then send the pre-processed + events as internal events to the main state machine. +

+

+ Another example of using self-generated events can be when you have a state machine specification that uses state entry actions. You can code that using a dedicated function to do the state transition. But if you want that code to be @@ -1050,7 +1060,15 @@ enter(Tag, State, Data) -> state entry events. You will have to handle the state entry events in every state. If you want state entry code in just a few states the previous - example may be more suitable. + example may be more suitable, especially to only send internal + events when entering just those few states. +

+

+ You can also in the previous example choose to generate + events looking just like the events you get from using + state entry events. + This may be confusing, or practical, + depending on your point of view.

Date: Tue, 20 Sep 2016 17:03:24 +0200 Subject: Improve docs --- system/doc/design_principles/statem.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 43359829b2..565b0e5274 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -1010,6 +1010,8 @@ do_unlock() -> the state, you must explicitly insert the internal event or use a state transition function. + This is something that can be forgotten, and if you find that + annoying please look at the next chapter.

The following is an implementation of entry actions @@ -1058,10 +1060,18 @@ enter(Tag, State, Data) -> Here is the same example as the previous but instead using the built in state entry events. - You will have to handle the state entry events in every state. +

+

+ Since the state entry events are unconditionally inserted by + the gen_statem engine you can not forget to insert them + yourself and you will have to handle the state entry events + in every state. +

+

If you want state entry code in just a few states the previous example may be more suitable, especially to only send internal events when entering just those few states. + Note: additional discipline will be required.

You can also in the previous example choose to generate -- 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 +- lib/tools/emacs/erlang-skels.el | 12 +- system/doc/design_principles/statem.xml | 194 +++++------ 5 files changed, 639 insertions(+), 460 deletions(-) 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), diff --git a/lib/tools/emacs/erlang-skels.el b/lib/tools/emacs/erlang-skels.el index 0284c9d686..95cc989c73 100644 --- a/lib/tools/emacs/erlang-skels.el +++ b/lib/tools/emacs/erlang-skels.el @@ -904,7 +904,7 @@ Please see the function `tempo-define-template'.") "%% @doc" n "%% Define the callback_mode() for this callback module." n (erlang-skel-separator-end 2) - "-spec callback_mode() -> gen_statem:callback_mode()." n + "-spec callback_mode() -> gen_statem:callback_mode_result()." n "callback_mode() -> state_functions." n n (erlang-skel-separator-start 2) @@ -936,6 +936,10 @@ Please see the function `tempo-define-template'.") "%% instead of StateName/3 functions like this!" n (erlang-skel-separator-end 2) "-spec state_name(" n> + "'enter', OldState :: gen_statem:state_name()," n> + "Data :: term()) ->" n> + "gen_statem:state_function_enter_result();" n + " (" n> "gen_statem:event_type(), Msg :: term()," n> "Data :: term()) ->" n> "gen_statem:state_function_result()." n @@ -1015,7 +1019,7 @@ Please see the function `tempo-define-template'.") "%% @doc" n "%% Define the callback_mode() for this callback module." n (erlang-skel-separator-end 2) - "-spec callback_mode() -> gen_statem:callback_mode()." n + "-spec callback_mode() -> gen_statem:callback_mode_result()." n "callback_mode() -> handle_event_function." n n (erlang-skel-separator-start 2) @@ -1044,6 +1048,10 @@ Please see the function `tempo-define-template'.") "%% StateName/3 functions are called instead!" n (erlang-skel-separator-end 2) "-spec handle_event(" n> + "'enter', OldState :: term()," n> + "State :: term(), Data :: term()) ->" n> + "gen_statem:handle_event_enter_result();" n + " (" n> "gen_statem:event_type(), Msg :: term()," n> "State :: term(), Data :: term()) ->" n> "gen_statem:handle_event_result()." n diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 565b0e5274..d2a9b23570 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -195,21 +195,19 @@ handle_event(EventType, EventContent, State, Data) ->

- - State Entry Events + + State Enter Calls

The gen_statem behavior can regardless of callback mode - automatically generate an - - event whenever the state changes - - so you can write state entry code + automatically call the state function + with special arguments whenever the state changes + so you can write state entry actions near the rest of the state transition rules. It typically looks like this:

 StateName(enter, _OldState, Data) ->
-    ... code for state entry here ...
+    ... code for state entry actions here ...
     {keep_state, NewData};
 StateName(EventType, EventContent, Data) ->
     ... code for actions here ...
@@ -217,7 +215,7 @@ StateName(EventType, EventContent, Data) ->
     

Depending on how your state machine is specified, this can be a very useful feature, but if you use it - you will have to handle the state entry events in all states. + you will have to handle the state enter call in all states.

@@ -751,12 +749,6 @@ stop() -> Generated by any regular process message sent to the gen_statem process. - enter - - Generated by a state transition with - OldState =/= NewState when running with - state entry events. - timeout Generated by state transition action @@ -972,63 +964,35 @@ do_unlock() ->
- Self-Generated Events -

- It can sometimes be beneficial to be able to generate events - to your own state machine. - This can be done with the state transition - action - {next_event,EventType,EventContent}. -

+ State Entry Actions

- You can generate events of any existing - type, - but the internal type can only be generated through action - next_event. Hence, it cannot come from an external source, - so you can be certain that an internal event is an event - from your state machine to itself. + Say you have a state machine specification + that uses state entry actions. + Allthough you can code this using self-generated events + (described in the next section), especially if just + one or a few states has got state entry actions, + this is a perfect use case for the built in + state enter calls.

- One example for this is to pre-process incoming data, for example - decrypting chunks or collecting characters up to a line break. - This could be modelled with a separate state machine that sends - the pre-processed events to the main state machine, or to decrease - overhead the small pre-processing state machine can be implemented - in the common state event handling of the main state machine - using a few state data variables and then send the pre-processed - events as internal events to the main state machine. -

-

- Another example of using self-generated events can be when you have - a state machine specification that uses state entry actions. - You can code that using a dedicated function - to do the state transition. But if you want that code to be - visible besides the other state logic, you can insert - an internal event that does the entry actions. - This has the same unfortunate consequence as using - state transition functions: everywhere you go to - the state, you must explicitly - insert the internal event - or use a state transition function. - This is something that can be forgotten, and if you find that - annoying please look at the next chapter. -

-

- The following is an implementation of entry actions - using internal events with content enter - using a helper function enter/3 for state entry: + You return a list containing state_enter from your + callback_mode/0 + function and the gen_statem engine will call your + state function once with the arguments + (enter, OldState, ...) whenever the state changes. + Then you just need to handle these event-like calls in all states.

process_flag(trap_exit, true), Data = #{code => Code}, - enter(ok, locked, Data). + {ok, locked, Data}. callback_mode() -> - state_functions. + [state_functions,state_enter]. -locked(internal, enter, Data) -> +locked(enter, _OldState, Data) -> do_lock(), {keep_state,Data#{remaining => Code}}; locked( @@ -1036,79 +1000,94 @@ locked( #{code := Code, remaining := Remaining} = Data) -> case Remaining of [Digit] -> - enter(next_state, open, Data); + {next_state, open, Data}; ... -open(internal, enter, Data) -> +open(enter, _OldState, Data) -> Tref = erlang:start_timer(10000, self(), lock), do_unlock(), {keep_state,Data#{timer => Tref}}; open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> - enter(next_state, locked, Data); + {next_state, locked, Data}; ... - -enter(Tag, State, Data) -> - {Tag,State,Data,[{next_event,internal,enter}]}. ]]>
- Using State Entry Events + Self-Generated Events

- Here is the same example as the previous but instead using - the built in - state entry events. + It can sometimes be beneficial to be able to generate events + to your own state machine. + This can be done with the state transition + action + {next_event,EventType,EventContent}.

- Since the state entry events are unconditionally inserted by - the gen_statem engine you can not forget to insert them - yourself and you will have to handle the state entry events - in every state. + You can generate events of any existing + type, + but the internal type can only be generated through action + next_event. Hence, it cannot come from an external source, + so you can be certain that an internal event is an event + from your state machine to itself.

- If you want state entry code in just a few states the previous - example may be more suitable, especially to only send internal - events when entering just those few states. - Note: additional discipline will be required. + One example for this is to pre-process incoming data, for example + decrypting chunks or collecting characters up to a line break. + Purists may argue that this should be modelled with a separate + state machine that sends pre-processed events + to the main state machine. + But to decrease overhead the small pre-processing state machine + can be implemented in the common state event handling + of the main state machine using a few state data variables + that then sends the pre-processed events as internal events + to the main state machine.

- You can also in the previous example choose to generate - events looking just like the events you get from using - state entry events. - This may be confusing, or practical, - depending on your point of view. + The following example use an input model where you give the lock + characters with put_chars(Chars) and then call + enter() to finish the input.

- process_flag(trap_exit, true), - Data = #{code => Code}, - {ok, locked, Data}. +-export(put_chars/1, enter/0). +... +put_chars(Chars) when is_binary(Chars) -> + gen_statem:call(?NAME, {chars,Chars}). -callback_mode() -> - [state_functions,state_entry_events]. +enter() -> + gen_statem:call(?NAME, enter). + +... locked(enter, _OldState, Data) -> do_lock(), - {keep_state,Data#{remaining => Code}}; -locked( - cast, {button,Digit}, - #{code := Code, remaining := Remaining} = Data) -> - case Remaining of - [Digit] -> - {next_state, open, Data}; + {keep_state,Data#{remaining => Code, buf => []}}; ... -open(enter, _OldState, Data) -> - Tref = erlang:start_timer(10000, self(), lock), - do_unlock(), - {keep_state,Data#{timer => Tref}}; -open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> - {next_state, locked, Data}; +handle_event({call,From}, {chars,Chars}, #{buf := Buf} = Data) -> + {keep_state, Data#{buf := [Chars|Buf], + [{reply,From,ok}]}; +handle_event({call,From}, enter, #{buf := Buf} = Data) -> + Chars = unicode:characters_to_binary(lists:reverse(Buf)), + try binary_to_integer(Chars) of + Digit -> + {keep_state, Data#{buf := []}, + [{reply,From,ok}, + {next_event,internal,{button,Chars}}]} + catch + error:badarg -> + {keep_state, Data#{buf := []}, + [{reply,From,{error,not_an_integer}}]} + end; ... ]]> +

+ If you start this program with code_lock:start([17]) + you can unlock with code_lock:put_chars(<<"001">>), + code_lock:put_chars(<<"7">>), code_lock:enter(). +

@@ -1117,7 +1096,7 @@ open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> Example Revisited

This section includes the example after all mentioned modifications - and some more using the entry actions, + and some more using state enter calls, which deserves a new state diagram:

@@ -1163,7 +1142,7 @@ init(Code) -> {ok, locked, Data}. callback_mode() -> - [state_functions,state_entry_events]. + [state_functions,state_enter]. locked(enter, _OldState, #{code := Code} = Data) -> do_lock(), @@ -1215,8 +1194,7 @@ code_change(_Vsn, State, Data, _Extra) -> This section describes what to change in the example to use one handle_event/4 function. The previously used approach to first branch depending on event - does not work that well here because of - the state entry events, + does not work that well here because of the state enter calls, so this example first branches depending on state:

... callback_mode() -> - [handle_event_function,state_entry_events]. + [handle_event_function,state_enter]. %% State: locked handle_event(enter, _OldState, locked, #{code := Code} = Data) -> @@ -1413,7 +1391,7 @@ init({Code,LockButton}) -> {ok, {locked,LockButton}, Data}. callback_mode() -> - [handle_event_function,state_entry_events]. + [handle_event_function,state_enter]. handle_event( {call,From}, {set_lock_button,NewLockButton}, -- 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 +++++++++++----- system/doc/design_principles/statem.xml | 17 ++++++-- 3 files changed, 101 insertions(+), 31 deletions(-) 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 diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index d2a9b23570..69d1e8e9fa 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -29,7 +29,7 @@ statem.xml - +

This section is to be read with the gen_statem(3) @@ -199,7 +199,10 @@ handle_event(EventType, EventContent, State, Data) -> State Enter Calls

The gen_statem behavior can regardless of callback mode - automatically call the state function + automatically + + call the state function + with special arguments whenever the state changes so you can write state entry actions near the rest of the state transition rules. @@ -214,8 +217,13 @@ StateName(EventType, EventContent, Data) -> {next_state, NewStateName, NewData}.

Depending on how your state machine is specified, - this can be a very useful feature, but if you use it - you will have to handle the state enter call in all states. + this can be a very useful feature, + but it forces you to handle the state enter calls in all states. + See also the + + State Entry Actions + + chapter.

@@ -964,6 +972,7 @@ do_unlock() ->
+ State Entry Actions

Say you have a state machine specification -- 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 +++- system/doc/design_principles/code_lock.dia | Bin 2932 -> 2945 bytes system/doc/design_principles/code_lock.png | Bin 59160 -> 59827 bytes system/doc/design_principles/code_lock_2.dia | Bin 2621 -> 2956 bytes system/doc/design_principles/code_lock_2.png | Bin 48927 -> 55553 bytes system/doc/design_principles/statem.xml | 581 +++++++++++++++++---------- 8 files changed, 817 insertions(+), 413 deletions(-) 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), diff --git a/system/doc/design_principles/code_lock.dia b/system/doc/design_principles/code_lock.dia index 8e6ff8a898..eaa2aca5b0 100644 Binary files a/system/doc/design_principles/code_lock.dia and b/system/doc/design_principles/code_lock.dia differ diff --git a/system/doc/design_principles/code_lock.png b/system/doc/design_principles/code_lock.png index 745fd91920..40bd35fc74 100644 Binary files a/system/doc/design_principles/code_lock.png and b/system/doc/design_principles/code_lock.png differ diff --git a/system/doc/design_principles/code_lock_2.dia b/system/doc/design_principles/code_lock_2.dia index 142909a2f5..3b9ba554d8 100644 Binary files a/system/doc/design_principles/code_lock_2.dia and b/system/doc/design_principles/code_lock_2.dia differ diff --git a/system/doc/design_principles/code_lock_2.png b/system/doc/design_principles/code_lock_2.png index ecf7b0d799..3aca9dd5aa 100644 Binary files a/system/doc/design_principles/code_lock_2.png and b/system/doc/design_principles/code_lock_2.png differ diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 69d1e8e9fa..9a50bef7b1 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -29,7 +29,7 @@ statem.xml - +

This section is to be read with the gen_statem(3) @@ -50,6 +50,7 @@

+ Event-Driven State Machines

Established Automata Theory does not deal much with @@ -94,7 +95,7 @@ State(S) x Event(E) -> Actions(A), State(S')

- + Callback Modes

The gen_statem behavior supports two callback modes: @@ -110,7 +111,12 @@ State(S) x Event(E) -> Actions(A), State(S')

 StateName(EventType, EventContent, Data) ->
     ... code for actions here ...
-    {next_state, NewStateName, NewData}.
+ {next_state, NewStateName, NewData}. + +

+ This form is used in most examples here for example in section + Example. +

@@ -121,7 +127,13 @@ StateName(EventType, EventContent, Data) ->

 handle_event(EventType, EventContent, State, Data) ->
     ... code for actions here ...
-    {next_state, NewState, NewData}
+ {next_state, NewState, NewData} + +

+ Se section + One Event Handler + for an example. +

@@ -134,10 +146,11 @@ handle_event(EventType, EventContent, State, Data) ->

+ Choosing the Callback Mode

The two - callback modes + callback modes give different possibilities and restrictions, but one goal remains: you want to handle all possible combinations of @@ -195,7 +208,7 @@ handle_event(EventType, EventContent, State, Data) ->

- + State Enter Calls

The gen_statem behavior can regardless of callback mode @@ -230,10 +243,160 @@ StateName(EventType, EventContent, Data) ->

+ + Actions +

+ In the first section + + Event-Driven State Machines + + actions were mentioned as a part of + the general state machine model. These general actions + are implemented with the code that callback module + gen_statem executes in an event-handling + callback function before returning + to the gen_statem engine. +

+

+ There are more specific state-transition actions + that a callback function can order the gen_statem + engine to do after the callback function return. + These are ordered by returning a list of + actions + in the + return tuple + from the + callback function. + These state transition actions affect the gen_statem + engine itself and can do the following: +

+ + + + Postpone + + the current event, see section + Postponing Events + + + + Hibernate + + the gen_statem, treated in + Hibernation + + + Start a + + state time-out, + read more in section + State Time-Outs + + + Start an + event time-out, + see more in section + Event Time-Outs + + + + Reply + + to a caller, mentioned at the end of section + All State Events + + + Generate the + + next event + + to handle, see section + Self-Generated Events + + +

+ For details, see the + + gen_statem(3) + + manual page. + You can, for example, reply to many callers + and generate multiple next events to handle. +

+
+ + + +
+ + Event Types +

+ Events are categorized in different + event types. + Events of all types are handled in the same callback function, + for a given state, and the function gets + EventType and EventContent as arguments. +

+

+ The following is a complete list of event types and where + they come from: +

+ + cast + + Generated by + gen_statem:cast. + + {call,From} + + Generated by + gen_statem:call, + where From is the reply address to use + when replying either through the state transition action + {reply,From,Msg} or by calling + gen_statem:reply. + + info + + Generated by any regular process message sent to + the gen_statem process. + + state_timeout + + Generated by state transition action + + {state_timeout,Time,EventContent} + + state timer timing out. + + timeout + + Generated by state transition action + + {timeout,Time,EventContent} + + (or its short form Time) + event timer timing out. + + internal + + Generated by state transition + action + {next_event,internal,EventContent}. + All event types above can also be generated using + {next_event,EventType,EventContent}. + + +
+ + + +
+ Example

This example starts off as equivalent to the example in section - gen_fsm-Behavior. + gen_fsm Behavior. In later sections, additions and tweaks are made using features in gen_statem that gen_fsm does not have. The end of this chapter provides the example again @@ -256,7 +419,6 @@ StateName(EventType, EventContent, Data) -> This code lock state machine can be implemented using gen_statem with the following callback module:

- init(Code) -> do_lock(), Data = #{code => Code, remaining => Code}, - {ok,locked,Data}. + {ok, locked, Data}. callback_mode() -> state_functions. @@ -287,19 +449,19 @@ locked( case Remaining of [Digit] -> do_unlock(), - {next_state,open,Data#{remaining := Code},10000}; + {next_state, open, Data#{remaining := Code}, + [{state_timeout,10000,lock}]; [Digit|Rest] -> % Incomplete - {next_state,locked,Data#{remaining := Rest}}; + {next_state, locked, Data#{remaining := Rest}}; _Wrong -> - {next_state,locked,Data#{remaining := Code}} + {next_state, locked, Data#{remaining := Code}} end. -open(timeout, _, Data) -> +open(state_timeout, lock, Data) -> do_lock(), - {next_state,locked,Data}; + {next_state, locked, Data}; open(cast, {button,_}, Data) -> - do_lock(), - {next_state,locked,Data}. + {next_state, open, Data}. do_lock() -> io:format("Lock~n", []). @@ -310,7 +472,7 @@ terminate(_Reason, State, _Data) -> State =/= locked andalso do_lock(), ok. code_change(_Vsn, State, Data, _Extra) -> - {ok,State,Data}. + {ok, State, Data}. ]]>

The code is explained in the next sections.

@@ -318,6 +480,7 @@ code_change(_Vsn, State, Data, _Extra) ->
+ Starting gen_statem

In the example in the previous section, gen_statem is @@ -380,7 +543,7 @@ start_link(Code) ->

If name registration succeeds, the new gen_statem process calls callback function code_lock:init(Code). - This function is expected to return {ok,State,Data}, + This function is expected to return {ok, State, Data}, where State is the initial state of the gen_statem, in this case locked; assuming that the door is locked to begin with. Data is the internal server data of the gen_statem. @@ -421,7 +584,7 @@ callback_mode() -> Function Module:callback_mode/0 selects the - CallbackMode + CallbackMode for the callback module, in this case state_functions. That is, each state has got its own handler function. @@ -432,6 +595,7 @@ callback_mode() ->

+ Handling Events

The function notifying the code lock about a button event is implemented using @@ -451,11 +615,13 @@ button(Digit) -> The event is made into a message and sent to the gen_statem. When the event is received, the gen_statem calls StateName(cast, Event, Data), which is expected to - return a tuple {next_state,NewStateName,NewData}. + return a tuple {next_state, NewStateName, NewData}, + or {next_state, NewStateName, NewData, Actions}. StateName is the name of the current state and NewStateName is the name of the next state to go to. NewData is a new value for the server data of - the gen_statem. + the gen_statem, and Actions is a list of + actions on the gen_statem engine.

% Complete do_unlock(), - {next_state,open,Data#{remaining := Code},10000}; + {next_state, open, Data#{remaining := Code}, + [{state_timeout,10000,lock}]}; [Digit|Rest] -> % Incomplete - {next_state,locked,Data#{remaining := Rest}}; + {next_state, locked, Data#{remaining := Rest}}; [_|_] -> % Wrong - {next_state,locked,Data#{remaining := Code}} + {next_state, locked, Data#{remaining := Code}} end. -open(timeout, _, Data) -> +open(state_timeout, lock, Data) -> do_lock(), - {next_state,locked,Data}; + {next_state, locked, Data}; open(cast, {button,_}, Data) -> - do_lock(), - {next_state,locked,Data}. + {next_state, open, Data}. ]]>

If the door is locked and a button is pressed, the pressed @@ -490,38 +656,55 @@ open(cast, {button,_}, Data) -> restarts from the start of the code sequence.

- In state open, any button locks the door, as - any event cancels the event timer, so no - time-out event occurs after a button event. + If the whole code is correct, the server changes states + to open. +

+

+ In state open, a button event is ignored + by staying in the same state. This can also be done + by returning {keep_state, Data} or in this case + since Data unchanged even by returning + keep_state_and_data.

- Event Time-Outs + + State Time-Outs

When a correct code has been given, the door is unlocked and the following tuple is returned from locked/2:

10,000 is a time-out value in milliseconds. After this time (10 seconds), a time-out occurs. - Then, StateName(timeout, 10000, Data) is called. + Then, StateName(state_timeout, lock, Data) is called. The time-out occurs when the door has been in state open for 10 seconds. After that the door is locked again:

+open(state_timeout, lock, Data) -> do_lock(), - {next_state,locked,Data}; + {next_state, locked, Data}; ]]> +

+ The timer for a state time-out is automatically cancelled + when the state machine changes states. You can restart + a state time-out by setting it to a new time, which cancels + the running timer and starts a new. This implies that + you can cancel a state time-out by restarting it with + time infinity. +

+ All State Events

Sometimes events can arrive in any state of the gen_statem. @@ -554,21 +737,24 @@ open(EventType, EventContent, Data) -> handle_event(EventType, EventContent, Data). handle_event({call,From}, code_length, #{code := Code} = Data) -> - {keep_state,Data,[{reply,From,length(Code)}]}. + {keep_state, Data, [{reply,From,length(Code)}]}. ]]>

This example uses gen_statem:call/2, which waits for a reply from the server. The reply is sent with a {reply,From,Reply} tuple - in an action list in the {keep_state,...} tuple - that retains the current state. + in an action list in the {keep_state, ...} tuple + that retains the current state. This return form is convenient + when you want to stay in the current state but do not know or + care about what it is.

+ One Event Handler

If mode handle_event_function is used, @@ -592,19 +778,19 @@ handle_event(cast, {button,Digit}, State, #{code := Code} = Data) -> case maps:get(remaining, Data) of [Digit] -> % Complete do_unlock(), - {next_state,open,Data#{remaining := Code},10000}; + {next_state, open, Data#{remaining := Code}, + [{state_timeout,10000,lock}}; [Digit|Rest] -> % Incomplete - {keep_state,Data#{remaining := Rest}}; + {keep_state, Data#{remaining := Rest}}; [_|_] -> % Wrong - {keep_state,Data#{remaining := Code}} + {keep_state, Data#{remaining := Code}} end; open -> - do_lock(), - {next_state,locked,Data} + keep_state_and_data end; -handle_event(timeout, _, open, Data) -> +handle_event(state_timeout, lock, open, Data) -> do_lock(), - {next_state,locked,Data}. + {next_state, locked, Data}. ... ]]> @@ -613,9 +799,11 @@ handle_event(timeout, _, open, Data) ->

+ Stopping
+ In a Supervision Tree

If the gen_statem is part of a supervision tree, @@ -655,6 +843,7 @@ terminate(_Reason, State, _Data) ->

+ Standalone gen_statem

If the gen_statem is not part of a supervision tree, @@ -681,127 +870,77 @@ stop() ->

- Actions + + Event Time-Outs

- In the first sections actions were mentioned as a part of - the general state machine model. These general actions - are implemented with the code that callback module - gen_statem executes in an event-handling - callback function before returning - to the gen_statem engine. + A timeout feature inherited from gen_statem's predecessor + gen_fsm, + is an event time-out, that is, + if an event arrives the timer is cancelled. + You get either an event or a time-out, but not both.

- There are more specific state-transition actions - that a callback function can order the gen_statem - engine to do after the callback function return. - These are ordered by returning a list of - actions - in the - return tuple - from the - callback function. - These state transition actions affect the gen_statem - engine itself and can do the following: + It is ordered by the state transition action + {timeout,Time,EventContent}, or just Time, + or even just Time instead of an action list + (the latter is a form inherited from gen_fsm.

- - Postpone the current event - Hibernate the gen_statem - Start an event time-out - Reply to a caller - Generate the next event to handle -

- In the example earlier was mentioned the event time-out - and replying to a caller. - An example of event postponing is included later in this chapter. - For details, see the - gen_statem(3) - manual page. - You can, for example, reply to many callers - and generate multiple next events to handle. + This type of time-out is useful to for example act on inactivity. + Let us start restart the code sequence + if no button is pressed for say 30 seconds:

-
- - + - Event Types +locked( + timeout, _, + #{code := Code, remaining := Remaining} = Data) -> + {next_state, locked, Data#{remaining := Code}}; +locked( + cast, {button,Digit}, + #{code := Code, remaining := Remaining} = Data) -> +... + [Digit|Rest] -> % Incomplete + {next_state, locked, Data#{remaining := Rest}, 30000}; +... + ]]>

- The previous sections mentioned a few - event types. - Events of all types are handled in the same callback function, - for a given state, and the function gets - EventType and EventContent as arguments. + Whenever we receive a button event we start an event timeout + of 30 seconds, and if we get an event type timeout + we reset the remaining code sequence.

- The following is a complete list of event types and where - they come from: + An event timeout is cancelled by any other event so you either + get some other event or the timeout event. It is therefore + not possible nor needed to cancel or restart an event timeout. + Whatever event you act on has already cancelled + the event timeout...

- - cast - - Generated by - gen_statem:cast. - - {call,From} - - Generated by - gen_statem:call, - where From is the reply address to use - when replying either through the state transition action - {reply,From,Msg} or by calling - gen_statem:reply. - - info - - Generated by any regular process message sent to - the gen_statem process. - - timeout - - Generated by state transition action - {timeout,Time,EventContent} (or its short form Time) - timer timing out. - - internal - - Generated by state transition action - {next_event,internal,EventContent}. - All event types above can also be generated using - {next_event,EventType,EventContent}. - -
- State Time-Outs -

- The time-out event generated by state transition action - {timeout,Time,EventContent} is an event time-out, - that is, if an event arrives the timer is cancelled. - You get either an event or a time-out, but not both. -

+ + Erlang Timers

- Often you want a timer not to be cancelled by any event - or you want to start a timer in one state and respond - to the time-out in another. This can be accomplished - with a regular Erlang timer: - erlang:start_timer. + The previous example of state time-outs only work if + the state machine stays in the same state during the + time-out time. And event time-outs only work if no + disturbing unrelated events occur.

- For the example so far in this chapter: using the - gen_statem event timer has the consequence that - if a button event is generated while in the open state, - the time-out is cancelled and the button event is delivered. - So, we choose to lock the door if this occurred. + You may want to start a timer in one state and respond + to the time-out in another, maybe cancel the time-out + without changing states, or perhaps run multiple + time-outs in parallel. All this can be accomplished + with Erlang Timers: + erlang:start_timer3,4.

- Suppose that we do not want a button to lock the door, - instead we want to ignore button events in the open state. - Then we start a timer when entering the open state - and wait for it to expire while ignoring button events: + Here is how to accomplish the state time-out + in the previous example by insted using an Erlang Timer:

do_unlock(), Tref = erlang:start_timer(10000, self(), lock), - {next_state,open,Data#{remaining := Code, timer := Tref}}; + {next_state, open, Data#{remaining := Code, timer => Tref}}; ... open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> do_lock(), - {next_state,locked,Data}; + {next_state,locked,maps:remove(timer, Data)}; open(cast, {button,_}, Data) -> {keep_state,Data}; ... ]]> +

+ Removing the timer key from the map when we + change to state locked is not strictly + necessary since we can only get into state open + with an updated timer map value. But it can be nice + to not have outdated values in the state Data! +

If you need to cancel a timer because of some other event, you can use erlang:cancel_timer(Tref). - Notice that a time-out message cannot arrive after this, - unless you have postponed it (see the next section) before, + Note that a time-out message cannot arrive after this, + unless you have postponed it before (see the next section), so ensure that you do not accidentally postpone such messages. + Also note that a time-out message may have arrived + just before you cancelling it, so you may have to read out + such a message from the process mailbox depending on + the return value from + erlang:cancel_timer(Tref).

- Another way to cancel a timer is not to cancel it, + Another way to handle a late time-out can be to not cancel it, but to ignore it if it arrives in a state where it is known to be late.

@@ -839,6 +990,7 @@ open(cast, {button,_}, Data) ->
+ Postponing Events

If you want to ignore a particular event in the current state @@ -877,6 +1029,7 @@ open(cast, {button,_}, Data) ->

+ Fuzzy State Diagrams

It is not uncommon that a state diagram does not specify @@ -893,6 +1046,7 @@ open(cast, {button,_}, Data) ->

+ Selective Receive

Erlang's selective receive statement is often used to @@ -972,7 +1126,7 @@ do_unlock() ->

- + State Entry Actions

Say you have a state machine specification @@ -981,7 +1135,7 @@ do_unlock() -> (described in the next section), especially if just one or a few states has got state entry actions, this is a perfect use case for the built in - state enter calls. + state enter calls.

You return a list containing state_enter from your @@ -1012,11 +1166,10 @@ locked( {next_state, open, Data}; ... -open(enter, _OldState, Data) -> - Tref = erlang:start_timer(10000, self(), lock), +open(enter, _OldState, _Data) -> do_unlock(), - {keep_state,Data#{timer => Tref}}; -open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> + {keep_state_and_data, [{state_timeout,10000,lock}]}; +open(state_timeout, lock, Data) -> {next_state, locked, Data}; ... ]]> @@ -1025,6 +1178,7 @@ open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->

+ Self-Generated Events

It can sometimes be beneficial to be able to generate events @@ -1054,7 +1208,7 @@ open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> to the main state machine.

- The following example use an input model where you give the lock + The following example uses an input model where you give the lock characters with put_chars(Chars) and then call enter() to finish the input.

@@ -1102,10 +1256,11 @@ handle_event({call,From}, enter, #{buf := Buf} = Data) ->
+ Example Revisited

- This section includes the example after all mentioned modifications - and some more using state enter calls, + This section includes the example after most of the mentioned + modifications and some more using state enter calls, which deserves a new state diagram:

@@ -1121,6 +1276,7 @@ handle_event({call,From}, enter, #{buf := Buf} = Data) ->

+ Callback Mode: state_functions

Using state functions: @@ -1155,7 +1311,11 @@ callback_mode() -> locked(enter, _OldState, #{code := Code} = Data) -> do_lock(), - {keep_state,Data#{remaining => Code}}; + {keep_state, Data#{remaining => Code}}; +locked( + timeout, _, + #{code := Code, remaining := Remaining} = Data) -> + {keep_state, Data#{remaining := Code}}; locked( cast, {button,Digit}, #{code := Code, remaining := Remaining} = Data) -> @@ -1163,26 +1323,25 @@ locked( [Digit] -> % Complete {next_state, open, Data}; [Digit|Rest] -> % Incomplete - {keep_state,Data#{remaining := Rest}}; + {keep_state, Data#{remaining := Rest}, 30000}; [_|_] -> % Wrong - {keep_state,Data#{remaining := Code}} + {keep_state, Data#{remaining := Code}} end; locked(EventType, EventContent, Data) -> handle_event(EventType, EventContent, Data). -open(enter, _OldState, Data) -> - Tref = erlang:start_timer(10000, self(), lock), +open(enter, _OldState, _Data) -> do_unlock(), - {keep_state,Data#{timer => Tref}}; -open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> + {keep_state_and_data, [{state_timeout,10000,lock}]}; +open(state_timeout, lock, Data) -> {next_state, locked, Data}; open(cast, {button,_}, _) -> - {keep_state_and_data,[postpone]}; + {keep_state_and_data, [postpone]}; open(EventType, EventContent, Data) -> handle_event(EventType, EventContent, Data). handle_event({call,From}, code_length, #{code := Code}) -> - {keep_state_and_data,[{reply,From,length(Code)}]}. + {keep_state_and_data, [{reply,From,length(Code)}]}. do_lock() -> io:format("Locked~n", []). @@ -1198,6 +1357,7 @@ code_change(_Vsn, State, Data, _Extra) ->

+ Callback Mode: handle_event_function

This section describes what to change in the example @@ -1215,9 +1375,15 @@ callback_mode() -> [handle_event_function,state_enter]. %% State: locked -handle_event(enter, _OldState, locked, #{code := Code} = Data) -> +handle_event( + enter, _OldState, locked, + #{code := Code} = Data) -> do_lock(), - {keep_state,Data#{remaining => Code}}; + {keep_state, Data#{remaining => Code}}; +handle_event( + timeout, _, locked, + #{code := Code, remaining := Remaining} = Data) -> + {keep_state, Data#{remaining := Code}}; handle_event( cast, {button,Digit}, locked, #{code := Code, remaining := Remaining} = Data) -> @@ -1225,31 +1391,30 @@ handle_event( [Digit] -> % Complete {next_state, open, Data}; [Digit|Rest] -> % Incomplete - {keep_state,Data#{remaining := Rest}}; + {keep_state, Data#{remaining := Rest}, 30000}; [_|_] -> % Wrong - {keep_state,Data#{remaining := Code}} + {keep_state, Data#{remaining := Code}} end; %% %% State: open -handle_event(enter, _OldState, open, Data) -> - Tref = erlang:start_timer(10000, self(), lock), +handle_event(enter, _OldState, open, _Data) -> do_unlock(), - {keep_state,Data#{timer => Tref}}; -handle_event(info, {timeout,Tref,lock}, open, #{timer := Tref} = Data) -> + {keep_state_and_data, [{state_timeout,10000,lock}]}; +handle_event(state_timeout, lock, open, Data) -> {next_state, locked, Data}; handle_event(cast, {button,_}, open, _) -> {keep_state_and_data,[postpone]}; %% %% Any state handle_event({call,From}, code_length, _State, #{code := Code}) -> - {keep_state_and_data,[{reply,From,length(Code)}]}. + {keep_state_and_data, [{reply,From,length(Code)}]}. ... ]]>

Notice that postponing buttons from the locked state - to the open state feels like the wrong thing to do + to the open state feels like a strange thing to do for a code lock, but it at least illustrates event postponing.

@@ -1257,6 +1422,7 @@ handle_event({call,From}, code_length, _State, #{code := Code}) ->
+ Filter the State

The example servers so far in this chapter @@ -1317,12 +1483,13 @@ format_status(Opt, [_PDict,State,Data]) ->

+ Complex State

The callback mode handle_event_function enables using a non-atom state as described in section - Callback Modes, + Callback Modes, for example, a complex state term like a tuple.

@@ -1396,7 +1563,7 @@ set_lock_button(LockButton) -> init({Code,LockButton}) -> process_flag(trap_exit, true), - Data = #{code => Code, remaining => undefined, timer => undefined}, + Data = #{code => Code, remaining => undefined}, {ok, {locked,LockButton}, Data}. callback_mode() -> @@ -1405,29 +1572,31 @@ callback_mode() -> handle_event( {call,From}, {set_lock_button,NewLockButton}, {StateName,OldLockButton}, Data) -> - {next_state,{StateName,NewLockButton},Data, + {next_state, {StateName,NewLockButton}, Data, [{reply,From,OldLockButton}]}; handle_event( {call,From}, code_length, {_StateName,_LockButton}, #{code := Code}) -> {keep_state_and_data, - [{reply,From,length(Code)}]}; + [{reply,From,length(Code)}]}; %% %% State: locked handle_event( EventType, EventContent, {locked,LockButton}, #{code := Code, remaining := Remaining} = Data) -> - case {EventType,EventContent} of - {enter,_OldState} -> + case {EventType, EventContent} of + {enter, _OldState} -> do_lock(), - {keep_state,Data#{remaining := Code}}; - {{call,From},{button,Digit}} -> + {keep_state, Data#{remaining := Code}}; + {timeout, _} -> + {keep_state, Data#{remaining := Code}}; + {{call,From}, {button,Digit}} -> case Remaining of [Digit] -> % Complete {next_state, {open,LockButton}, Data, [{reply,From,ok}]}; [Digit|Rest] -> % Incomplete - {keep_state, Data#{remaining := Rest}, + {keep_state, Data#{remaining := Rest, 30000}, [{reply,From,ok}]}; [_|_] -> % Wrong {keep_state, Data#{remaining := Code}, @@ -1438,18 +1607,16 @@ handle_event( %% State: open handle_event( EventType, EventContent, - {open,LockButton}, #{timer := Timer} = Data) -> - case {EventType,EventContent} of - {enter,_OldState} -> - Tref = erlang:start_timer(10000, self(), lock), + {open,LockButton}, Data) -> + case {EventType, EventContent} of + {enter, _OldState} -> do_unlock(), - {keep_state,Data#{timer := Tref}}; - {info,{timeout,Timer,lock}} -> + {keep_state_and_data, [{state_timeout,10000,lock}]}; + {state_timeout, lock} -> {next_state, {locked,LockButton}, Data}; - {{call,From},{button,Digit}} -> + {{call,From}, {button,Digit}} -> if Digit =:= LockButton -> - erlang:cancel_timer(Timer), {next_state, {locked,LockButton}, Data, [{reply,From,locked}]); true -> @@ -1494,6 +1661,7 @@ format_status(Opt, [_PDict,State,Data]) ->

+ Hibernation

If you have many servers in one node @@ -1519,20 +1687,21 @@ format_status(Opt, [_PDict,State,Data]) ->

- case {EventType,EventContent} of - {enter,_OldState} -> - Tref = erlang:start_timer(10000, self(), lock), + {open,LockButton}, Data) -> + case {EventType, EventContent} of + {enter, _OldState} -> do_unlock(), - {keep_state,Data#{timer := Tref},[hibernate]}; + {keep_state_and_data, + [{state_timeout,10000,lock},hibernate]}; ... ]]>

- The - [hibernate] - action list on the last line + The atom + hibernate + in the action list on the last line when entering the {open,_} state is the only change. If any event arrives in the {open,_}, state, we do not bother to rehibernate, so the server stays @@ -1546,6 +1715,10 @@ handle_event( be aware of using hibernate while in the {open,_} state, which would clutter the code.

+

+ Another not uncommon scenario is to use the event time-out + to triger hibernation after a certain time of inactivity. +

This server probably does not use heap memory worth hibernating for. -- 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 ++++++-------- lib/tools/emacs/erlang-skels.el | 40 +++--- system/doc/design_principles/statem.xml | 6 +- 4 files changed, 134 insertions(+), 222 deletions(-) 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( diff --git a/lib/tools/emacs/erlang-skels.el b/lib/tools/emacs/erlang-skels.el index 95cc989c73..eeba7f34e9 100644 --- a/lib/tools/emacs/erlang-skels.el +++ b/lib/tools/emacs/erlang-skels.el @@ -931,18 +931,16 @@ Please see the function `tempo-define-template'.") "%% Whenever a gen_statem receives an event, the function " n "%% with the name of the current state (StateName) " n "%% is called to handle the event." n - "%%" n - "%% NOTE: If there is an exported function handle_event/4, it is called" n - "%% instead of StateName/3 functions like this!" n (erlang-skel-separator-end 2) - "-spec state_name(" n> - "'enter', OldState :: gen_statem:state_name()," n> + "-spec state_name('enter'," n> + "OldState :: atom()," n> "Data :: term()) ->" n> - "gen_statem:state_function_enter_result();" n - " (" n> - "gen_statem:event_type(), Msg :: term()," n> + "gen_statem:state_enter_result('state_name');" n> + "(gen_statem:event_type()," n> + "Msg :: term()," n> "Data :: term()) ->" n> - "gen_statem:state_function_result()." n + "gen_statem:event_handler_result(atom())." n + ;; "state_name({call,Caller}, _Msg, Data) ->" n> "{next_state, state_name, Data, [{reply,Caller,ok}]}." n n @@ -1043,18 +1041,18 @@ Please see the function `tempo-define-template'.") "%% @private" n "%% @doc" n "%% This function is called for every event a gen_statem receives." n - "%%" n - "%% NOTE: If there is no exported function handle_event/4," n - "%% StateName/3 functions are called instead!" n - (erlang-skel-separator-end 2) - "-spec handle_event(" n> - "'enter', OldState :: term()," n> - "State :: term(), Data :: term()) ->" n> - "gen_statem:handle_event_enter_result();" n - " (" n> - "gen_statem:event_type(), Msg :: term()," n> - "State :: term(), Data :: term()) ->" n> - "gen_statem:handle_event_result()." n + (erlang-skel-separator-end 2) + "-spec handle_event('enter'," n> + "OldState :: term()," n> + "State :: term()," n> + "Data :: term()) ->" n> + "gen_statem:state_enter_result(term());" n> + "(gen_statem:event_type()," n> + "Msg :: term()," n> + "State :: term()," n> + "Data :: term()) ->" n> + "gen_statem:event_handler_result(term())." n + ;; "handle_event({call,From}, _Msg, State, Data) ->" n> "{next_state, State, Data, [{reply,From,ok}]}." n n diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 9a50bef7b1..bece95e6b8 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -214,7 +214,7 @@ handle_event(EventType, EventContent, State, Data) -> The gen_statem behavior can regardless of callback mode automatically - call the state function + call the state callback with special arguments whenever the state changes so you can write state entry actions @@ -264,7 +264,7 @@ StateName(EventType, EventContent, Data) -> These are ordered by returning a list of actions in the - return tuple + return tuple from the callback function. These state transition actions affect the gen_statem @@ -1141,7 +1141,7 @@ do_unlock() -> You return a list containing state_enter from your callback_mode/0 function and the gen_statem engine will call your - state function once with the arguments + state callback once with the arguments (enter, OldState, ...) whenever the state changes. Then you just need to handle these event-like calls in all states.

-- cgit v1.2.3 From 0c422708ed793a1ee107848f41bf591e4e0a9725 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 13 Oct 2016 15:05:19 +0200 Subject: Revert "Check libsctp for sctp funcs in configure.in" This reverts commit 52fae83743a6e84d719f4f82fe45e6e0efdbd6f0: Check libsctp for sctp funcs in configure.in Call AC_CHECK_LIB before calling AC_CHECK_FUNCS to check for functions in libsctp. Otherwise AC_CHECK_FUNCS will not link with libsctp to see if function exists. Reverting to the old behaviour of not checking for libsctp before checking for sctp functions. Then it works on Linux by loading libsctp and looking up the symbols i runtime, and it works on FreeBSD since there is no separate libsctp to link against - the functions are part of the default system libraries. --- erts/configure.in | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erts/configure.in b/erts/configure.in index 69b9af3c8c..f2c272d292 100644 --- a/erts/configure.in +++ b/erts/configure.in @@ -1726,8 +1726,7 @@ if test "x$enable_sctp" != "xno" ; then fi if test x"$ac_cv_header_netinet_sctp_h" = x"yes"; then - AC_CHECK_LIB(sctp, sctp_bindx) - AC_CHECK_FUNCS([sctp_peeloff sctp_getladdrs sctp_freeladdrs sctp_getpaddrs sctp_freepaddrs]) + AC_CHECK_FUNCS([sctp_bindx sctp_peeloff sctp_getladdrs sctp_freeladdrs sctp_getpaddrs sctp_freepaddrs]) AC_CHECK_DECLS([SCTP_UNORDERED, SCTP_ADDR_OVER, SCTP_ABORT, SCTP_EOF, SCTP_SENDALL, SCTP_ADDR_CONFIRMED, SCTP_DELAYED_ACK_TIME, -- 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(-) 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 From b3c46562296bb088976475141f5837f002998245 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 14 Oct 2016 11:05:45 +0200 Subject: Configure enable libsctp dependency --- configure.in | 7 +++++-- erts/configure.in | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/configure.in b/configure.in index 6db83124be..a149acbb55 100644 --- a/configure.in +++ b/configure.in @@ -2,7 +2,7 @@ dnl Process this file with autoconf to produce a configure script. dnl %CopyrightBegin% dnl -dnl Copyright Ericsson AB 1998-2014. All Rights Reserved. +dnl Copyright Ericsson AB 1998-2016. All Rights Reserved. dnl dnl Licensed under the Apache License, Version 2.0 (the "License"); dnl you may not use this file except in compliance with the License. @@ -228,7 +228,10 @@ AS_HELP_STRING([--enable-kernel-poll], [enable kernel poll support]) AS_HELP_STRING([--disable-kernel-poll], [disable kernel poll support])) AC_ARG_ENABLE(sctp, -AS_HELP_STRING([--enable-sctp], [enable sctp support]) +AS_HELP_STRING([--enable-sctp], [enable sctp support (default) +to on demand load the SCTP library in runtime]) +AS_HELP_STRING([--enable-sctp=lib], [enable sctp support +to link against the SCTP library]) AS_HELP_STRING([--disable-sctp], [disable sctp support])) AC_ARG_ENABLE(hipe, diff --git a/erts/configure.in b/erts/configure.in index f2c272d292..4799178583 100644 --- a/erts/configure.in +++ b/erts/configure.in @@ -196,12 +196,18 @@ AS_HELP_STRING([--disable-kernel-poll], [disable kernel poll support]), AC_ARG_ENABLE(sctp, -AS_HELP_STRING([--enable-sctp], [enable sctp support (default)]) +AS_HELP_STRING([--enable-sctp], [enable sctp support (default) +to on demand load the SCTP library in runtime if needed]) +AS_HELP_STRING([--enable-sctp=lib], [enable sctp support +to link against the SCTP library]) AS_HELP_STRING([--disable-sctp], [disable sctp support]), -[ case "$enableval" in - no) enable_sctp=no ;; - *) enable_sctp=yes ;; - esac ], enable_sctp=unknown) +[ case "x$enableval" in + xno|xyes|xlib|x) + ;; + x*) + AC_MSG_ERROR("invalid value --enable-sctp=$enableval") + ;; + esac ]) AC_ARG_ENABLE(hipe, AS_HELP_STRING([--enable-hipe], [enable hipe support]) @@ -1726,6 +1732,8 @@ if test "x$enable_sctp" != "xno" ; then fi if test x"$ac_cv_header_netinet_sctp_h" = x"yes"; then + AS_IF([test "x$enable_sctp" = "xlib"], + AC_CHECK_LIB(sctp, sctp_bindx)) AC_CHECK_FUNCS([sctp_bindx sctp_peeloff sctp_getladdrs sctp_freeladdrs sctp_getpaddrs sctp_freepaddrs]) AC_CHECK_DECLS([SCTP_UNORDERED, SCTP_ADDR_OVER, SCTP_ABORT, SCTP_EOF, SCTP_SENDALL, SCTP_ADDR_CONFIRMED, -- cgit v1.2.3