summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaimo Niskanen <raimo@erlang.org>2019-08-23 09:42:59 +0200
committerRaimo Niskanen <raimo@erlang.org>2019-08-29 15:46:35 +0200
commitf515415944a7952786e130034b735bdf473df91a (patch)
tree95d3c87354772df567ede482ebe4cd386d9e1e07
parent8f6b49452953499d26fc784cc0fde035119ca75a (diff)
downloaderlang-f515415944a7952786e130034b735bdf473df91a.tar.gz
Implement timeout cancel and update
-rw-r--r--lib/stdlib/doc/src/gen_statem.xml78
-rw-r--r--lib/stdlib/src/gen_statem.erl227
-rw-r--r--lib/stdlib/test/gen_statem_SUITE.erl107
-rw-r--r--system/doc/design_principles/statem.xml85
4 files changed, 397 insertions, 100 deletions
diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml
index ef548ad643..423da60804 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>
@@ -1176,7 +1194,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>
@@ -1229,6 +1247,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/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl
index 7e3f93d535..64cc0cd602 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()}.
@@ -1525,20 +1535,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,
@@ -1555,14 +1565,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},
@@ -1587,10 +1596,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,
@@ -1609,15 +1620,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(
@@ -1760,7 +1796,7 @@ loop_state_change(
%%
case TimeoutTypes of
%% Optimization
- %% - only cancel timeout when there is an active timeout
+ %% - only cancel timeout when it is active
%%
#{state_timeout := TimerRef} ->
%% State timeout active
@@ -1789,7 +1825,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,
@@ -1797,7 +1833,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(
@@ -1822,7 +1858,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,
@@ -1830,6 +1867,8 @@ loop_timeouts(
NextEventsR, Hibernate, [], Postponed,
Timers, _Seen, TimeoutEvents) ->
%%
+ %% End of timeouts
+ %%
S_1 =
S#state{
state_data = NextState_NewData,
@@ -1865,37 +1904,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,
@@ -1903,8 +1933,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,
@@ -1931,13 +1963,20 @@ 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),
case Debug of
?not_sys_debug ->
- loop_timeouts(
+ loop_timeouts_register(
P, Debug, S, Events, NextState_NewData,
NextEventsR, Hibernate, TimeoutsR, Postponed,
Timers, Seen, TimeoutEvents,
@@ -1950,15 +1989,18 @@ loop_timeouts(
{start_timer,
{TimeoutType,Time,TimeoutMsg,TimeoutOpts},
State}),
- loop_timeouts(
+ loop_timeouts_register(
P, Debug_1, S, Events, NextState_NewData,
NextEventsR, Hibernate, TimeoutsR, Postponed,
Timers, Seen, TimeoutEvents,
TimeoutType, TimerRef)
end
end.
+
+%% Loop helper to register a newly started timer
+%% and to cancel any running timer
%%
-loop_timeouts(
+loop_timeouts_register(
P, Debug, S, Events, NextState_NewData,
NextEventsR, Hibernate, TimeoutsR, Postponed,
{TimerRefs, TimeoutTypes}, Seen, TimeoutEvents,
@@ -1980,7 +2022,7 @@ loop_timeouts(
P, Debug, S,
Events, NextState_NewData,
NextEventsR, Hibernate, TimeoutsR, Postponed,
- Timers, Seen#{TimeoutType => true}, TimeoutEvents);
+ Timers, Seen#{TimeoutType => true}, TimeoutEvents);
#{} ->
%% Insert the new timer type and ref
Timers =
@@ -2033,6 +2075,79 @@ loop_timeouts_cancel(
Timers, Seen#{TimeoutType => true}, TimeoutEvents)
end.
+%% Loop helper to update the timeout message by cancelling
+%% the old timer and restarting with the remaining time and new message
+%%
+loop_timeouts_update(
+ P, Debug, S,
+ Events, NextState_NewData,
+ NextEventsR, Hibernate, TimeoutsR, Postponed,
+ {TimerRefs,TimeoutTypes}, Seen, TimeoutEvents,
+ TimeoutType, TimeoutMsg) ->
+ %%
+ case TimeoutTypes of
+ #{TimeoutType := OldTimerRef} ->
+ case erlang:cancel_timer(OldTimerRef) of
+ false ->
+ %% Already expired - insert timeout event with new msg
+ receive
+ {timeout,OldTimerRef,_} ->
+ ok
+ end,
+ Timers =
+ {maps:remove(OldTimerRef, TimerRefs),
+ maps:remove(TimeoutType, TimeoutTypes)},
+ TimeoutEvents_1 =
+ [{TimeoutType,TimeoutMsg}|TimeoutEvents],
+ loop_timeouts(
+ P, Debug, S,
+ Events, NextState_NewData,
+ NextEventsR, Hibernate, TimeoutsR, Postponed,
+ Timers, Seen#{TimeoutType => true}, TimeoutEvents_1);
+ Time ->
+ %% Start a new timer for the remaining time
+ TimerRef =
+ erlang:start_timer(Time, self(), TimeoutMsg),
+ %% Insert the new timer type and ref
+ Timers =
+ {maps:remove(
+ OldTimerRef,
+ TimerRefs#{TimerRef => TimeoutType}),
+ TimeoutTypes#{TimeoutType := TimerRef}},
+ case Debug of
+ ?not_sys_debug ->
+ loop_timeouts(
+ P, Debug, S,
+ Events, NextState_NewData,
+ NextEventsR, Hibernate, TimeoutsR, Postponed,
+ Timers, Seen#{TimeoutType => true}, TimeoutEvents);
+ _ ->
+ {State,_Data} = S#state.state_data,
+ Debug_1 =
+ sys_debug(
+ Debug, P#params.name,
+ {start_timer,
+ {TimeoutType,Time,TimeoutMsg,[]},
+ State}),
+ loop_timeouts(
+ P, Debug_1, S,
+ Events, NextState_NewData,
+ NextEventsR, Hibernate, TimeoutsR, Postponed,
+ Timers, Seen#{TimeoutType => true}, TimeoutEvents)
+ end
+ end;
+ #{} ->
+ Timers = {TimerRefs,TimeoutTypes},
+ %% Not running - insert timeout event
+ 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
%%
@@ -2094,13 +2209,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.
%%
diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl
index 3f92bfa4d7..31808a915f 100644
--- a/lib/stdlib/test/gen_statem_SUITE.erl
+++ b/lib/stdlib/test/gen_statem_SUITE.erl
@@ -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,
@@ -875,7 +879,6 @@ state_timeout(_Config) ->
{ok,STM} =
gen_statem:start_link(
?MODULE, {map_statem,Machine,[]}, [{debug,[trace]}]),
- sys:trace(STM, true),
TRef = erlang:start_timer(1000, self(), kull),
ok = gen_statem:call(STM, {go,500}),
ok = gen_statem:call(STM, check),
@@ -901,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),
@@ -1895,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 23e9054547..b2d769d3a9 100644
--- a/system/doc/design_principles/statem.xml
+++ b/system/doc/design_principles/statem.xml
@@ -561,34 +561,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, EventContent, Time, Opts}</c>
+ <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 />
+ <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>
@@ -844,9 +858,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
@@ -869,6 +883,32 @@ 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 <em>transition action</em> 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 <em>transition action</em> 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" />
@@ -1200,10 +1240,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>
@@ -1472,12 +1514,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
@@ -1548,6 +1591,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>
<!-- =================================================================== -->