From 3ed7d729cab697b9f668dadb563d629de10f593d Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 10 Apr 2018 10:50:41 +0200 Subject: Fix timeout parsing and doc feedback --- lib/stdlib/doc/src/gen_statem.xml | 134 +++++++++++++++++++++++------------ lib/stdlib/src/gen_statem.erl | 91 ++++++++++++------------ lib/stdlib/test/gen_statem_SUITE.erl | 46 +++++++++++- 3 files changed, 178 insertions(+), 93 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 252a8370ad..fe391b329a 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -71,16 +71,18 @@ had and adds some really useful:

- Gathered state code. - Arbitrary term state. - Event postponing. - Self-generated events. - State time-out. - Multiple generic named time-outs. - Absolute time-out time. - Automatic state enter calls. - Reply from other state than the request. - Multiple sys traceable replies. + Gathered state code + Arbitrary term state + Event postponing + Self-generated events + State time-out + Multiple generic named time-outs + Absolute time-out time + Automatic state enter calls + + Reply from other state than the request, sys traceable + + Multiple sys traceable replies @@ -232,8 +234,10 @@ erlang:'!' -----> Module:StateName/3 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. + Another way to do it is to 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.

If you in gen_statem, for example, postpone @@ -703,9 +707,9 @@ handle_event(_, _, State, Data) ->

If Module:code_change/4 - 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. + should transform the state, + 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 @@ -723,12 +727,19 @@ handle_event(_, _, State, Data) ->

Transition options can be set by actions - and they modify how the state transition is done: + and modify the state transition. + Here are the sequence of steps for a state transition:

- If the state changes, is the initial state, + If + + state enter calls + + are used, and either: + the state changes, it is the initial state, + or one of the callback results repeat_state @@ -736,16 +747,21 @@ handle_event(_, _, State, Data) -> repeat_state_and_data - is used, and also - state enter calls - are used, the gen_statem calls + is used; the gen_statem calls 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 callback that changed states. + appended to the actions + returned by the state callback that caused the state entry. +

+

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

@@ -774,7 +790,7 @@ handle_event(_, _, State, Data) -> All events stored with action() next_event - are inserted to be processed before the other queued events. + are inserted to be processed before previously queued events.

@@ -788,7 +804,9 @@ handle_event(_, _, State, Data) -> delivered to the state machine before any external not yet received event so if there is such a time-out requested, the corresponding time-out zero event is enqueued as - the newest event. + the newest received event; + that is after already queued events + such as inserted and postponed events.

Any event cancels an @@ -826,7 +844,7 @@ handle_event(_, _, State, Data) -> When a new message arrives the state callback is called with the corresponding event, - and we start again from the top of this list. + and we start again from the top of this sequence.

@@ -851,13 +869,19 @@ handle_event(_, _, State, Data) -> proc_lib:hibernate/3 before going into receive to wait for a new external event. - If there are enqueued events, - to prevent receiving any new event, an - erlang:garbage_collect/0 - is done instead to simulate - that the gen_statem entered hibernation - and immediately got awakened by the oldest enqueued event.

+ +

+ If there are enqueued events to process + when hibrnation is requested, + this is optimized by not hibernating but instead calling + + erlang:garbage_collect/0 + + to simulate that the gen_statem entered hibernation + and immediately got awakened by an enqueued event. +

+
@@ -892,7 +916,7 @@ handle_event(_, _, State, Data) -> 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. + received external event, but after already queued events.

Note that it is not possible nor needed to cancel this time-out, @@ -978,7 +1002,9 @@ handle_event(_, _, State, Data) -> If Abs is true an absolute timer is started, and if it is false a relative, which is the default. See - erlang:start_timer/4 + + erlang:start_timer/4 + for details.

@@ -1004,7 +1030,9 @@ handle_event(_, _, State, Data) ->

Actions that set - transition options + + transition options + override any previous of the same type, so the last in the containing list wins. For example, the last @@ -1016,7 +1044,9 @@ handle_event(_, _, State, Data) ->

Sets the - transition_option() + + transition_option() + postpone() for this state transition. This action is ignored when returned from @@ -1029,7 +1059,11 @@ handle_event(_, _, State, Data) -> next_event

- Stores the specified EventType + This action does not set any + + transition_option() + + but instead stores the specified EventType and EventContent for insertion after all actions have been executed.

@@ -1101,15 +1135,15 @@ handle_event(_, _, State, Data) -> transition options.

- Timeout + Time

- Short for {timeout,Timeout,Timeout}, that is, + Short for {timeout,Time,Time}, that is, the time-out message is the time-out time. This form exists to make the state callback - return value {next_state,NextState,NewData,Timeout} - allowed like for gen_fsm's + return value {next_state,NextState,NewData,Time} + allowed like for gen_fsm.

timeout @@ -1161,7 +1195,11 @@ handle_event(_, _, State, Data) -> enter_loop/5,6.

- It replies to a caller waiting for a reply in + It does not set any + + transition_option() + + but instead replies to a caller waiting for a reply in call/2. From must be the term from argument {call,From} @@ -2144,16 +2182,20 @@ init(Args) -> erlang:error(not_implemented, [Args]). You may also not change states from this call. Should you return {next_state,NextState, ...} with NextState =/= State the gen_statem crashes. - It is possible to use {repeat_state, ...}, - {repeat_state_and_data,_} or - repeat_state_and_data but all of them makes little + Note that it is actually allowed to use + {repeat_state, NewData, ...} although it 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. + If you do not update NewData and have some + loop termination condition, or if you use + {repeat_state_and_data, _} or + repeat_state_and_data you have an infinite loop! 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. + keep_state_and_data + since changing states from a state enter call + is not possible anyway.

Note the fact that you can use diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 34507bfd1c..f7dc0050b3 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -143,7 +143,7 @@ timeout_action() | reply_action(). -type timeout_action() :: - (Timeout :: event_timeout()) | % {timeout,Timeout} + (Time :: event_timeout()) | % {timeout,Time,Time} {'timeout', % Set the event_timeout option Time :: event_timeout(), EventContent :: term()} | {'timeout', % Set the event_timeout option @@ -327,7 +327,8 @@ %% Type validation functions -compile( {inline, - [callback_mode/1, state_enter/1, from/1, event_type/1]}). + [callback_mode/1, state_enter/1, + event_type/1, from/1, timeout_event_type/1]}). %% callback_mode(CallbackMode) -> case CallbackMode of @@ -344,23 +345,26 @@ state_enter(StateEnter) -> false end. %% -from({Pid,_}) when is_pid(Pid) -> true; -from(_) -> false. -%% -event_type({call,From}) -> - from(From); event_type(Type) -> case Type of {call,From} -> from(From); + %% cast -> true; info -> true; - timeout -> true; - state_timeout -> true; internal -> true; - {timeout,_} -> true; - _ -> false + _ -> timeout_event_type(Type) + end. +%% +from({Pid,_}) when is_pid(Pid) -> true; +from(_) -> false. +%% +timeout_event_type(Type) -> + case Type of + timeout -> true; + state_timeout -> true; + {timeout,_Name} -> true; + _ -> false end. - -define( @@ -1178,12 +1182,6 @@ loop_event_result( [Event|Events]) end. --compile({inline, [hibernate_in_trans_opts/1]}). -hibernate_in_trans_opts(false) -> - (#trans_opts{})#trans_opts.hibernate; -hibernate_in_trans_opts(#trans_opts{hibernate = Hibernate}) -> - Hibernate. - %% Ensure that Actions are a list loop_event_actions( Parent, Debug, S, @@ -1216,10 +1214,16 @@ loop_event_actions_list( S#state{ state = NextState, data = NewerData, - hibernate = TransOpts#trans_opts.hibernate}, + hibernate = hibernate_in_trans_opts(TransOpts)}, [Event|Events]) end. +-compile({inline, [hibernate_in_trans_opts/1]}). +hibernate_in_trans_opts(false) -> + (#trans_opts{})#trans_opts.hibernate; +hibernate_in_trans_opts(#trans_opts{hibernate = Hibernate}) -> + Hibernate. + parse_actions(false, Debug, S, Actions) -> parse_actions(true, Debug, S, Actions, #trans_opts{}); parse_actions(TransOpts, Debug, S, Actions) -> @@ -1335,15 +1339,15 @@ parse_actions_next_event( parse_actions_timeout( StateCall, Debug, S, Actions, TransOpts, - {TimerType,Time,TimerMsg,TimerOpts} = AbsoluteTimeout) -> + {TimeoutType,Time,TimerMsg,TimerOpts} = AbsoluteTimeout) -> %% - case classify_timer(Time, listify(TimerOpts)) of + case classify_timeout(TimeoutType, Time, listify(TimerOpts)) of absolute -> parse_actions_timeout_add( StateCall, Debug, S, Actions, TransOpts, AbsoluteTimeout); relative -> - RelativeTimeout = {TimerType,Time,TimerMsg}, + RelativeTimeout = {TimeoutType,Time,TimerMsg}, parse_actions_timeout_add( StateCall, Debug, S, Actions, TransOpts, RelativeTimeout); @@ -1355,8 +1359,8 @@ parse_actions_timeout( end; parse_actions_timeout( StateCall, Debug, S, Actions, TransOpts, - {_,Time,_} = RelativeTimeout) -> - case classify_timer(Time, []) of + {TimeoutType,Time,_} = RelativeTimeout) -> + case classify_timeout(TimeoutType, Time, []) of relative -> parse_actions_timeout_add( StateCall, Debug, S, Actions, @@ -1369,14 +1373,16 @@ parse_actions_timeout( end; parse_actions_timeout( StateCall, Debug, S, Actions, TransOpts, - Timeout) -> - case classify_timer(Timeout, []) of + Time) -> + case classify_timeout(timeout, Time, []) of relative -> + RelativeTimeout = {timeout,Time,Time}, parse_actions_timeout_add( - StateCall, Debug, S, Actions, TransOpts, Timeout); + StateCall, Debug, S, Actions, + TransOpts, RelativeTimeout); badarg -> [error, - {bad_action_from_state_function,Timeout}, + {bad_action_from_state_function,Time}, ?STACKTRACE(), Debug] end. @@ -1662,10 +1668,15 @@ call_state_function( %% -> absolute | relative | badarg -classify_timer(Time, Opts) -> - classify_timer(Time, Opts, false). -%% -classify_timer(Time, [], Abs) -> +classify_timeout(TimeoutType, Time, Opts) -> + case timeout_event_type(TimeoutType) of + true -> + classify_time(false, Time, Opts); + false -> + badarg + end. + +classify_time(Abs, Time, []) -> case Abs of true when is_integer(Time); @@ -1678,9 +1689,9 @@ classify_timer(Time, [], Abs) -> _ -> badarg end; -classify_timer(Time, [{abs,Abs}|Opts], _) when is_boolean(Abs) -> - classify_timer(Time, Opts, Abs); -classify_timer(_, Opts, _) when is_list(Opts) -> +classify_time(_, Time, [{abs,Abs}|Opts]) when is_boolean(Abs) -> + classify_time(Abs, Time, Opts); +classify_time(_, _, Opts) when is_list(Opts) -> badarg. %% Stop and start timers as well as create timeout zero events @@ -1711,15 +1722,7 @@ parse_timers( {TimerType,Time,TimerMsg} -> parse_timers( TimerRefs, Timers, TimeoutsR, Seen, TimeoutEvents, - TimerType, Time, TimerMsg, []); - 0 -> - parse_timers( - TimerRefs, Timers, TimeoutsR, Seen, TimeoutEvents, - timeout, zero, 0, []); - Time -> - parse_timers( - TimerRefs, Timers, TimeoutsR, Seen, TimeoutEvents, - timeout, Time, Time, []) + TimerType, Time, TimerMsg, []) end. parse_timers( diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 3f48fe1590..053233df9b 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -60,7 +60,8 @@ tcs(start) -> tcs(stop) -> [stop1, stop2, stop3, stop4, stop5, stop6, stop7, stop8, stop9, stop10]; tcs(abnormal) -> - [abnormal1, abnormal1clean, abnormal1dirty, abnormal2]; + [abnormal1, abnormal1clean, abnormal1dirty, + abnormal2, abnormal3, abnormal4]; tcs(sys) -> [sys1, call_format_status, error_format_status, terminate_crash_format, @@ -524,6 +525,43 @@ abnormal2(Config) -> process_flag(trap_exit, OldFl), ok = verify_empty_msgq(). +%% Check that bad return actions makes the stm crash. Note that we must +%% trap exit since we must link to get the real bad_return_ error +abnormal3(Config) -> + OldFl = process_flag(trap_exit, true), + {ok,Pid} = gen_statem:start_link(?MODULE, start_arg(Config, []), []), + + %% bad return value in the gen_statem loop + {{{bad_action_from_state_function,badaction},_},_} = + ?EXPECT_FAILURE(gen_statem:call(Pid, badaction), Reason), + receive + {'EXIT',Pid,{{bad_action_from_state_function,badaction},_}} -> ok + after 5000 -> + ct:fail(gen_statem_did_not_die) + end, + + process_flag(trap_exit, OldFl), + ok = verify_empty_msgq(). + +%% Check that bad timeout actions makes the stm crash. Note that we must +%% trap exit since we must link to get the real bad_return_ error +abnormal4(Config) -> + OldFl = process_flag(trap_exit, true), + {ok,Pid} = gen_statem:start_link(?MODULE, start_arg(Config, []), []), + + %% bad return value in the gen_statem loop + BadTimeout = {badtimeout,4711,ouch}, + {{{bad_action_from_state_function,BadTimeout},_},_} = + ?EXPECT_FAILURE(gen_statem:call(Pid, BadTimeout), Reason), + receive + {'EXIT',Pid,{{bad_action_from_state_function,BadTimeout},_}} -> ok + after 5000 -> + ct:fail(gen_statem_did_not_die) + end, + + process_flag(trap_exit, OldFl), + ok = verify_empty_msgq(). + shutdown(Config) -> process_flag(trap_exit, true), @@ -1806,10 +1844,12 @@ idle(cast, {connect,Pid}, Data) -> idle({call,From}, connect, Data) -> gen_statem:reply(From, accept), {next_state,wfor_conf,Data,infinity}; % NoOp timeout just to test API -idle(cast, badreturn, _Data) -> - badreturn; idle({call,_From}, badreturn, _Data) -> badreturn; +idle({call,_From}, badaction, Data) -> + {keep_state, Data, [badaction]}; +idle({call,_From}, {badtimeout,_,_} = BadTimeout, Data) -> + {keep_state, Data, BadTimeout}; idle({call,From}, {delayed_answer,T}, Data) -> receive after T -> -- cgit v1.2.3