From 61935c77915a8e206a82cba1c9c9f64be462905d Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 21 Jun 2018 09:53:55 +0200 Subject: Clean up and optimize code and doc --- lib/stdlib/doc/src/gen_statem.xml | 228 ++++++++++++++------------ lib/stdlib/src/gen_statem.erl | 261 ++++++++++++++++-------------- lib/stdlib/test/gen_statem_SUITE.erl | 73 ++++++--- system/doc/design_principles/statem.xml | 277 ++++++++++++++++++-------------- 4 files changed, 474 insertions(+), 365 deletions(-) diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index eb0f7d24f0..dfecd235c9 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -167,7 +167,7 @@ erlang:'!' -----> Module:StateName/3

- The "state callback" 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 @@ -179,7 +179,7 @@ erlang:'!' -----> Module:StateName/3 When the callback mode is state_functions, the state must be an atom and - is used as the state callback name; see + is used as the state callback name; see Module:StateName/3. This co-locates all code for a specific state in one function as the gen_statem engine @@ -192,7 +192,7 @@ erlang:'!' -----> Module:StateName/3 When the callback mode is handle_event_function, the state can be any term - and the state callback 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 @@ -200,12 +200,36 @@ erlang:'!' -----> Module:StateName/3 forever creating an infinite busy loop.

- The gen_statem enqueues incoming events in order of arrival - and presents these to the - 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. + When gen_statem receives a process message it is + converted into an event and the + state callback + is called with the event as two arguments: type and content. + When the + state callback + has processed the event it returns to gen_statem + which does a state transition. + If this state transition is to a different state, + that is: NextState =/= State, it is a state change. +

+

+ The + state callback + may return + transition actions + for gen_statem + to execute during the state transition, + for example to reply to a + gen_statem:call/2,3. +

+

+ One of the possible transition actions + is to postpone the current event. + Then it is not retried in the current state. + The gen_statem engine keeps a queue of events + divided into the postponed events + and the events still to process. + After a state change the queue restarts + with the postponed events.

The gen_statem event queue model is sufficient @@ -215,13 +239,17 @@ erlang:'!' -----> Module:StateName/3 to entering a new receive statement.

- The state callback + The + state callback can insert events using the - action() + transition actions next_event - and such an event is inserted as the next to present - to the state callback. That is, as if it is - the oldest incoming event. A dedicated + and such an event is inserted in the event queue + as the next to call the + state callback + with. + That is, as if it is the oldest incoming event. + A dedicated event_type() internal can be used for such events making them impossible to mistake for external events. @@ -236,24 +264,26 @@ erlang:'!' -----> Module:StateName/3

The gen_statem engine can automatically make a specialized call to the - state callback + state callback 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 an event at the state transition, - and/or to use a dedicated state transition function, + Another way to do it is to explicitly insert an event + at the state transition, + and/or to use a dedicated state transition function, but that is something you will have to remember - at every state transition to the state(s) that need it. + at every state transition to the state(s) that need it.

If you in gen_statem, for example, postpone - 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. + an event in one state and then call another state callback + of yours, you have not done a state change + and hence the postponed event is not retried, + which is logical but can be confusing.

- For the details of a state transition, see type + For the details of a state transition, see type transition_option().

@@ -276,7 +306,8 @@ erlang:'!' -----> Module:StateName/3 The gen_statem process can go into hibernation; see proc_lib:hibernate/3. It is done when a - state callback or + state callback + or Module:init/1 specifies hibernate in the returned Actions @@ -551,7 +582,7 @@ handle_event(_, _, State, Data) -> callback mode is handle_event_function, the state can be any term. - After a state change (NextState =/= State), + After a state change (NextState =/= State), all postponed events are retried.

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

@@ -595,7 +626,7 @@ handle_event(_, _, State, Data) ->

internal events can only be generated by the - state machine itself through the state transition action + state machine itself through the transition action next_event.

@@ -633,9 +664,9 @@ handle_event(_, _, State, Data) -> This is the return type from Module:callback_mode/0 and selects - callback mode + callback mode and whether to do - state enter calls, + state enter calls, or not.

@@ -684,13 +715,15 @@ handle_event(_, _, State, Data) -> If Module:callback_mode/0 returns a list containing state_enter, - the gen_statem engine will, at every state change, + the gen_statem engine will, at every state change, call the state callback with arguments (enter, OldState, Data). This may look like an event but is really a call - performed after the previous state callback returned - and before any event is delivered to the new state callback. + performed after the previous + state callback + returned and before any event is delivered to the new + state callback. See Module:StateName/3 and @@ -703,27 +736,27 @@ handle_event(_, _, State, Data) -> repeat_state_and_data - tuple from the state callback. + tuple from the state callback.

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

If Module:code_change/4 should transform the state, - it is regarded as a state rename and not a state change, - which will not cause a state enter call. + it is regarded as a state rename and not a state change, + which will not cause a state enter call.

- Note that a state enter call will be done + 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 cannot happen for a subsequent state change, - but will happen when repeating the state enter call. + actually is not a state change. + In this case OldState =:= State, + which can not happen for a subsequent state change, + but will happen when repeating the state enter call.

@@ -733,8 +766,11 @@ handle_event(_, _, State, Data) ->

Transition options can be set by actions - and modify the state transition. - Here are the sequence of steps for a state transition: + and modify the state transition. + The state transition takes place when the + state callback + has processed an event and returns. + Here are the sequence of steps for a state transition:

@@ -765,7 +801,7 @@ handle_event(_, _, State, Data) -> returned by the state callback that caused the state entry.

- Should this state enter call return any of + Should this state enter call return any of the mentioned repeat_* callback results it is repeated again, with the updated Data.

@@ -787,7 +823,8 @@ handle_event(_, _, State, Data) ->

- If the state changes, the queue of incoming events + If this is a state change, + the queue of incoming events is reset to start with the oldest postponed.

@@ -821,7 +858,7 @@ handle_event(_, _, State, Data) -> if the event queue is empty.

- A state change cancels a + A state change cancels a state_timeout() and any new transition option of this type belongs to the new state. @@ -830,7 +867,7 @@ handle_event(_, _, State, Data) ->

If there are enqueued events the - state callback + state callback for the possibly new state is called with the oldest enqueued event, and we start again from the top of this list. @@ -848,7 +885,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 callback + state callback is called with the corresponding event, and we start again from the top of this sequence.

@@ -861,7 +898,7 @@ handle_event(_, _, State, Data) ->

If true, postpones the current event and retries - it when the state changes + it after a state change (NextState =/= State).

@@ -1021,9 +1058,9 @@ handle_event(_, _, State, Data) ->

- These state transition actions can be invoked by + These transition actions can be invoked by returning them from the - state callback + state callback when it is called with an event, from @@ -1054,7 +1091,7 @@ handle_event(_, _, State, Data) -> transition_option() postpone() - for this state transition. + for this state transition. This action is ignored when returned from Module:init/1 or given to @@ -1093,9 +1130,9 @@ handle_event(_, _, State, Data) ->

- These state transition actions can be invoked by + These transition actions can be invoked by returning them from the - state callback, from + state callback, from Module:init/1 or by giving them to enter_loop/5,6. @@ -1119,7 +1156,7 @@ handle_event(_, _, State, Data) -> Sets the transition_option() hibernate() - for this state transition. + for this state transition.

@@ -1129,9 +1166,9 @@ handle_event(_, _, State, Data) ->

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

@@ -1193,9 +1230,9 @@ handle_event(_, _, State, Data) ->

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

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

@@ -1239,7 +1276,7 @@ handle_event(_, _, State, Data) -> The Actions are executed when entering the first state just as for a - state callback, + state callback, except that the action postpone is forced to false since there is no event to postpone.

@@ -1292,11 +1329,13 @@ handle_event(_, _, State, Data) -> next_state

- The gen_statem does a state transition to + The gen_statem does a state transition to NextState (which can be the same as the current state), sets NewData, and executes all Actions. + If NextState =/= CurrentState + the state transition is a state change.

@@ -1318,54 +1357,33 @@ handle_event(_, _, State, Data) -> 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 + 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}. + The same as + {keep_state,CurrentData,Actions}.

repeat_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. If the gen_statem runs with state enter calls, - the state enter call is repeated, see type + the state enter call is repeated, see type transition_option(), - otherwise repeat_state is the same as + other than that repeat_state is the same as keep_state.

repeat_state_and_data

- The gen_statem keeps the current state and data, or - does a state transition to the current state if you like, - and executes all Actions. - This is the same as + The same as {repeat_state,CurrentData,Actions}. - If the gen_statem runs with - state enter calls, - the state enter call is repeated, see type - transition_option(), - otherwise repeat_state_and_data is the same as - keep_state_and_data.

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

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

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

@@ -1826,7 +1846,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 callback + state callback in the current code version is called. More occasions may be added in future versions of gen_statem. @@ -1883,7 +1903,7 @@ handle_event(_, _, State, Data) ->

This callback is optional, so callback modules need not export it. If a release upgrade/downgrade with - Change={advanced,Extra} + Change = {advanced,Extra} specified in the .appup file is made when code_change/4 is not implemented the process will crash with exit reason undef. @@ -1893,7 +1913,7 @@ handle_event(_, _, State, Data) -> This function is called by a gen_statem when it is to update its internal state during a release upgrade/downgrade, that is, when the instruction {update,Module,Change,...}, - where Change={advanced,Extra}, is specified in the + where Change = {advanced,Extra}, is specified in the appup file. For more information, see OTP Design Principles. @@ -1933,13 +1953,13 @@ handle_event(_, _, State, Data) ->

Also note when upgrading a gen_statem, this function and hence - the Change={advanced,Extra} parameter in the + the Change = {advanced,Extra} parameter in the appup file is not only needed to update the internal state or to act on the Extra argument. It is also needed if an upgrade or downgrade should change callback mode, - or else the callback mode after the code change + or else the callback mode after the code change will not be honoured, most probably causing a server crash.

@@ -2148,7 +2168,7 @@ init(Args) -> erlang:error(not_implemented, [Args]). {call,From}, the caller waits for a reply. The reply can be sent from this or from any other - state callback + state callback by returning with {reply,From,Reply} in Actions, in Replies, @@ -2173,9 +2193,9 @@ init(Args) -> erlang:error(not_implemented, [Args]).

When the gen_statem runs with - state enter calls, + state enter calls, these functions are also called with arguments - (enter, OldState, ...) whenever the state changes. + (enter, OldState, ...) during every state change. In this case there are some restrictions on the actions that may be returned: diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index faa43fbc1e..24b268cd38 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -330,6 +330,7 @@ %% Type validation functions +%% - return true if the value is of the type, false otherwise -compile( {inline, [callback_mode/1, state_enter/1, @@ -1277,7 +1278,7 @@ parse_actions(StateCall, Debug, S, [Action|Actions], TransOpts) -> end. parse_actions_reply( - StateCall, ?not_sys_debug, S, Actions, TransOpts, + StateCall, ?not_sys_debug = Debug, S, Actions, TransOpts, From, Reply) -> %% case from(From) of @@ -1287,8 +1288,7 @@ parse_actions_reply( false -> [error, {bad_action_from_state_function,{reply,From,Reply}}, - ?STACKTRACE(), - ?not_sys_debug] + ?STACKTRACE(), Debug] end; parse_actions_reply( StateCall, Debug, #state{name = Name, state = State} = S, @@ -1302,12 +1302,11 @@ parse_actions_reply( false -> [error, {bad_action_from_state_function,{reply,From,Reply}}, - ?STACKTRACE(), - Debug] + ?STACKTRACE(), Debug] end. parse_actions_next_event( - StateCall, ?not_sys_debug, S, + StateCall, ?not_sys_debug = Debug, S, Actions, TransOpts, Type, Content) -> case event_type(Type) of true when StateCall -> @@ -1320,8 +1319,7 @@ parse_actions_next_event( [error, {bad_state_enter_action_from_state_function, {next_event,Type,Content}}, - ?STACKTRACE(), - ?not_sys_debug] + ?STACKTRACE(), Debug] end; parse_actions_next_event( StateCall, Debug, #state{name = Name, state = State} = S, @@ -1403,13 +1401,13 @@ parse_actions_timeout_add( loop_event_done( Parent, ?not_sys_debug, #state{postponed = P} = S, +%% #state{postponed = will_not_happen = P} = S, Events, Event, NextState, NewData, #trans_opts{ postpone = Postpone, hibernate = Hibernate, - timeouts_r = [], next_events_r = []}) -> + timeouts_r = [], next_events_r = NextEventsR}) -> %% - %% Optimize the simple cases - %% i.e no timer changes, no inserted events and no debug, + %% Optimize the simple cases i.e no debug and no timer changes, %% by duplicate stripped down code %% %% Fast path @@ -1417,14 +1415,12 @@ loop_event_done( case Postpone of true -> loop_event_done_fast( - Parent, Hibernate, - S, - Events, [Event|P], NextState, NewData); + Parent, Hibernate, S, + Events, [Event|P], NextState, NewData, NextEventsR); false -> loop_event_done_fast( - Parent, Hibernate, - S, - Events, P, NextState, NewData) + Parent, Hibernate, S, + Events, P, NextState, NewData, NextEventsR) end; loop_event_done( Parent, Debug_0, @@ -1456,26 +1452,23 @@ loop_event_done( {S#state.name,State}, {consume,Event_0,NextState})|P_0] end, - {Events_2,P_2,Timers_2} = - %% Move all postponed events to queue, - %% cancel the event timer, - %% and cancel the state timeout if the state changes - if - NextState =:= State -> - {Events_0,P_1, + {Events_2,P_2, + Timers_2} = + %% Cancel the event timeout + if + NextState =:= State -> + {Events_0,P_1, cancel_timer_by_type( timeout, {TimerTypes_0,CancelTimers_0})}; - true -> - {lists:reverse(P_1, Events_0), - [], - cancel_timer_by_type( - state_timeout, + true -> + %% Move all postponed events to queue + %% and cancel the state timeout + {lists:reverse(P_1, Events_0),[], + cancel_timer_by_type( + state_timeout, cancel_timer_by_type( timeout, {TimerTypes_0,CancelTimers_0}))} - %% The state timer is removed from TimerTypes - %% but remains in TimerRefs until we get - %% the cancel_timer msg - end, + end, {TimerRefs_3,{TimerTypes_3,CancelTimers_3},TimeoutEvents} = %% Stop and start timers parse_timers(TimerRefs_0, Timers_2, TimeoutsR), @@ -1495,114 +1488,144 @@ loop_event_done( hibernate = Hibernate}, lists:reverse(Events_4R)). +loop_event_done(Parent, Debug, S, Q) -> +%% io:format( +%% "loop_event_done:~n" +%% " state = ~p, data = ~p, postponed = ~p~n " +%% " timer_refs = ~p, timer_types = ~p, cancel_timers = ~p.~n" +%% " Q = ~p.~n", +%% [S#state.state,S#state.data,S#state.postponed, +%% S#state.timer_refs,S#state.timer_types,S#state.cancel_timers, +%% Q]), + case Q of + [] -> + %% Get a new event + loop(Parent, Debug, S); + [{Type,Content}|Events] -> + %% Loop until out of enqueued events + loop_event(Parent, Debug, S, Events, Type, Content) + end. + + %% Fast path %% +%% Cancel event timer and state timer only if running loop_event_done_fast( Parent, Hibernate, #state{ state = NextState, - timer_types = #{timeout := _} = TimerTypes, + timer_types = TimerTypes, cancel_timers = CancelTimers} = S, - Events, P, NextState, NewData) -> - %% - %% Same state, event timeout active - %% - loop_event_done_fast( - Parent, Hibernate, S, - Events, P, NextState, NewData, - cancel_timer_by_type( - timeout, {TimerTypes,CancelTimers})); -loop_event_done_fast( - Parent, Hibernate, - #state{state = NextState} = S, - Events, P, NextState, NewData) -> - %% + Events, P, NextState, NewData, NextEventsR) -> %% Same state - %% - loop_event_done( - Parent, ?not_sys_debug, - S#state{ - data = NewData, - postponed = P, - hibernate = Hibernate}, - Events); -loop_event_done_fast( - Parent, Hibernate, - #state{ - timer_types = #{timeout := _} = TimerTypes, - cancel_timers = CancelTimers} = S, - Events, P, NextState, NewData) -> - %% - %% State change, event timeout active - %% - loop_event_done_fast( - Parent, Hibernate, S, - lists:reverse(P, Events), [], NextState, NewData, - cancel_timer_by_type( - state_timeout, - cancel_timer_by_type( - timeout, {TimerTypes,CancelTimers}))); + case TimerTypes of + #{timeout := _} -> + %% Event timeout active + loop_event_done_fast_2( + Parent, Hibernate, S, + Events, P, NextState, NewData, NextEventsR, + cancel_timer_by_type( + timeout, {TimerTypes,CancelTimers})); + _ -> + %% No event timeout active + loop_event_done_fast_2( + Parent, Hibernate, S, + Events, P, NextState, NewData, NextEventsR, + {TimerTypes,CancelTimers}) + end; loop_event_done_fast( Parent, Hibernate, #state{ - timer_types = #{state_timeout := _} = TimerTypes, + timer_types = TimerTypes, cancel_timers = CancelTimers} = S, - Events, P, NextState, NewData) -> - %% - %% State change, state timeout active - %% - loop_event_done_fast( - Parent, Hibernate, S, - lists:reverse(P, Events), [], NextState, NewData, - cancel_timer_by_type( - state_timeout, - cancel_timer_by_type( - timeout, {TimerTypes,CancelTimers}))); + Events, P, NextState, NewData, NextEventsR) -> + %% State change + case TimerTypes of + #{timeout := _} -> + %% Event timeout active + %% - cancel state_timeout too since it is faster than inspecting + loop_event_done_fast( + Parent, Hibernate, S, + Events, P, NextState, NewData, NextEventsR, + cancel_timer_by_type( + state_timeout, + cancel_timer_by_type( + timeout, {TimerTypes,CancelTimers}))); + #{state_timeout := _} -> + %% State_timeout active but not event timeout + loop_event_done_fast( + Parent, Hibernate, S, + Events, P, NextState, NewData, NextEventsR, + cancel_timer_by_type( + state_timeout, {TimerTypes,CancelTimers})); + _ -> + %% No event nor state_timeout active + loop_event_done_fast( + Parent, Hibernate, S, + Events, P, NextState, NewData, NextEventsR, + {TimerTypes,CancelTimers}) + end. +%% +%% Retry postponed events loop_event_done_fast( - Parent, Hibernate, - #state{} = S, - Events, P, NextState, NewData) -> - %% - %% State change, no timeout to automatically cancel - %% - loop_event_done( - Parent, ?not_sys_debug, - S#state{ - state = NextState, - data = NewData, - postponed = [], - hibernate = Hibernate}, - lists:reverse(P, Events)). + Parent, Hibernate, S, + Events, P, NextState, NewData, NextEventsR, TimerTypes_CancelTimers) -> + case P of + %% Handle 0..2 postponed events without list reversal since + %% that will move out all live registers and back again + [] -> + loop_event_done_fast_2( + Parent, Hibernate, S, + Events, [], NextState, NewData, NextEventsR, + TimerTypes_CancelTimers); + [E] -> + loop_event_done_fast_2( + Parent, Hibernate, S, + [E|Events], [], NextState, NewData, NextEventsR, + TimerTypes_CancelTimers); + [E1,E2] -> + loop_event_done_fast_2( + Parent, Hibernate, S, + [E2,E1|Events], [], NextState, NewData, NextEventsR, + TimerTypes_CancelTimers); + _ -> + %% A bit slower path + loop_event_done_fast_2( + Parent, Hibernate, S, + lists:reverse(P, Events), [], NextState, NewData, NextEventsR, + TimerTypes_CancelTimers) + end. %% %% Fast path %% -loop_event_done_fast( +loop_event_done_fast_2( Parent, Hibernate, S, - Events, P, NextState, NewData, + Events, P, NextState, NewData, NextEventsR, {TimerTypes,CancelTimers}) -> %% - loop_event_done( - Parent, ?not_sys_debug, - S#state{ - state = NextState, - data = NewData, - postponed = P, - timer_types = TimerTypes, - cancel_timers = CancelTimers, - hibernate = Hibernate}, - Events). - -loop_event_done(Parent, Debug, S, Q) -> - case Q of + NewS = + S#state{ + state = NextState, + data = NewData, + postponed = P, + timer_types = TimerTypes, + cancel_timers = CancelTimers, + hibernate = Hibernate}, + case NextEventsR of + %% Handle 0..2 next events without list reversal since + %% that will move out all live registers and back again [] -> - %% Get a new event - loop(Parent, Debug, S); - [{Type,Content}|Events] -> - %% Loop until out of enqueued events - loop_event(Parent, Debug, S, Events, Type, Content) + loop_event_done(Parent, ?not_sys_debug, NewS, Events); + [E] -> + loop_event_done(Parent, ?not_sys_debug, NewS, [E|Events]); + [E2,E1] -> + loop_event_done(Parent, ?not_sys_debug, NewS, [E1,E2|Events]); + _ -> + %% A bit slower path + loop_event_done( + Parent, ?not_sys_debug, NewS, lists:reverse(NextEventsR, Events)) end. - %%--------------------------------------------------------------------------- %% Server loop helpers diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 053233df9b..017939fdd6 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -121,7 +121,8 @@ end_per_testcase(_CaseName, Config) -> start1(Config) -> %%OldFl = process_flag(trap_exit, true), - {ok,Pid0} = gen_statem:start_link(?MODULE, start_arg(Config, []), []), + {ok,Pid0} = + gen_statem:start_link(?MODULE, start_arg(Config, []), [{debug,[trace]}]), ok = do_func_test(Pid0), ok = do_sync_func_test(Pid0), stop_it(Pid0), @@ -135,7 +136,8 @@ start1(Config) -> %% anonymous w. shutdown start2(Config) -> %% Dont link when shutdown - {ok,Pid0} = gen_statem:start(?MODULE, start_arg(Config, []), []), + {ok,Pid0} = + gen_statem:start(?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid0), ok = do_sync_func_test(Pid0), stopped = gen_statem:call(Pid0, {stop,shutdown}), @@ -641,51 +643,72 @@ state_enter(_Config) -> end, start => fun (enter, Prev, N) -> - Self ! {enter,start,Prev,N}, + Self ! {N,enter,start,Prev}, {keep_state,N + 1}; (internal, Prev, N) -> - Self ! {internal,start,Prev,N}, + Self ! {N,internal,start,Prev}, {keep_state,N + 1}; + (timeout, M, N) -> + {keep_state, N + 1, + {reply, {Self,N}, {timeout,M}}}; ({call,From}, repeat, N) -> {repeat_state,N + 1, - [{reply,From,{repeat,start,N}}]}; + [{reply,From,{N,repeat,start}}]}; ({call,From}, echo, N) -> {next_state,wait,N + 1, - {reply,From,{echo,start,N}}}; + [{reply,From,{N,echo,start}},{timeout,0,N}]}; ({call,From}, {stop,Reason}, N) -> {stop_and_reply,Reason, - [{reply,From,{stop,N}}],N + 1} + [{reply,From,{N,stop}}],N + 1} end, wait => fun (enter, Prev, N) when N < 5 -> {repeat_state,N + 1, - {reply,{Self,N},{enter,Prev}}}; + [{reply,{Self,N},{enter,Prev}}, + {timeout,0,N}, + {state_timeout,0,N}]}; (enter, Prev, N) -> - Self ! {enter,wait,Prev,N}, - {keep_state,N + 1}; + Self ! {N,enter,wait,Prev}, + {keep_state,N + 1, + [{timeout,0,N}, + {state_timeout,0,N}]}; + (timeout, M, N) -> + {keep_state, N + 1, + {reply, {Self,N}, {timeout,M}}}; + (state_timeout, M, N) -> + {keep_state, N + 1, + {reply, {Self,N}, {state_timeout,M}}}; ({call,From}, repeat, N) -> {repeat_state_and_data, - [{reply,From,{repeat,wait,N}}]}; + [{reply,From,{N,repeat,wait}}, + {timeout,0,N}]}; ({call,From}, echo, N) -> {next_state,start,N + 1, [{next_event,internal,wait}, - {reply,From,{echo,wait,N}}]} + {reply,From,{N,echo,wait}}]} end}, {ok,STM} = gen_statem:start_link( - ?MODULE, {map_statem,Machine,[state_enter]}, []), - - [{enter,start,start,1}] = flush(), - {echo,start,2} = gen_statem:call(STM, echo), - [{3,{enter,start}},{4,{enter,start}},{enter,wait,start,5}] = flush(), - {wait,[6|_]} = sys:get_state(STM), - {repeat,wait,6} = gen_statem:call(STM, repeat), - [{enter,wait,wait,6}] = flush(), - {echo,wait,7} = gen_statem:call(STM, echo), - [{enter,start,wait,8},{internal,start,wait,9}] = flush(), - {repeat,start,10} = gen_statem:call(STM, repeat), - [{enter,start,start,11}] = flush(), - {stop,12} = gen_statem:call(STM, {stop,bye}), + ?MODULE, {map_statem,Machine,[state_enter]}, [{debug,[trace]}]), + + [{1,enter,start,start}] = flush(), + {2,echo,start} = gen_statem:call(STM, echo), + [{3,{enter,start}}, + {4,{enter,start}}, + {5,enter,wait,start}, + {6,{timeout,5}}, + {7,{state_timeout,5}}] = flush(), + {wait,[8|_]} = sys:get_state(STM), + {8,repeat,wait} = gen_statem:call(STM, repeat), + [{8,enter,wait,wait}, + {9,{timeout,8}}, + {10,{state_timeout,8}}] = flush(), + {11,echo,wait} = gen_statem:call(STM, echo), + [{12,enter,start,wait}, + {13,internal,start,wait}] = flush(), + {14,repeat,start} = gen_statem:call(STM, repeat), + [{15,enter,start,start}] = flush(), + {16,stop} = gen_statem:call(STM, {stop,bye}), [{'EXIT',STM,bye}] = flush(), {noproc,_} = diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 98fd1fd69d..29e19163a5 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -44,27 +44,39 @@ Event-Driven State Machines

Established Automata Theory does not deal much with - how a state transition is triggered, + how a state transition is triggered, but assumes that the output is a function of the input (and the state) and that they are some kind of values.

For an Event-Driven State Machine, the input is an event - that triggers a state transition and the output - is actions executed during the state transition. + that triggers a state transition and the output + is actions executed during the state transition. It can analogously to the mathematical model of a - Finite-State Machine be described as + Finite State Machine be described as a set of relations of the following form:

 State(S) x Event(E) -> Actions(A), State(S')
-

These relations are interpreted as follows: +

+ These relations are interpreted as follows: if we are in state S and event E occurs, we are to perform actions A and make a transition to state S'. Notice that S' can be equal to S and that A can be empty.

+

+ In gen_statem we define + a state change as a state transition + in which the new state S' is different from + the current state S, where "different" means + Erlang's strict inequality: =/= + also know as "does not match". + During a state changes, + gen_statem does more things + than during other state transitions. +

As A and S' depend only on S and E, the kind of state machine described @@ -95,8 +107,8 @@ State(S) x Event(E) -> Actions(A), State(S') Co-located callback code for each state, - regardless of - Event Type + for all + Event Types (such as call, cast and info) @@ -114,13 +126,13 @@ State(S) x Event(E) -> Actions(A), State(S') - State Enter Calls + State Enter Calls (callback on state entry co-located with the rest of each state's callback code) - Easy-to-use timeouts + Easy-to-use time-outs (State Time-Outs, Event Time-Outs and @@ -152,11 +164,11 @@ State(S) x Event(E) -> Actions(A), State(S') Callback Module

- The callback module contains functions that implement + The callback module contains functions that implement the state machine. When an event occurs, the gen_statem behaviour engine - calls a function in the callback module with the event, + calls a function in the callback module with the event, current state and server data. This function performs the actions for this event, and returns the new state and server data @@ -166,7 +178,7 @@ State(S) x Event(E) -> Actions(A), State(S') The behaviour engine holds the state machine state, server data, timer references, a queue of posponed messages and other metadata. It receives all process messages, - handles the system messages, and calls the callback module + handles the system messages, and calls the callback module with machine specific events.

@@ -177,7 +189,7 @@ State(S) x Event(E) -> Actions(A), State(S') Callback Modes

- The gen_statem behavior supports two callback modes: + The gen_statem behavior supports two callback modes:

@@ -202,31 +214,33 @@ State(S) x Event(E) -> Actions(A), State(S')

- The callback mode is selected at server start + The callback mode is selected at server start and may be changed with a code upgrade/downgrade.

See the section - Event Handler + State Callback that describes the event handling callback function(s).

- The callback mode is selected by implementing + The callback mode is selected by implementing a mandatory callback function Module:callback_mode() - that returns one of the callback modes. + that returns one of the callback modes.

The Module:callback_mode() - function may also return a list containing the callback mode + function may also return a list containing the callback mode and the atom state_enter in which case - State Enter Calls - are activated for the callback mode. + + state enter calls + + are activated for the callback mode.

@@ -237,11 +251,11 @@ State(S) x Event(E) -> Actions(A), State(S') it is the one most like gen_fsm. But if you do not want the restriction that the state must be an atom, or if you do not want to write - one event handler function per state; please read on... + one state callback function per state; please read on...

The two - Callback Modes + callback modes give different possibilities and restrictions, with one common goal: to handle all possible combinations of events and states. @@ -257,7 +271,7 @@ State(S) x Event(E) -> Actions(A), State(S') With state_functions, you are restricted to use atom-only states, and the gen_statem engine branches depending on state name for you. - This encourages the callback module to co-locate + This encourages the callback module to co-locate the implementation of all event actions particular to one state in the same place in the code, hence to focus on one state at the time. @@ -302,11 +316,12 @@ State(S) x Event(E) -> Actions(A), State(S')

- - Event Handler + + State Callback

- Which callback function that handles an event - depends on the callback mode: + The state callback is the callback function + that handles an event in the current state, + and which function that is depends on the callback mode:

state_functions @@ -329,7 +344,9 @@ State(S) x Event(E) -> Actions(A), State(S')

See section - One Event Handler + + One State Callback + for an example.

@@ -338,15 +355,17 @@ State(S) x Event(E) -> Actions(A), State(S') The state is either the name of the function itself or an argument to it. The other arguments are the EventType described in section Event Types, - the event dependent EventContent, and the current server Data. + the event dependent EventContent, + and the current server Data.

- State enter calls are also handled by the event handler and have - slightly different arguments. See the section + State enter calls are also handled by the event handler + and have slightly different arguments. See section State Enter Calls.

- The event handler return values are defined in the description of + The state callback return values + are defined in the description of Module:StateName/3 @@ -361,24 +380,29 @@ State(S) x Event(E) -> Actions(A), State(S')

Set next state and update the server data. - If the Actions field is used, execute state transition actions. - An empty Actions list is equivalent to not returning the field. + If the Actions field is used, + execute transition actions. + An empty Actions list is equivalent to + not returning the field.

See section - - State Transition Actions + + Transition Actions for a list of possible - state transition actions. + transition actions.

- If NextState =/= State the state machine changes - to a new state. A + If NextState =/= State this is a state change + so the extra things gen_statem does are: the event queue + is restarted from the oldest + postponed event, + any current + state time-out + is cancelled, and a state enter call - is performed if enabled and all - postponed events - are retried. + is performed, if enabled.

@@ -388,7 +412,7 @@ State(S) x Event(E) -> Actions(A), State(S')

Same as the next_state values with - NextState =:= State, that is, no state change. + NextState =:= State, that is, no state change.

@@ -414,9 +438,16 @@ State(S) x Event(E) -> Actions(A), State(S') State Enter Calls - are enabled, repeat the state enter call + are enabled, repeat the state enter call as if this state was entered again.

+

+ If these return values are used from a + state enter call the OldState does not change, + but if used from an event handling state callback + the new state enter call's OldState + will be the current state. +

{stop, Reason, NewData}
@@ -435,7 +466,10 @@ State(S) x Event(E) -> Actions(A), State(S')

Same as the stop values, but first execute the given - state transition actions that may only be reply actions. + + transition actions + + that may only be reply actions.

@@ -449,8 +483,8 @@ State(S) x Event(E) -> Actions(A), State(S') Module:init(Args) callback function is called before any - Event Handler - is called. This function behaves like an event handler + state callback + is called. This function behaves like an state callback function, but gets its only argument Args from the gen_statem @@ -474,8 +508,8 @@ State(S) x Event(E) -> Actions(A), State(S')
- - State Transition Actions + + Transition Actions

In the first section @@ -483,13 +517,13 @@ State(S) x Event(E) -> Actions(A), State(S') actions were mentioned as a part of the general state machine model. These general actions - are implemented with the code that callback module + 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 + There are more specific transition actions that a callback function can command the gen_statem engine to do after the callback function return. These are commanded by returning a list of @@ -500,7 +534,7 @@ State(S) x Event(E) -> Actions(A), State(S') from the callback function. - These are the possible state transition actions: + These are the possible transition actions:

@@ -596,13 +630,13 @@ State(S) x Event(E) -> Actions(A), State(S') Event Types

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

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

@@ -624,7 +658,7 @@ State(S) x Event(E) -> Actions(A), State(S') Generated by gen_statem:call, where From is the reply address to use - when replying either through the state transition action + when replying either through the transition action {reply,From,Msg} or by calling gen_statem:reply. @@ -643,7 +677,7 @@ State(S) x Event(E) -> Actions(A), State(S')
- Generated by state transition action + Generated by transition action {state_timeout,Time,EventContent} @@ -655,7 +689,7 @@ State(S) x Event(E) -> Actions(A), State(S') - Generated by state transition action + Generated by transition action {{timeout,Name},Time,EventContent} @@ -667,7 +701,7 @@ State(S) x Event(E) -> Actions(A), State(S') - Generated by state transition action + Generated by transition action {timeout,Time,EventContent} @@ -680,10 +714,10 @@ State(S) x Event(E) -> Actions(A), State(S') - Generated by state transition + Generated by state transition action {next_event,internal,EventContent}. - All event types above can also be generated using + All event types above can also be generated using {next_event,EventType,EventContent}.
@@ -696,14 +730,14 @@ State(S) x Event(E) -> Actions(A), State(S') State Enter Calls

The gen_statem behavior can if this is enabled, - regardless of callback mode, + regardless of callback mode, automatically call the state callback with special arguments whenever the state changes so you can write state enter actions - near the rest of the state transition rules. + near the rest of the state transition rules. It typically looks like this:

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

- Since the state enter call is not an event there are restrictions + Since the state enter call is not an event there are restrictions on the allowed return value and State Transition Actions. You may not change the state, postpone this non-event, or - insert events. + insert any events.

- The first state that is entered will get a state enter call + The first state that is entered + will get a state enter call with OldState equal to the current state.

- You may repeat the state enter call using the {repeat_state,...} + You may repeat the state enter call + using the {repeat_state,...} return value from the - Event Handler. + state callback. In this case OldState will also be equal to the current state.

Depending on how your state machine is specified, - this can be a very useful feature, - but it forces you to handle the state enter calls 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 Enter Actions - chapter. + section.

@@ -765,7 +801,7 @@ StateName(EventType, EventContent, Data) ->

This code lock state machine can be implemented using - gen_statem with the following callback module: + gen_statem with the following callback module:

The second argument, ?MODULE, is the name of - the callback module, that is, the module where the callback + the callback module, that is, + the module where the callback functions are located, which is this module.

@@ -935,7 +972,7 @@ init(Code) -> Module:callback_mode/0 selects the CallbackMode - for the callback module, in this case + for the callback module, in this case state_functions. That is, each state has got its own handler function:

@@ -1051,11 +1088,11 @@ open(state_timeout, lock, 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. + when the state machine does a state change. + 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.

@@ -1137,7 +1174,7 @@ open(...) -> ... ; care about what it is.

- If the common event handler needs to know the current state + If the common state callback needs to know the current state a function handle_common/4 can be used instead:

... ;
- - One Event Handler + + One State Callback

If - Callback Mode + callback mode handle_event_function is used, all events are handled in @@ -1289,7 +1326,10 @@ stop() -> You get either an event or a time-out, but not both.

- It is ordered by the state transition action + It is ordered by the + + transition action + {timeout,Time,EventContent}, or just an integer Time, even without the enclosing actions list (the latter is a form inherited from gen_fsm. @@ -1315,7 +1355,7 @@ locked( ]]>

Whenever we receive a button event we start an event time-out - of 30 seconds, and if we get an event type timeout + of 30 seconds, and if we get an event type of timeout we reset the remaining code sequence.

@@ -1327,7 +1367,7 @@ locked(

Note that an event time-out does not work well with - when you have for example a status call as in + when you have for example a status call as in section All State Events, or handle unknown events, since all kinds of events will cancel the event time-out. @@ -1383,14 +1423,14 @@ open(cast, {button,_}, Data) -> ]]>

Specific generic time-outs can just as - State Time-Outs + state time-outs be restarted or cancelled by setting it to a new time or infinity.

- In this particular case we do not need to cancel the timeout - since the timeout event is the only possible reason to - change the state from open to locked. + In this particular case we do not need to cancel the time-out + since the time-out event is the only possible reason to + do a state change from open to locked.

Instead of bothering with when to cancel a time-out, @@ -1442,7 +1482,7 @@ open(cast, {button,_}, Data) -> ]]>

Removing the timer key from the map when we - change to state locked is not strictly + do a state change to 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! @@ -1474,13 +1514,13 @@ open(cast, {button,_}, Data) ->

If you want to ignore a particular event in the current state and handle it in a future state, you can postpone the event. - A postponed event is retried after the state has - changed, that is, OldState =/= NewState. + A postponed event is retried after a state change, + that is, OldState =/= NewState.

- Postponing is ordered by the state transition - - State Transition Action + Postponing is ordered by the + + transition action postpone.

@@ -1496,7 +1536,8 @@ open(cast, {button,_}, Data) -> ... ]]>

- Since a postponed event is only retried after a state change, + Since a postponed event is only retried + after a state change, you have to think about where to keep a state data item. You can keep it in the server Data or in the State itself, @@ -1505,7 +1546,7 @@ open(cast, {button,_}, Data) -> (see section Complex State) with - Callback Mode + callback mode handle_event_function. If a change in the value changes the set of events that is handled, then the value should be kept in the State. @@ -1606,17 +1647,17 @@ do_unlock() -> sys compatible behaviors must respond to system messages and therefore do that in their engine receive loop, - passing non-system messages to the callback module. + passing non-system messages to the callback module.

The - - State Transition Action + + transition action postpone is designed to model selective receives. A selective receive implicitly postpones any not received events, but the postpone - state transition action explicitly postpones one received event. + transition action explicitly postpones one received event.

Both mechanisms have the same theoretical @@ -1638,14 +1679,17 @@ do_unlock() -> (described in the next section), especially if just one or a few states has got state enter 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 - callback_mode/0 + + callback_mode/0 + function and the gen_statem engine will call your - state callback once with the arguments - (enter, OldState, ...) whenever the state changes. + state callback once with an event + (enter, OldState, ...) + whenever it does a state change. Then you just need to handle these event-like calls in all states.

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 + + transition action {next_event,EventType,EventContent}.

@@ -1731,11 +1775,9 @@ open(state_timeout, lock, Data) ->

A variant of this is to use a - - Complex State - + complex state with - One Event Handler. + one state callback. The state is then modeled with for example a tuple {MainFSMState,SubFSMState}.

@@ -1795,7 +1837,7 @@ open(internal, {button,_}, Data) -> Example Revisited

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

+ +
+ + Time-outs +

+ Time-outs in gen_statem are started from a + + transition action + + during a state transition that is when exiting from the + state callback. +

+

+ There are 3 types of time-outs in gen_statem: +

+ + + + state_timeout + + + + There is one + State Time-Out + that is automatically cancelled by a state change. + + + + {timeout, Name} + + + + There are any number of + Generic Time-Outs + differing by their Name. + They have no automatic cancelling. + + + + timeout + + + + There is one + Event Time-Out + that is automatically cancelled by any event. + Note that + postponed + and + inserted + events cancel this timeout just as external events. + + +

+ When a time-out is started any running time-out with the same tag, + state_timeout, {timeout, Name} or timeout, + is cancelled, that is the time-out is restarted with the new time. +

+

+ All time-outs has got an EventContent that is part of the + + transition action + + that starts the time-out. + Different EventContents does not create different time-outs. + The EventContent is delivered to the + state callback + when the time-out expires. +

+
+ + Cancelling a Time-Out +

+ If a time-out is started with the time infinity it will + never time out, in fact it will not even be started, and any + running time-out with the same tag will be cancelled. + The EventContent will in this case be ignored, + so why not set it to undefined. +

+
+
+ + Time-Out Zero +

+ If a time-out is started with the time 0 it will + actually not be started. Instead the time-out event will + immediately be inserted to be processed after any events + already enqueued, and before any not yet received external events. + Note that some time-outs are automatically cancelled + so if you for example combine + postponing + an event in a state change with starting an + event time-out + with time 0 there will be no timeout event inserted + since the event time-out is cancelled by the postponed + event that is delivered due to the state change. +

+
+
+ +
-- cgit v1.2.3