From 85e9fed232a6d89e3659cabbb2169cf3e21127e3 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 24 Jan 2017 14:15:26 +0100 Subject: Implement repeat_state and repeat_state_and_data --- lib/stdlib/doc/src/gen_statem.xml | 82 +++++++++++++++++++--- lib/stdlib/src/gen_statem.erl | 116 ++++++++++++++++++-------------- lib/stdlib/test/gen_statem_SUITE.erl | 35 +++++++--- system/doc/design_principles/statem.xml | 11 +++ 4 files changed, 175 insertions(+), 69 deletions(-) diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 6fa2d86e0b..b20abbea5d 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -587,8 +587,8 @@ handle_event(_, _, State, Data) ->

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

@@ -606,7 +606,16 @@ handle_event(_, _, State, Data) -> See Module:StateName/3 and - Module:handle_event/4. + Module:handle_event/4. + Such a call can be repeated by returning a + + repeat_state + + or + + repeat_state_and_data + + tuple from the state callback.

If @@ -625,7 +634,8 @@ handle_event(_, _, State, Data) -> 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. + which can not happen for a subsequent state change, + but will happen when repeating the state enter call.

@@ -640,7 +650,15 @@ handle_event(_, _, State, Data) ->

- If the state changes or is the initial state, and + If the state changes, is the initial state, + + repeat_state + + or + + repeat_state_and_data + + is used, and also state enter calls are used, the gen_statem calls the new state callback with arguments @@ -1095,6 +1113,37 @@ handle_event(_, _, State, Data) -> {next_state,CurrentState,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 + transition_option(), + otherwise 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 + {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

@@ -1870,22 +1919,33 @@ handle_event(_, _, State, Data) -> actions that may be returned: postpone() - and + is not allowed since a state enter call is not + an event so there is no event to postpone, and {next_event,_,_} - are not allowed. + is not allowed since using state enter calls + should not affect how events are consumed and produced. 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. + It is possible to use {repeat_state, ...}, + {repeat_state_and_data,_} or + repeat_state_and_data but all of them makes little + sense since you immediately will be called again with a new + state enter call making this just a weird way + of looping, and there are better ways to loop in Erlang. + You are advised to use {keep_state,...}, + {keep_state_and_data,_} or + keep_state_and_data since you can not change states + from a state enter call anyway.

Note the fact that you can use throw to return the result, which can be useful. For example to bail out with throw(keep_state_and_data) - from deep within complex code that is in no position to - return {next_state,State,Data}. + from deep within complex code that can not + return {next_state,State,Data} because + State or Data is no longer in scope.

diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 4b5f5f676c..5de31ebfe0 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -185,12 +185,23 @@ 'keep_state_and_data' | % {keep_state_and_data,[]} {'keep_state_and_data', % Keep state and data -> only actions Actions :: [ActionType] | ActionType} | + %% + {'repeat_state', % {repeat_state,NewData,[]} + NewData :: data()} | + {'repeat_state', % Repeat state, change data + NewData :: data(), + Actions :: [ActionType] | ActionType} | + 'repeat_state_and_data' | % {repeat_state_and_data,[]} + {'repeat_state_and_data', % Repeat state and data -> only actions + Actions :: [ActionType] | ActionType} | + %% 'stop' | % {stop,normal} {'stop', % Stop the server Reason :: term()} | {'stop', % Stop the server Reason :: term(), NewData :: data()} | + %% {'stop_and_reply', % Reply then stop the server Reason :: term(), Replies :: [reply_action()] | reply_action()} | @@ -602,13 +613,10 @@ enter(Module, Opts, State, Data, Server, Actions, Parent) -> name => Name, state => State, data => Data, - postponed => P, + 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 + %% loop_event_actions/11 when it finally loops back to loop/3 %% in loop_events/10 - %% - %% Marker for initial state, cleared immediately when used - init_state => true }, NewDebug = sys_debug(Debug, S, State, {enter,Event,State}), case call_callback_mode(S) of @@ -617,7 +625,7 @@ enter(Module, Opts, State, Data, Server, Actions, Parent) -> TimerTypes = #{}, loop_event_actions( Parent, NewDebug, NewS, TimerRefs, TimerTypes, - Events, Event, State, Data, NewActions); + Events, Event, State, Data, NewActions, true); {Class,Reason,Stacktrace} -> terminate( Class, Reason, Stacktrace, @@ -900,13 +908,13 @@ loop_event( {NewTimerRefs,NewTimerTypes} = cancel_timer_by_type( timeout, TimerRefs, TimerTypes), - {NewData,NextState,Actions} = + {NewData,NextState,Actions,EnterCall} = parse_event_result( true, Debug, NewS, Result, Events, Event, State, Data), loop_event_actions( Parent, Debug, S, NewTimerRefs, NewTimerTypes, - Events, Event, NextState, NewData, Actions); + Events, Event, NextState, NewData, Actions, EnterCall); {Class,Reason,Stacktrace} -> terminate( Class, Reason, Stacktrace, Debug, S, [Event|Events]) @@ -915,31 +923,16 @@ loop_event( loop_event_actions( Parent, Debug, #{state := State, state_enter := StateEnter} = S, TimerRefs, TimerTypes, - Events, Event, NextState, NewData, Actions) -> + Events, Event, NextState, NewData, + Actions, EnterCall) -> case parse_actions(Debug, S, State, Actions) of {ok,NewDebug,Hibernate,TimeoutsR,Postpone,NextEventsR} -> if - StateEnter, NextState =/= State -> + StateEnter, EnterCall -> loop_event_enter( Parent, NewDebug, S, TimerRefs, TimerTypes, Events, Event, NextState, NewData, Hibernate, TimeoutsR, Postpone, NextEventsR); - 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, TimerRefs, TimerTypes, - Events, Event, NextState, NewData, - Hibernate, TimeoutsR, Postpone, NextEventsR); - false -> - loop_event_result( - Parent, NewDebug, S, TimerRefs, TimerTypes, - Events, Event, NextState, NewData, - Hibernate, TimeoutsR, Postpone, NextEventsR) - end; true -> loop_event_result( Parent, NewDebug, S, TimerRefs, TimerTypes, @@ -958,14 +951,16 @@ loop_event_enter( Hibernate, TimeoutsR, Postpone, NextEventsR) -> case call_state_function(S, enter, State, NextState, NewData) of {ok,Result,NewS} -> - {NewerData,_,Actions} = - parse_event_result( - false, Debug, NewS, Result, - Events, Event, NextState, NewData), - loop_event_enter_actions( - Parent, Debug, NewS, TimerRefs, TimerTypes, - Events, Event, NextState, NewerData, - Hibernate, TimeoutsR, Postpone, NextEventsR, Actions); + case parse_event_result( + false, Debug, NewS, Result, + Events, Event, NextState, NewData) of + {NewerData,_,Actions,EnterCall} -> + loop_event_enter_actions( + Parent, Debug, NewS, TimerRefs, TimerTypes, + Events, Event, NextState, NewerData, + Hibernate, TimeoutsR, Postpone, NextEventsR, + Actions, EnterCall) + end; {Class,Reason,Stacktrace} -> terminate( Class, Reason, Stacktrace, @@ -974,19 +969,27 @@ loop_event_enter( end. loop_event_enter_actions( - Parent, Debug, S, TimerRefs, TimerTypes, + Parent, Debug, #{state_enter := StateEnter} = S, TimerRefs, TimerTypes, Events, Event, NextState, NewData, - Hibernate, TimeoutsR, Postpone, NextEventsR, Actions) -> + Hibernate, TimeoutsR, Postpone, NextEventsR, + Actions, EnterCall) -> case parse_enter_actions( - Debug, S, NextState, Actions, - Hibernate, TimeoutsR) + Debug, S, NextState, Actions, Hibernate, TimeoutsR) of {ok,NewDebug,NewHibernate,NewTimeoutsR,_,_} -> - loop_event_result( - Parent, NewDebug, S, TimerRefs, TimerTypes, - Events, Event, NextState, NewData, - NewHibernate, NewTimeoutsR, Postpone, NextEventsR); + if + StateEnter, EnterCall -> + loop_event_enter( + Parent, NewDebug, S, TimerRefs, TimerTypes, + Events, Event, NextState, NewData, + NewHibernate, NewTimeoutsR, Postpone, NextEventsR); + true -> + loop_event_result( + Parent, NewDebug, S, TimerRefs, TimerTypes, + Events, Event, NextState, NewData, + NewHibernate, NewTimeoutsR, Postpone, NextEventsR) + end; {Class,Reason,Stacktrace} -> terminate( Class, Reason, Stacktrace, @@ -1212,6 +1215,7 @@ parse_event_result( terminate( exit, Reason, ?STACKTRACE(), Debug, S#{data := NewData}, [Event|Events]); + %% {stop_and_reply,Reason,Replies} -> Q = [Event|Events], reply_then_terminate( @@ -1222,22 +1226,34 @@ parse_event_result( reply_then_terminate( exit, Reason, ?STACKTRACE(), Debug, S#{data := NewData}, Q, Replies); + %% {next_state,State,NewData} -> - {NewData,State,[]}; + {NewData,State,[],false}; {next_state,NextState,NewData} when AllowStateChange -> - {NewData,NextState,[]}; + {NewData,NextState,[],true}; {next_state,State,NewData,Actions} -> - {NewData,State,Actions}; + {NewData,State,Actions,false}; {next_state,NextState,NewData,Actions} when AllowStateChange -> - {NewData,NextState,Actions}; + {NewData,NextState,Actions,true}; + %% {keep_state,NewData} -> - {NewData,State,[]}; + {NewData,State,[],false}; {keep_state,NewData,Actions} -> - {NewData,State,Actions}; + {NewData,State,Actions,false}; keep_state_and_data -> - {Data,State,[]}; + {Data,State,[],false}; {keep_state_and_data,Actions} -> - {Data,State,Actions}; + {Data,State,Actions,false}; + %% + {repeat_state,NewData} -> + {NewData,State,[],true}; + {repeat_state,NewData,Actions} -> + {NewData,State,Actions,true}; + repeat_state_and_data -> + {Data,State,[],true}; + {repeat_state_and_data,Actions} -> + {Data,State,Actions,true}; + %% _ -> terminate( error, diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 3eddaab316..24f0864b3c 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -600,15 +600,26 @@ state_enter(_Config) -> (internal, Prev, N) -> Self ! {internal,start,Prev,N}, {keep_state,N + 1}; + ({call,From}, repeat, N) -> + {repeat_state,N + 1, + [{reply,From,{repeat,start,N}}]}; ({call,From}, echo, N) -> - {next_state,wait,N + 1,{reply,From,{echo,start,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} + {stop_and_reply,Reason, + [{reply,From,{stop,N}}],N + 1} end, wait => - fun (enter, Prev, N) -> + fun (enter, Prev, N) when N < 5 -> + {repeat_state,N + 1, + {reply,{Self,N},{enter,Prev}}}; + (enter, Prev, N) -> Self ! {enter,wait,Prev,N}, {keep_state,N + 1}; + ({call,From}, repeat, N) -> + {repeat_state_and_data, + [{reply,From,{repeat,wait,N}}]}; ({call,From}, echo, N) -> {next_state,start,N + 1, [{next_event,internal,wait}, @@ -620,11 +631,15 @@ state_enter(_Config) -> [{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}), + [{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}), [{'EXIT',STM,bye}] = flush(), {noproc,_} = @@ -1790,6 +1805,10 @@ handle_event( {keep_state,[NewData|Machine]}; {keep_state,NewData,Ops} -> {keep_state,[NewData|Machine],Ops}; + {repeat_state,NewData} -> + {repeat_state,[NewData|Machine]}; + {repeat_state,NewData,Ops} -> + {repeat_state,[NewData|Machine],Ops}; Other -> Other end; diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index f627145f9f..8e7f496d9e 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -1180,6 +1180,17 @@ open(state_timeout, lock, Data) -> {next_state, locked, Data}; ... ]]> +

+ You can repeat the state entry code by returning one of + {repeat_state, ...}, {repeat_state_and_data,_} + or repeat_state_and_data that otherwise behaves + exactly like their keep_state siblings. + See the type + + state_callback_result() + + in the reference manual. +

-- cgit v1.2.3