diff options
author | Raimo Niskanen <raimo@erlang.org> | 2019-09-09 14:34:26 +0200 |
---|---|---|
committer | Raimo Niskanen <raimo@erlang.org> | 2019-09-09 14:34:26 +0200 |
commit | 9dbdeb852c54a4633c4a899c7be198d4bb8aa3ba (patch) | |
tree | 6778f52bd6950a675a84ade03cb676cf54318a34 | |
parent | 253349fd37db625a287e276ee1fc32bbb6638076 (diff) | |
parent | be7de82343a71a4e5e30606fdcb34697218c4c77 (diff) | |
download | erlang-9dbdeb852c54a4633c4a899c7be198d4bb8aa3ba.tar.gz |
Merge branch 'raimo/stdlib/gen_statem-improve-timers/OTP-15510' into maint
* raimo/stdlib/gen_statem-improve-timers/OTP-15510:
Update documentation after review
Change data structure for Timers
Implement timeout cancel and update
Improve sys:log of timeouts
Log time-outs in crash and get_status
-rw-r--r-- | lib/stdlib/doc/src/gen_statem.xml | 78 | ||||
-rw-r--r-- | lib/stdlib/doc/src/sys.xml | 28 | ||||
-rw-r--r-- | lib/stdlib/src/gen_statem.erl | 408 | ||||
-rw-r--r-- | lib/stdlib/src/sys.erl | 2 | ||||
-rw-r--r-- | lib/stdlib/test/gen_statem_SUITE.erl | 116 | ||||
-rw-r--r-- | system/doc/design_principles/statem.xml | 146 |
6 files changed, 570 insertions, 208 deletions
diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 5513b7d649..0e35bd3db3 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -57,19 +57,37 @@ containing detailed facts that may rot by age. </p> <note> - <p> - This behavior appeared in Erlang/OTP 19.0. - In OTP 19.1 a backwards incompatible change of - the return tuple from - <seealso marker="#Module:init/1"><c>Module:init/1</c></seealso> - was made and the mandatory callback function - <seealso marker="#Module:callback_mode/0"> - <c>Module:callback_mode/0</c> - </seealso> - was introduced. In OTP 20.0 the - <seealso marker="#type-generic_timeout"><c>generic timeouts</c></seealso> - were added. - </p> + <list type="bulleted"> + <item>This behavior appeared in Erlang/OTP 19.0.</item> + <item> + In OTP 19.1 a backwards incompatible change of + the return tuple from + <seealso marker="#Module:init/1"><c>Module:init/1</c></seealso> + was made and the mandatory callback function + <seealso marker="#Module:callback_mode/0"> + <c>Module:callback_mode/0</c> + </seealso> + was introduced. + </item> + <item> + In OTP 20.0 + <seealso marker="#type-generic_timeout"> + generic time-outs + </seealso> + were added. + </item> + <item> + In OTP 22.1 time-out content + <seealso marker="#type-timeout_update_action"> + <c>update</c> + </seealso> + and explicit time-out + <seealso marker="#type-timeout_cancel_action"> + <c>cancel</c> + </seealso> + were added. + </item> + </list> </note> <p> <c>gen_statem</c> has got the same features that @@ -653,7 +671,7 @@ handle_event(_, _, State, Data) -> <name name="timeout_event_type"/> <desc> <p> - There are 3 types of timeout events that the state machine + There are 3 types of time-out events that the state machine can generate for itself with the corresponding <seealso marker="#type-timeout_action">timeout_action()</seealso>s. </p> @@ -1189,7 +1207,7 @@ handle_event(_, _, State, Data) -> <seealso marker="#enter_loop/5"><c>enter_loop/5,6</c></seealso>. </p> <p> - These timeout actions sets timeout + These time-out actions sets time-out <seealso marker="#type-transition_option">transition options</seealso>. </p> <taglist> @@ -1242,6 +1260,36 @@ handle_event(_, _, State, Data) -> </desc> </datatype> <datatype> + <name name="timeout_cancel_action" since="OTP @OTP-15510@"/> + <desc> + <p> + This is a shorter and clearer form of + <seealso marker="#type-timeout_action"> + timeout_action() + </seealso> + with <c>Time = infinity</c> which cancels a time-out. + </p> + </desc> + </datatype> + <datatype> + <name name="timeout_update_action" since="OTP @OTP-15510@"/> + <desc> + <p> + Updates a time-out with a new <c>EventContent</c>. + See + <seealso marker="#type-timeout_action"> + timeout_action() + </seealso> + for how to start a time-out. + </p> + <p> + If no time-out of the same type is active instead + insert the time-out event just like when starting + a time-out with relative <c>Time = 0</c>. + </p> + </desc> + </datatype> + <datatype> <name name="reply_action"/> <desc> <p> diff --git a/lib/stdlib/doc/src/sys.xml b/lib/stdlib/doc/src/sys.xml index e22ca89ef5..ebea054fff 100644 --- a/lib/stdlib/doc/src/sys.xml +++ b/lib/stdlib/doc/src/sys.xml @@ -239,6 +239,34 @@ </item> <tag> <c> + {start_timer,<anno>Action</anno>,<anno>State</anno>} + </c> + </tag> + <item> + <p> + Is produced by <c>gen_statem</c> + when the action <c><anno>Action</anno></c> + starts a timer in state <c><anno>State</anno></c>. + </p> + </item> + <tag> + <c> + {insert_timeout,<anno>Event</anno>,<anno>State</anno>} + </c> + </tag> + <item> + <p> + Is produced by <c>gen_statem</c> when a timeout zero action + inserts event <c><anno>Event</anno></c> + in state <c><anno>State</anno></c>. + </p> + <p> + <c><anno>Event</anno></c> is + an <c>{EventType,EventContent}</c> tuple. + </p> + </item> + <tag> + <c> {enter,<anno>State</anno>} </c> </tag> diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 49911eac2c..ed25799cc2 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -176,7 +176,17 @@ {'state_timeout', % Set the state_timeout option Time :: state_timeout(), EventContent :: term(), - Options :: (timeout_option() | [timeout_option()])}. + Options :: (timeout_option() | [timeout_option()])} | + timeout_cancel_action() | + timeout_update_action(). +-type timeout_cancel_action() :: + {'timeout', 'cancel'} | + {{'timeout', Name :: term()}, 'cancel'} | + {'state_timeout', 'cancel'}. +-type timeout_update_action() :: + {'timeout', 'update', EventContent :: term()} | + {{'timeout', Name :: term()}, 'update', EventContent :: term()} | + {'state_timeout', 'update', EventContent :: term()}. -type reply_action() :: {'reply', % Reply to a caller From :: from(), Reply :: term()}. @@ -420,11 +430,9 @@ timeout_event_type(Type) -> {state_data = {undefined,undefined} :: {State :: term(),Data :: term()}, postponed = [] :: [{event_type(),term()}], - timers = {#{},#{}} :: - {%% TimerRef => TimeoutType - TimerRefs :: #{reference() => timeout_event_type()}, - %% TimeoutType => TimerRef - TimeoutTypes :: #{timeout_event_type() => reference()}}, + timers = #{} :: + #{TimeoutType :: timeout_event_type() => + {TimerRef :: reference(), TimeoutMsg :: term()}}, hibernate = false :: boolean() }). @@ -807,13 +815,14 @@ format_status( Opt, [PDict,SysState,Parent,Debug, {#params{name = Name} = P, - #state{postponed = Postponed} = S}]) -> + #state{postponed = Postponed, timers = Timers} = S}]) -> Header = gen:format_status_header("Status for state machine", Name), Log = sys:get_log(Debug), [{header,Header}, {data, [{"Status",SysState}, {"Parent",Parent}, + {"Time-outs",list_timeouts(Timers)}, {"Logged Events",Log}, {"Postponed",Postponed}]} | case format_status(Opt, PDict, update_parent(P, Parent), S) of @@ -860,6 +869,14 @@ print_event(Dev, SystemEvent, Name) -> io:format( Dev, "*DBG* ~tp enter in state ~tp~n", [Name,State]); + {start_timer,Action,State} -> + io:format( + Dev, "*DBG* ~tp start_timer ~tp in state ~tp~n", + [Name,Action,State]); + {insert_timeout,Event,State} -> + io:format( + Dev, "*DBG* ~tp insert_timeout ~tp in state ~tp~n", + [Name,Event,State]); {terminate,Reason,State} -> io:format( Dev, "*DBG* ~tp terminate ~tp in state ~tp~n", @@ -945,15 +962,12 @@ loop_receive( {'$gen_cast',Cast} -> loop_receive_result(P, Debug, S, {cast,Cast}); %% - {timeout,TimerRef,TimeoutMsg} -> - {TimerRefs,TimeoutTypes} = S#state.timers, - case TimerRefs of - #{TimerRef := TimeoutType} -> + {timeout,TimerRef,TimeoutType} -> + case S#state.timers of + #{TimeoutType := {TimerRef,TimeoutMsg}} = Timers -> %% Our timer - Timers = - {maps:remove(TimerRef, TimerRefs), - maps:remove(TimeoutType, TimeoutTypes)}, - S_1 = S#state{timers = Timers}, + Timers_1 = maps:remove(TimeoutType, Timers), + S_1 = S#state{timers = Timers_1}, loop_receive_result( P, Debug, S_1, {TimeoutType,TimeoutMsg}); #{} -> @@ -1514,20 +1528,20 @@ loop_actions_timeout( true -> case listify(TimeoutOpts) of %% Optimization cases - [] when ?relative_timeout(Time) -> - RelativeTimeout = {TimeoutType,Time,TimeoutMsg}, + [{abs,true}] when ?absolute_timeout(Time) -> loop_actions_list( P, Debug, S, Q, NextState_NewData, NextEventsR, Hibernate, - [RelativeTimeout|TimeoutsR], Postpone, + [Timeout|TimeoutsR], Postpone, CallEnter, StateCall, Actions); - [{abs,true}] when ?absolute_timeout(Time) -> + [{abs,false}] when ?relative_timeout(Time) -> + RelativeTimeout = {TimeoutType,Time,TimeoutMsg}, loop_actions_list( P, Debug, S, Q, NextState_NewData, NextEventsR, Hibernate, - [Timeout|TimeoutsR], Postpone, + [RelativeTimeout|TimeoutsR], Postpone, CallEnter, StateCall, Actions); - [{abs,false}] when ?relative_timeout(Time) -> + [] when ?relative_timeout(Time) -> RelativeTimeout = {TimeoutType,Time,TimeoutMsg}, loop_actions_list( P, Debug, S, Q, NextState_NewData, @@ -1544,14 +1558,13 @@ loop_actions_timeout( [Timeout|TimeoutsR], Postpone, CallEnter, StateCall, Actions); false when ?relative_timeout(Time) -> - RelativeTimeout = - {TimeoutType,Time,TimeoutMsg}, + RelativeTimeout = {TimeoutType,Time,TimeoutMsg}, loop_actions_list( P, Debug, S, Q, NextState_NewData, NextEventsR, Hibernate, [RelativeTimeout|TimeoutsR], Postpone, CallEnter, StateCall, Actions); - badarg -> + _ -> terminate( error, {bad_action_from_state_function,Timeout}, @@ -1576,10 +1589,12 @@ loop_actions_timeout( P, Debug, S, Q, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, Postpone, CallEnter, StateCall, Actions, - {TimeoutType,Time,_} = Timeout) -> + {TimeoutType,Time,_TimeoutMsg} = Timeout) -> %% case timeout_event_type(TimeoutType) of - true when ?relative_timeout(Time) -> + true + when ?relative_timeout(Time); + Time =:= update -> loop_actions_list( P, Debug, S, Q, NextState_NewData, NextEventsR, Hibernate, @@ -1598,15 +1613,40 @@ loop_actions_timeout( loop_actions_timeout( P, Debug, S, Q, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, Postpone, - CallEnter, StateCall, Actions, Time) -> + CallEnter, StateCall, Actions, + {TimeoutType,cancel} = Action) -> + %% + case timeout_event_type(TimeoutType) of + true -> + Timeout = {TimeoutType,infinity,undefined}, + loop_actions_list( + P, Debug, S, Q, NextState_NewData, + NextEventsR, Hibernate, + [Timeout|TimeoutsR], Postpone, + CallEnter, StateCall, Actions); + false -> + terminate( + error, + {bad_action_from_state_function,Action}, + ?STACKTRACE(), P, Debug, + S#state{ + state_data = NextState_NewData, + hibernate = Hibernate}, + Q) + end; +loop_actions_timeout( + P, Debug, S, Q, NextState_NewData, + NextEventsR, Hibernate, TimeoutsR, Postpone, + CallEnter, StateCall, Actions, + Time) -> %% if ?relative_timeout(Time) -> - RelativeTimeout = {timeout,Time,Time}, + Timeout = {timeout,Time,Time}, loop_actions_list( P, Debug, S, Q, NextState_NewData, NextEventsR, Hibernate, - [RelativeTimeout|TimeoutsR], Postpone, + [Timeout|TimeoutsR], Postpone, CallEnter, StateCall, Actions); true -> terminate( @@ -1683,23 +1723,20 @@ loop_state_transition( %% State transition to the same state %% loop_keep_state( - P, Debug, #state{timers = {TimerRefs,TimeoutTypes} = Timers} = S, + P, Debug, #state{timers = Timers} = S, Events, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, Postponed) -> %% %% Cancel event timeout %% - case TimeoutTypes of - %% Optimization - %% - only cancel timer when there is a timer to cancel - #{timeout := TimerRef} -> + case Timers of + #{timeout := {TimerRef,_TimeoutMsg}} -> %% Event timeout active loop_next_events( P, Debug, S, Events, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, Postponed, - cancel_timer_by_ref_and_type( - TimerRef, timeout, TimerRefs, TimeoutTypes)); + cancel_timer(timeout, TimerRef, Timers)); _ -> %% No event timeout active loop_next_events( @@ -1741,34 +1778,32 @@ loop_state_change( end. %% loop_state_change( - P, Debug, #state{timers = {TimerRefs,TimeoutTypes} = Timers} = S, + P, Debug, #state{timers = Timers} = S, Events, NextState_NewData, NextEventsR, Hibernate, TimeoutsR) -> %% %% Cancel state and event timeout %% - case TimeoutTypes of + case Timers of %% Optimization - %% - only cancel timeout when there is an active timeout + %% - only cancel timeout when it is active %% - #{state_timeout := TimerRef} -> + #{state_timeout := {TimerRef,_TimeoutMsg}} -> %% State timeout active %% - cancel event timeout too since it is faster than inspecting loop_next_events( P, Debug, S, Events, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, [], - cancel_timer_by_type( + cancel_timer( timeout, - cancel_timer_by_ref_and_type( - TimerRef, state_timeout, TimerRefs, TimeoutTypes))); - #{timeout := TimerRef} -> + cancel_timer(state_timeout, TimerRef, Timers))); + #{timeout := {TimerRef,_TimeoutMsg}} -> %% Event timeout active but not state timeout %% - cancel event timeout only loop_next_events( P, Debug, S, Events, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, [], - cancel_timer_by_ref_and_type( - TimerRef, timeout, TimerRefs, TimeoutTypes)); + cancel_timer(timeout, TimerRef, Timers)); _ -> %% No state nor event timeout active. loop_next_events( @@ -1778,7 +1813,7 @@ loop_state_change( end. %% Continue state transition with processing of -%% inserted events and timeout events +%% timeouts and inserted events %% loop_next_events( P, Debug, S, @@ -1786,7 +1821,7 @@ loop_next_events( NextEventsR, Hibernate, [], Postponed, Timers) -> %% - %% Optimization when there are no timeout actions + %% Optimization when there are no timeouts %% hence no timeout zero events to append to Events %% - avoid loop_timeouts loop_done( @@ -1811,7 +1846,8 @@ loop_next_events( NextEventsR, Hibernate, TimeoutsR, Postponed, Timers, Seen, TimeoutEvents). -%% Continue state transition with processing of timeout events +%% Continue state transition with processing of timeouts +%% and finally inserted events %% loop_timeouts( P, Debug, S, @@ -1819,6 +1855,8 @@ loop_timeouts( NextEventsR, Hibernate, [], Postponed, Timers, _Seen, TimeoutEvents) -> %% + %% End of timeouts + %% S_1 = S#state{ state_data = NextState_NewData, @@ -1854,37 +1892,28 @@ loop_timeouts( NextEventsR, Hibernate, [Timeout|TimeoutsR], Postponed, Timers, Seen, TimeoutEvents) -> %% - case Timeout of - {TimeoutType,Time,TimeoutMsg} -> - %% Relative timeout - case Seen of - #{TimeoutType := _} -> - %% Type seen before - ignore - loop_timeouts( - P, Debug, S, - Events, NextState_NewData, - NextEventsR, Hibernate, TimeoutsR, Postponed, - Timers, Seen, TimeoutEvents); - #{} -> - loop_timeouts( + TimeoutType = element(1, Timeout), + case Seen of + #{TimeoutType := _} -> + %% Type seen before - ignore + loop_timeouts( + P, Debug, S, + Events, NextState_NewData, + NextEventsR, Hibernate, TimeoutsR, Postponed, + Timers, Seen, TimeoutEvents); + #{} -> + case Timeout of + {_,Time,TimeoutMsg} -> + %% Relative timeout or update + loop_timeouts_start( P, Debug, S, Events, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, Postponed, Timers, Seen, TimeoutEvents, - TimeoutType, Time, TimeoutMsg, []) - end; - {TimeoutType,Time,TimeoutMsg,TimeoutOpts} -> - %% Absolute timeout - case Seen of - #{TimeoutType := _} -> - %% Type seen before - ignore - loop_timeouts( - P, Debug, S, - Events, NextState_NewData, - NextEventsR, Hibernate, TimeoutsR, Postponed, - Timers, Seen, TimeoutEvents); - #{} -> - loop_timeouts( + TimeoutType, Time, TimeoutMsg, []); + {_,Time,TimeoutMsg,TimeoutOpts} -> + %% Absolute timeout + loop_timeouts_start( P, Debug, S, Events, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, Postponed, @@ -1892,8 +1921,10 @@ loop_timeouts( TimeoutType, Time, TimeoutMsg, listify(TimeoutOpts)) end end. + +%% Loop helper to start or restart a timeout %% -loop_timeouts( +loop_timeouts_start( P, Debug, S, Events, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, Postponed, @@ -1920,51 +1951,79 @@ loop_timeouts( NextEventsR, Hibernate, TimeoutsR, Postponed, Timers, Seen, [{TimeoutType,TimeoutMsg}|TimeoutEvents], TimeoutType); + update -> + loop_timeouts_update( + P, Debug, S, + Events, NextState_NewData, + NextEventsR, Hibernate, TimeoutsR, Postponed, + Timers, Seen, TimeoutEvents, + TimeoutType, TimeoutMsg); _ -> %% (Re)start the timer TimerRef = - erlang:start_timer(Time, self(), TimeoutMsg, TimeoutOpts), - {TimerRefs,TimeoutTypes} = Timers, - case TimeoutTypes of - #{TimeoutType := OldTimerRef} -> - %% Cancel the running timer, - %% update the timeout type, - %% insert the new timer ref, - %% and remove the old timer ref - Timers_1 = - {maps:remove( - OldTimerRef, - TimerRefs#{TimerRef => TimeoutType}), - TimeoutTypes#{TimeoutType := TimerRef}}, - cancel_timer(OldTimerRef), - loop_timeouts( - P, Debug, S, - Events, NextState_NewData, + erlang:start_timer(Time, self(), TimeoutType, TimeoutOpts), + case Debug of + ?not_sys_debug -> + loop_timeouts_register( + P, Debug, S, Events, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, Postponed, - Timers_1, Seen#{TimeoutType => true}, TimeoutEvents); - #{} -> - %% Insert the new timer type and ref - Timers_1 = - {TimerRefs#{TimerRef => TimeoutType}, - TimeoutTypes#{TimeoutType => TimerRef}}, - loop_timeouts( - P, Debug, S, - Events, NextState_NewData, + Timers, Seen, TimeoutEvents, + TimeoutType, TimerRef, TimeoutMsg); + _ -> + {State,_Data} = NextState_NewData, + Debug_1 = + sys_debug( + Debug, P#params.name, + {start_timer, + {TimeoutType,Time,TimeoutMsg,TimeoutOpts}, + State}), + loop_timeouts_register( + P, Debug_1, S, Events, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, Postponed, - Timers_1, Seen#{TimeoutType => true}, TimeoutEvents) + Timers, Seen, TimeoutEvents, + TimeoutType, TimerRef, TimeoutMsg) end end. +%% Loop helper to register a newly started timer +%% and to cancel any running timer +%% +loop_timeouts_register( + P, Debug, S, Events, NextState_NewData, + NextEventsR, Hibernate, TimeoutsR, Postponed, + Timers, Seen, TimeoutEvents, + TimeoutType, TimerRef, TimeoutMsg) -> + %% + case Timers of + #{TimeoutType := {OldTimerRef,_OldTimeoutMsg}} -> + %% Cancel the running timer, + %% and update timer type and ref + cancel_timer(OldTimerRef), + Timers_1 = Timers#{TimeoutType := {TimerRef,TimeoutMsg}}, + loop_timeouts( + P, Debug, S, + Events, NextState_NewData, + NextEventsR, Hibernate, TimeoutsR, Postponed, + Timers_1, Seen#{TimeoutType => true}, TimeoutEvents); + #{} -> + %% Insert the new timer type and ref + Timers_1 = Timers#{TimeoutType => {TimerRef,TimeoutMsg}}, + loop_timeouts( + P, Debug, S, + Events, NextState_NewData, + NextEventsR, Hibernate, TimeoutsR, Postponed, + Timers_1, Seen#{TimeoutType => true}, TimeoutEvents) + end. + %% Loop helper to cancel a timeout %% loop_timeouts_cancel( P, Debug, S, Events, NextState_NewData, NextEventsR, Hibernate, TimeoutsR, Postponed, - {TimerRefs,TimeoutTypes} = Timers, Seen, TimeoutEvents, - TimeoutType) -> + Timers, Seen, TimeoutEvents, TimeoutType) -> %% This function body should have been: - %% Timers_1 = cancel_timer_by_type(TimeoutType, Timers), + %% Timers_1 = cancel_timer(TimeoutType, Timers), %% loop_timeouts( %% P, Debug, S, %% Events, NextState_NewData, @@ -1974,14 +2033,12 @@ loop_timeouts_cancel( %% Explicitly separate cases to get separate code paths for when %% the map key exists vs. not, since otherwise the external call %% to erlang:cancel_timer/1 and to map:remove/2 within - %% cancel_timer_by_type/2 would cause all live registers + %% cancel_timer/2 would cause all live registers %% to be saved to and restored from the stack also for %% the case when the map key TimeoutType does not exist - case TimeoutTypes of - #{TimeoutType := TimerRef} -> - Timers_1 = - cancel_timer_by_ref_and_type( - TimerRef, TimeoutType, TimerRefs, TimeoutTypes), + case Timers of + #{TimeoutType := {TimerRef,_TimeoutMsg}} -> + Timers_1 = cancel_timer(TimeoutType, TimerRef, Timers), loop_timeouts( P, Debug, S, Events, NextState_NewData, @@ -1995,6 +2052,36 @@ loop_timeouts_cancel( Timers, Seen#{TimeoutType => true}, TimeoutEvents) end. +%% Loop helper to update the timeout message, +%% or insert an event if no timer is running +%% +loop_timeouts_update( + P, Debug, S, + Events, NextState_NewData, + NextEventsR, Hibernate, TimeoutsR, Postponed, + Timers, Seen, TimeoutEvents, + TimeoutType, TimeoutMsg) -> + %% + case Timers of + #{TimeoutType := {TimerRef,_OldTimeoutMsg}} -> + Timers_1 = Timers#{TimeoutType := {TimerRef,TimeoutMsg}}, + loop_timeouts( + P, Debug, S, + Events, NextState_NewData, + NextEventsR, Hibernate, TimeoutsR, Postponed, + Timers_1, Seen#{TimeoutType => true}, + TimeoutEvents); + #{} -> + TimeoutEvents_1 = + [{TimeoutType,TimeoutMsg}|TimeoutEvents], + loop_timeouts( + P, Debug, S, + Events, NextState_NewData, + NextEventsR, Hibernate, TimeoutsR, Postponed, + Timers, Seen#{TimeoutType => true}, + TimeoutEvents_1) + end. + %% Continue state transition with prepending timeout zero events %% before event queue reversal i.e appending timeout zero events %% @@ -2056,13 +2143,13 @@ parse_timeout_opts_abs(Opts, Abs) -> %% Enqueue immediate timeout events (timeout 0 events) %% -%% Event timer timeout 0 events gets special treatment since -%% an event timer is cancelled by any received event, -%% so if there are enqueued events before the event timer -%% timeout 0 event - the event timer is cancelled hence no event. +%% Event timeout 0 events gets special treatment since +%% an event timeout is cancelled by any received event, +%% so if there are enqueued events before the event +%% timeout 0 event - the event timeout is cancelled hence no event. %% %% Other (state_timeout and {timeout,Name}) timeout 0 events -%% that are after an event timer timeout 0 event are considered to +%% that occur after an event timer timeout 0 event are considered to %% belong to timers that were started after the event timer %% timeout 0 event fired, so they do not cancel the event timer. %% @@ -2079,7 +2166,8 @@ prepend_timeout_events( {State,_Data} = S#state.state_data, Debug_1 = sys_debug( - Debug, P#params.name, {in,TimeoutEvent,State}), + Debug, P#params.name, + {insert_timeout,TimeoutEvent,State}), prepend_timeout_events( P, Debug_1, S, TimeoutEvents, [TimeoutEvent]) end; @@ -2099,7 +2187,8 @@ prepend_timeout_events( {State,_Data} = S#state.state_data, Debug_1 = sys_debug( - Debug, P#params.name, {in,TimeoutEvent,State}), + Debug, P#params.name, + {insert_timeout,TimeoutEvent,State}), prepend_timeout_events( P, Debug_1, S, TimeoutEvents, [TimeoutEvent|EventsR]) end. @@ -2190,7 +2279,9 @@ error_info( name = Name, callback_mode = CallbackMode, state_enter = StateEnter} = P, - #state{postponed = Postponed} = S, + #state{ + postponed = Postponed, + timers = Timers} = S, Q) -> Log = sys:get_log(Debug), ?LOG_ERROR(#{label=>{gen_statem,terminate}, @@ -2200,6 +2291,7 @@ error_info( callback_mode=>CallbackMode, state_enter=>StateEnter, state=>format_status(terminate, get(), P, S), + timeouts=>list_timeouts(Timers), log=>Log, reason=>{Class,Reason,Stacktrace}, client_info=>client_stacktrace(Q)}, @@ -2238,6 +2330,7 @@ format_log(#{label:={gen_statem,terminate}, callback_mode:=CallbackMode, state_enter:=StateEnter, state:=FmtData, + timeouts:=Timeouts, log:=Log, reason:={Class,Reason,Stacktrace}, client_info:=ClientInfo}) -> @@ -2293,6 +2386,10 @@ format_log(#{label:={gen_statem,terminate}, [] -> ""; _ -> "** Stacktrace =~n** ~tp~n" end ++ + case Timeouts of + {0,_} -> ""; + _ -> "** Time-outs: ~p~n" + end ++ case Log of [] -> ""; _ -> "** Log =~n** ~tp~n" @@ -2317,6 +2414,10 @@ format_log(#{label:={gen_statem,terminate}, [] -> []; _ -> [error_logger:limit_term(FixedStacktrace)] end ++ + case Timeouts of + {0,_} -> []; + _ -> [error_logger:limit_term(Timeouts)] + end ++ case Log of [] -> []; _ -> [[error_logger:limit_term(T) || T <- Log]] @@ -2370,40 +2471,55 @@ listify(Item) -> [Item]. --define(cancel_timer(TimerRef), - case erlang:cancel_timer(TimerRef) of - false -> - %% No timer found and we have not seen the timeout message - receive - {timeout,(TimerRef),_} -> - ok - end; - _ -> - %% Timer was running - ok - end). - +-define( + cancel_timer(TimerRef), + case erlang:cancel_timer(TimerRef) of + false -> + %% No timer found and we have not seen the timeout message + receive + {timeout,(TimerRef),_} -> + ok + end; + _ -> + %% Timer was running + ok + end). +%% +%% Cancel timer and consume timeout message +%% -compile({inline, [cancel_timer/1]}). cancel_timer(TimerRef) -> ?cancel_timer(TimerRef). +-define( + cancel_timer(TimeoutType, TimerRef, Timers), + begin + ?cancel_timer(TimerRef), + maps:remove(begin TimeoutType end, begin Timers end) + end). +%% +%% Cancel timer and remove from Timers +%% +-compile({inline, [cancel_timer/3]}). +cancel_timer(TimeoutType, TimerRef, Timers) -> + ?cancel_timer(TimeoutType, TimerRef, Timers). + %% Cancel timer if running, otherwise no op %% %% Remove the timer from Timers --compile({inline, [cancel_timer_by_type/2]}). -cancel_timer_by_type(TimeoutType, {TimerRefs,TimeoutTypes} = Timers) -> - case TimeoutTypes of +-compile({inline, [cancel_timer/2]}). +cancel_timer(TimeoutType, Timers) -> + case Timers of #{TimeoutType := TimerRef} -> - ?cancel_timer(TimerRef), - {maps:remove(TimerRef, TimerRefs), - maps:remove(TimeoutType, TimeoutTypes)}; + ?cancel_timer(TimeoutType, TimerRef, Timers); #{} -> Timers end. --compile({inline, [cancel_timer_by_ref_and_type/4]}). -cancel_timer_by_ref_and_type( - TimerRef, TimeoutType, TimerRefs, TimeoutTypes) -> - ?cancel_timer(TimerRef), - {maps:remove(TimerRef, TimerRefs), - maps:remove(TimeoutType, TimeoutTypes)}. +%% Return a list of all pending timeouts +list_timeouts(Timers) -> + {maps:size(Timers), + maps:fold( + fun(TimeoutType, {_TimerRef,TimeoutMsg}, Acc) -> + [{TimeoutType,TimeoutMsg}|Acc] + end, [], Timers)}. diff --git a/lib/stdlib/src/sys.erl b/lib/stdlib/src/sys.erl index 6ff9aa33b4..93bf4743d2 100644 --- a/lib/stdlib/src/sys.erl +++ b/lib/stdlib/src/sys.erl @@ -51,6 +51,8 @@ | {'code_change', Event :: _, State :: _} | {'postpone', Event :: _, State :: _, NextState :: _} | {'consume', Event :: _, State :: _, NextState :: _} + | {'start_timer', Action :: _, State :: _} + | {'insert_timeout', Event :: _, State :: _} | {'enter', State :: _} | {'terminate', Reason :: _, State :: _} | term(). diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 16cf8f43f9..31808a915f 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2016-2018. All Rights Reserved. +%% Copyright Ericsson AB 2016-2019. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ suite() -> [{ct_hooks,[ts_install_cth]}, - {timetrap,{minutes,1}}]. + {timetrap,{seconds,10}}]. all() -> [{group, start}, @@ -38,7 +38,8 @@ all() -> {group, abnormal}, {group, abnormal_handle_event}, shutdown, stop_and_reply, state_enter, event_order, - state_timeout, event_types, generic_timers, code_change, + state_timeout, timeout_cancel_and_update, + event_types, generic_timers, code_change, {group, sys}, hibernate, auto_hibernate, enter_loop, {group, undef_callbacks}, undef_in_terminate]. @@ -518,10 +519,11 @@ abnormal2(Config) -> ?MODULE, start_arg(Config, []), [{debug,[log]}]), %% bad return value in the gen_statem loop - {{{bad_return_from_state_function,badreturn},_},_} = + Cause = bad_return_from_state_function, + {{{Cause,badreturn},_},_} = ?EXPECT_FAILURE(gen_statem:call(Pid, badreturn), Reason), receive - {'EXIT',Pid,{{bad_return_from_state_function,badreturn},_}} -> ok + {'EXIT',Pid,{{Cause,badreturn},_}} -> ok after 5000 -> ct:fail(gen_statem_did_not_die) end, @@ -538,10 +540,11 @@ abnormal3(Config) -> ?MODULE, start_arg(Config, []), [{debug,[log]}]), %% bad return value in the gen_statem loop - {{{bad_action_from_state_function,badaction},_},_} = + Cause = bad_action_from_state_function, + {{{Cause,badaction},_},_} = ?EXPECT_FAILURE(gen_statem:call(Pid, badaction), Reason), receive - {'EXIT',Pid,{{bad_action_from_state_function,badaction},_}} -> ok + {'EXIT',Pid,{{Cause,badaction},_}} -> ok after 5000 -> ct:fail(gen_statem_did_not_die) end, @@ -559,10 +562,11 @@ abnormal4(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), + Cause = bad_action_from_state_function, + {{{Cause,BadTimeout},_},_} = + ?EXPECT_FAILURE(gen_statem:call(Pid, {badtimeout,BadTimeout}), Reason), receive - {'EXIT',Pid,{{bad_action_from_state_function,BadTimeout},_}} -> ok + {'EXIT',Pid,{{Cause,BadTimeout},_}} -> ok after 5000 -> ct:fail(gen_statem_did_not_die) end, @@ -872,8 +876,9 @@ state_timeout(_Config) -> {reply,From,ok}} end}, - {ok,STM} = gen_statem:start_link(?MODULE, {map_statem,Machine,[]}, []), - sys:trace(STM, true), + {ok,STM} = + gen_statem:start_link( + ?MODULE, {map_statem,Machine,[]}, [{debug,[trace]}]), TRef = erlang:start_timer(1000, self(), kull), ok = gen_statem:call(STM, {go,500}), ok = gen_statem:call(STM, check), @@ -899,6 +904,88 @@ state_timeout(_Config) -> +timeout_cancel_and_update(_Config) -> + process_flag(trap_exit, true), + %% + Machine = + #{init => + fun () -> + {ok,start,0} + end, + start => + fun + ({call,From}, test, 0) -> + self() ! message_to_self, + {next_state, state1, From, + %% Verify that internal events goes before external + [{state_timeout,17,1}, + {next_event,internal,1}]} + end, + state1 => + fun + (internal, 1, _) -> + {keep_state_and_data, + [{state_timeout,cancel}, + {{timeout,a},17,1}]}; + (info, message_to_self, _) -> + {keep_state_and_data, + [{{timeout,a},update,a}]}; + ({timeout,a}, a, Data) -> + {next_state,state2,Data, + [{state_timeout,17,2}, + {next_event,internal,2}]} + end, + state2 => + fun + (internal, 2, _) -> + receive after 50 -> ok end, + %% Now state_timeout 17 should have triggered + {keep_state_and_data, + [{state_timeout,update,b}, + {timeout,17,2}]}; + (state_timeout, b, From) -> + {next_state,state3,3, + [{reply,From,ok}, + 17000]} + end, + state3 => + fun + ({call,From}, stop, 3) -> + {stop_and_reply, normal, + [{reply,From,ok}]} + end + }, + %% + {ok,STM} = + gen_statem:start_link( + ?MODULE, {map_statem,Machine,[]}, [{debug,[trace]}]), + ok = gen_statem:call(STM, test), + {status, STM, {module,gen_statem}, Info} = sys:get_status(STM), + ct:log("Status info: ~p~n", [Info]), + {_,Timeouts} = dig_data_tuple(Info), + {_, {1,[{timeout,17000}]}} = lists:keyfind("Time-outs", 1, Timeouts), + %% + ok = gen_statem:call(STM, stop), + receive + {'EXIT',STM,normal} -> + ok + after 500 -> + ct:fail(did_not_stop) + end, + %% + verify_empty_msgq(). + +dig_data_tuple([{data,_} = DataTuple|_]) -> DataTuple; +dig_data_tuple([H|T]) when is_list(H) -> + case dig_data_tuple(H) of + false -> dig_data_tuple(T); + DataTuple -> DataTuple + end; +dig_data_tuple([_|T]) -> dig_data_tuple(T); +dig_data_tuple([]) -> false. + + + %% Test that all event types can be sent with {next_event,EventType,_} event_types(_Config) -> process_flag(trap_exit, true), @@ -1042,7 +1129,8 @@ generic_timers(_Config) -> sys1(Config) -> {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), - {status, Pid, {module,gen_statem}, _} = sys:get_status(Pid), + {status, Pid, {module,gen_statem}, Info} = sys:get_status(Pid), + ct:log("Status info: ~p~n", [Info]), sys:suspend(Pid), Parent = self(), Tag = make_ref(), @@ -1892,7 +1980,7 @@ idle({call,_From}, badreturn, _Data) -> badreturn; idle({call,_From}, badaction, Data) -> {keep_state, Data, [badaction]}; -idle({call,_From}, {badtimeout,_,_} = BadTimeout, Data) -> +idle({call,_From}, {badtimeout,BadTimeout}, Data) -> {keep_state, Data, BadTimeout}; idle({call,From}, {delayed_answer,T}, Data) -> receive diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 81a0c617f1..360bf958ad 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -108,7 +108,9 @@ State(S) x Event(E) -> Actions(A), State(S')</pre> <item> Co-located callback code for each state, for all - <seealso marker="#Event Types"><em>Event Types</em></seealso> + <seealso marker="#Event Types and Event Content"> + <em>Event Types</em> + </seealso> (such as <em>call</em>, <em>cast</em> and <em>info</em>) </item> <item> @@ -136,7 +138,7 @@ State(S) x Event(E) -> Actions(A), State(S')</pre> (<seealso marker="#State Time-Outs">State Time-Outs</seealso>, <seealso marker="#Event Time-Outs">Event Time-Outs</seealso> and - <seealso marker="#Generic Time-Outs">Generic Time-outs</seealso> + <seealso marker="#Generic Time-Outs">Generic Time-Outs</seealso> (named time-outs)) </item> </list> @@ -353,9 +355,10 @@ State(S) x Event(E) -> Actions(A), State(S')</pre> </taglist> <p> The state is either the name of the function itself or an argument to it. - The other arguments are the <c>EventType</c> described in section - <seealso marker="#Event Types">Event Types</seealso>, - the event dependent <c>EventContent</c>, + The other arguments are the <c>EventType</c> + and the event dependent <c>EventContent</c>, + both described in section + <seealso marker="#Event Types and Event Content">Event Types and Event Content</seealso>, and the current server <c>Data</c>. </p> <p> @@ -561,34 +564,48 @@ State(S) x Event(E) -> Actions(A), State(S')</pre> </item> <tag> <seealso marker="stdlib:gen_statem#type-state_timeout"> - <c>{state_timeout, EventContent, Time}</c> + <c>{state_timeout, Time, EventContent}</c> + </seealso> + <br /> + <c>{state_timeout, Time, EventContent, Opts}</c><br /> + <seealso marker="stdlib:gen_statem#type-timeout_update_action"> + <c>{state_timeout, update, EventContent}</c> </seealso> <br /> - <c>{state_timeout, EventContent, Time, Opts}</c> + <seealso marker="stdlib:gen_statem#type-timeout_cancel_action"> + <c>{state_timeout, cancel}</c> + </seealso> </tag> <item> - Start a state time-out, read more in sections + Start, update or cancel a state time-out, read more in sections <seealso marker="#Time-Outs">Time-Outs</seealso> and <seealso marker="#State Time-Outs">State Time-Outs</seealso>. </item> <tag> <seealso marker="stdlib:gen_statem#type-generic_timeout"> - <c>{{timeout, Name}, EventContent, Time}</c> + <c>{{timeout, Name}, Time, EventContent}</c> + </seealso> + <br /> + <c>{{timeout, Name}, Time, EventContent, Opts}</c><br /> + <seealso marker="stdlib:gen_statem#type-timeout_update_action"> + <c>{{timeout, Name}, update, EventContent}</c> </seealso> <br /> - <c>{{timeout, Name}, EventContent, Time, Opts}</c> + <seealso marker="stdlib:gen_statem#type-timeout_cancel_action"> + <c>{{timeout, Name}, cancel}</c> + </seealso> </tag> <item> - Start a generic time-out, read more in sections + Start, update or cancel a generic time-out, read more in sections <seealso marker="#Time-Outs">Time-Outs</seealso> and <seealso marker="#Generic Time-Outs">Generic Time-Outs</seealso>. </item> <tag> <seealso marker="stdlib:gen_statem#type-event_timeout"> - <c>{timeout, EventContent, Time}</c> + <c>{timeout, Time, EventContent}</c> </seealso> <br /> - <c>{timeout, EventContent, Time, Opts}</c><br /> + <c>{timeout, Time, EventContent, Opts}</c><br /> <c>Time</c> </tag> <item> @@ -636,19 +653,30 @@ State(S) x Event(E) -> Actions(A), State(S')</pre> in the <c>gen_statem(3)</c> manual page for type <seealso marker="stdlib:gen_statem#type-transition_option"><c>transition_option()</c></seealso>. </p> + <p> + The different + <seealso marker="#Time-Outs">Time-Outs</seealso> and + <seealso marker="#Inserted Events"><c>next_event</c></seealso> + actions generate new events with corresponding + <seealso marker="#Event Types and Event Content"> + Event Types and Event Content + </seealso>. + </p> </section> <!-- =================================================================== --> <section> - <marker id="Event Types" /> - <title>Event Types</title> + <marker id="Event Types and Event Content" /> + <title>Event Types and Event Content</title> <p> Events are categorized in different <seealso marker="stdlib:gen_statem#type-event_type"><em>event types</em></seealso>. Events of all types are for a given state handled in the same callback function, and that function gets <c>EventType</c> and <c>EventContent</c> as arguments. + The meaning of the <c>EventContent</c> + depends on the <c>EventType</c>. </p> <p> The following is a complete list of <em>event types</em> and where @@ -662,7 +690,10 @@ State(S) x Event(E) -> Actions(A), State(S')</pre> </tag> <item> Generated by - <seealso marker="stdlib:gen_statem#cast/2"><c>gen_statem:cast</c></seealso>. + <seealso marker="stdlib:gen_statem#cast/2"> + <c>gen_statem:cast(ServerRef, Msg)</c> + </seealso> + where <c>Msg</c> becomes the <c>EventContent</c>. </item> <tag> <seealso marker="stdlib:gen_statem#type-external_event_type"> @@ -671,11 +702,17 @@ State(S) x Event(E) -> Actions(A), State(S')</pre> </tag> <item> Generated by - <seealso marker="stdlib:gen_statem#call/2"><c>gen_statem:call</c></seealso>, - where <c>From</c> is the reply address to use + <seealso marker="stdlib:gen_statem#call/2"> + <c>gen_statem:call(ServerRef, Request)</c> + </seealso> + where <c>Request</c> becomes the <c>EventContent</c>. + <c>From</c> is the reply address to use when replying either through the <em>transition action</em> - <c>{reply,From,Msg}</c> or by calling - <seealso marker="stdlib:gen_statem#reply/1"><c>gen_statem:reply</c></seealso>. + <c>{reply,From,Reply}</c> or by calling + <seealso marker="stdlib:gen_statem#reply/1"> + <c>gen_statem:reply(From, Reply)</c> + </seealso> + from the <em>callback module</em>. </item> <tag> <seealso marker="stdlib:gen_statem#type-external_event_type"> @@ -685,6 +722,7 @@ State(S) x Event(E) -> Actions(A), State(S')</pre> <item> Generated by any regular process message sent to the <c>gen_statem</c> process. + The process message becomes the <c>EventContent</c>. </item> <tag> <seealso marker="stdlib:gen_statem#type-timeout_event_type"> @@ -736,7 +774,7 @@ State(S) x Event(E) -> Actions(A), State(S')</pre> </tag> <item> Generated by <em>transition action</em> - <seealso marker="stdlib:gen_statem#type-enter_action"><c>{next_event,internal,EventContent}</c></seealso>. + <seealso marker="stdlib:gen_statem#type-action"><c>{next_event,internal,EventContent}</c></seealso>. All <em>event types</em> above can also be generated using the <c>next_event</c> action: <c>{next_event,EventType,EventContent}</c>. @@ -805,7 +843,7 @@ StateName(EventType, EventContent, Data) -> <section> <marker id="Time-Outs" /> - <title>Time-outs</title> + <title>Time-Outs</title> <p> Time-outs in <c>gen_statem</c> are started from a <seealso marker="#Transition Actions"> @@ -856,9 +894,9 @@ StateName(EventType, EventContent, Data) -> </item> </taglist> <p> - When a time-out is started any running time-out with the same tag, + When a time-out is started any running time-out of the same type; <c>state_timeout</c>, <c>{timeout, Name}</c> or <c>timeout</c>, - is cancelled, that is the time-out is restarted with the new time. + is cancelled, that is, the time-out is restarted with the new time. </p> <p> All time-outs has got an <c>EventContent</c> that is part of the @@ -881,6 +919,39 @@ StateName(EventType, EventContent, Data) -> The <c>EventContent</c> will in this case be ignored, so why not set it to <c>undefined</c>. </p> + <p> + A more explicit way to cancel a timer is to use a + <seealso marker="#Transition Actions"> + <em>transition action</em> + </seealso> + on the form + <seealso marker="stdlib:gen_statem#type-timeout_cancel_action"> + <c>{TimeoutType, cancel}</c> + </seealso> + which is a feature introduced in OTP 22.1. + </p> + </section> + <section> + <marker id="Updating a Time-Out" /> + <title>Updating a Time-Out</title> + <p> + While a time-out is running, its <c>EventContent</c> + can be updated using a + <seealso marker="#Transition Actions"> + <em>transition action</em> + </seealso> + on the form + <seealso marker="stdlib:gen_statem#type-timeout_update_action"> + <c>{TimeoutType, update, NewEventContent}</c> + </seealso> + which is a feature introduced in OTP 22.1. + </p> + <p> + If this feature is used while no such <c>TimeoutType</c> + is running then a time-out event is immediately delivered + as when starting a + <seealso marker="#Time-Out Zero">Time-Out Zero</seealso>. + </p> </section> <section> <marker id="Time-Out Zero" /> @@ -1212,10 +1283,12 @@ open(state_timeout, lock, Data) -> <p> The timer for a state time-out is automatically cancelled when the state machine does a <em>state change</em>. - 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 <c>infinity</c>. + </p> + <p> + You can restart, cancel or update a state time-out. + See section + <seealso marker="#Time-Outs">Time-Outs</seealso> + for details. </p> </section> @@ -1484,12 +1557,13 @@ locked( <p> An event time-out is cancelled by any other event so you either get some other event or the time-out event. It is therefore - not possible nor needed to cancel or restart an event time-out. - Whatever event you act on has already cancelled - the event time-out... + not possible nor needed to cancel, restart or update an event time-out. + Whatever event you act on has already cancelled the event time-out, + so there is never a running event time-out + while the <em>state callback</em> executes. </p> <p> - Note that an event time-out does not work well with + Note that an event time-out does not work well when you have for example a status call as in section <seealso marker="#All State Events">All State Events</seealso>, or handle unknown events, since all kinds of events @@ -1560,6 +1634,12 @@ open(cast, {button,_}, Data) -> a late time-out event can be handled by ignoring it if it arrives in a state where it is known to be late. </p> + <p> + You can restart, cancel or update a generic time-out. + See section + <seealso marker="#Time-Outs">Time-Outs</seealso> + for details. + </p> </section> <!-- =================================================================== --> @@ -1870,7 +1950,7 @@ open(state_timeout, lock, Data) -> <seealso marker="#Transition Actions"> <em>transition action</em> </seealso> - <c>{next_event,EventType,EventContent}</c>. + <seealso marker="stdlib:gen_statem#type-action"><c>{next_event,EventType,EventContent}</c></seealso>. </p> <p> You can generate events of any existing |