aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRaimo Niskanen <[email protected]>2016-09-30 18:00:38 +0200
committerRaimo Niskanen <[email protected]>2016-10-12 11:27:34 +0200
commit77e175589b0ee3c1a4c94aef3cdcdf54cd84c53c (patch)
treec1f5fa31d25e4e3f8974448f131f980a798bfbeb
parent800265f49f912dcf66846b13aa8032bf2f380caf (diff)
downloadotp-77e175589b0ee3c1a4c94aef3cdcdf54cd84c53c.tar.gz
otp-77e175589b0ee3c1a4c94aef3cdcdf54cd84c53c.tar.bz2
otp-77e175589b0ee3c1a4c94aef3cdcdf54cd84c53c.zip
Implement state timeouts
-rw-r--r--lib/stdlib/doc/src/gen_statem.xml105
-rw-r--r--lib/stdlib/src/gen_statem.erl464
-rw-r--r--lib/stdlib/test/gen_statem_SUITE.erl80
-rw-r--r--system/doc/design_principles/code_lock.diabin2932 -> 2945 bytes
-rw-r--r--system/doc/design_principles/code_lock.pngbin59160 -> 59827 bytes
-rw-r--r--system/doc/design_principles/code_lock_2.diabin2621 -> 2956 bytes
-rw-r--r--system/doc/design_principles/code_lock_2.pngbin48927 -> 55553 bytes
-rw-r--r--system/doc/design_principles/statem.xml581
8 files changed, 817 insertions, 413 deletions
diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml
index bba2de5e77..c0631c8448 100644
--- a/lib/stdlib/doc/src/gen_statem.xml
+++ b/lib/stdlib/doc/src/gen_statem.xml
@@ -527,7 +527,8 @@ handle_event(_, _, State, Data) ->
Type <c>info</c> originates from regular process messages sent
to the <c>gen_statem</c>. Also, the state machine
implementation can generate events of types
- <c>timeout</c>, <c>enter</c> and <c>internal</c> to itself.
+ <c>timeout</c>, <c>state_timeout</c>, <c>enter</c>,
+ and <c>internal</c> to itself.
</p>
</desc>
</datatype>
@@ -657,8 +658,7 @@ handle_event(_, _, State, Data) ->
All events stored with
<seealso marker="#type-action"><c>action()</c></seealso>
<c>next_event</c>
- are inserted in the queue to be processed before
- other events.
+ are inserted to be processed before the other queued events.
</p>
</item>
<item>
@@ -668,35 +668,36 @@ handle_event(_, _, State, Data) ->
are used, the <c>gen_statem</c> calls
the new state function with arguments
<seealso marker="#type-state_enter">(enter, OldState, Data)</seealso>.
- If this call returns any
+ Any
<seealso marker="#type-enter_action"><c>actions</c></seealso>
- that sets transition options
- they are merged with the current
- That is: <c>hibernate</c> and <c>timeout</c> overrides
- the current and <c>reply</c> sends a reply.
- This has the same effect as if you would have appended
- the actions from this state enter call to the actions
+ returned from this call are handled as if they were
+ appended to the actions
returned by the state function that changed states.
</p>
</item>
<item>
<p>
- If an
- <seealso marker="#type-event_timeout"><c>event_timeout()</c></seealso>
- is set through
- <seealso marker="#type-action"><c>action()</c></seealso>
- <c>timeout</c>,
- an event timer is started if the value is less than
- <c>infinity</c> or a time-out zero event
- is enqueued if the value is zero.
+ If there are enqueued events the (possibly new)
+ <seealso marker="#state_function">state function</seealso>
+ is called with the oldest enqueued event,
+ and we start again from the top of this list.
</p>
</item>
<item>
<p>
- The (possibly new)
+ Timeout timers
+ <seealso marker="#type-state_timeout"><c>state_timeout()</c></seealso>
+ and
+ <seealso marker="#type-event_timeout"><c>event_timeout()</c></seealso>
+ are handled. This may lead to a time-out zero event
+ being generated to the
<seealso marker="#state_function">state function</seealso>
- is called with the oldest enqueued event if there is any,
- otherwise the <c>gen_statem</c> goes into <c>receive</c>
+ and we start again from the top of this list.
+ </p>
+ </item>
+ <item>
+ <p>
+ Otherwise the <c>gen_statem</c> goes into <c>receive</c>
or hibernation
(if
<seealso marker="#type-hibernate"><c>hibernate()</c></seealso>
@@ -704,8 +705,11 @@ handle_event(_, _, State, Data) ->
to wait for the next message. In hibernation the next
non-system event awakens the <c>gen_statem</c>, or rather
the next incoming message awakens the <c>gen_statem</c>,
- but if it is a system event
- it goes right back into hibernation.
+ but if it is a system event it goes right back into hibernation.
+ When a new message arrives the
+ <seealso marker="#state_function">state function</seealso>
+ is called with the corresponding event,
+ and we start again from the top of this list.
</p>
</item>
</list>
@@ -747,20 +751,20 @@ handle_event(_, _, State, Data) ->
<seealso marker="#type-event_type"><c>event_type()</c></seealso>
<c>timeout</c>
after this time (in milliseconds) unless another
- event arrives in which case this time-out is cancelled.
- Notice that a retried or inserted event
- counts like a new in this respect.
+ event arrives or has arrived
+ in which case this time-out is cancelled.
+ Note that a retried, inserted or state time-out zero
+ events counts as arrived.
</p>
<p>
If the value is <c>infinity</c>, no timer is started, as
- it never triggers anyway.
+ it never would trigger anyway.
</p>
<p>
- If the value is <c>0</c>, the time-out event is immediately enqueued
- unless there already are enqueued events, as the
- time-out is then immediately cancelled.
- This is a feature ensuring that a time-out <c>0</c> event
- is processed before any not yet received external event.
+ If the value is <c>0</c> 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.
</p>
<p>
Note that it is not possible or needed to cancel this time-out,
@@ -769,6 +773,34 @@ handle_event(_, _, State, Data) ->
</desc>
</datatype>
<datatype>
+ <name name="state_timeout"/>
+ <desc>
+ <p>
+ Generates an event of
+ <seealso marker="#type-event_type"><c>event_type()</c></seealso>
+ <c>state_timeout</c>
+ after this time (in milliseconds) unless the <c>gen_statem</c>
+ changes states (<c>NewState =/= OldState</c>)
+ which case this time-out is cancelled.
+ </p>
+ <p>
+ If the value is <c>infinity</c>, no timer is started, as
+ it never would trigger anyway.
+ </p>
+ <p>
+ If the value is <c>0</c> 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.
+ </p>
+ <p>
+ Setting this timer while it is running will restart it with
+ the new time-out value. Therefore it is possible to cancel
+ this timeout by setting it to <c>infinity</c>.
+ </p>
+ </desc>
+ </datatype>
+ <datatype>
<name name="action"/>
<desc>
<p>
@@ -886,6 +918,15 @@ handle_event(_, _, State, Data) ->
to <c><anno>Time</anno></c> with <c><anno>EventContent</anno></c>.
</p>
</item>
+ <tag><c>state_timeout</c></tag>
+ <item>
+ <p>
+ Sets the
+ <seealso marker="#type-transition_option"><c>transition_option()</c></seealso>
+ <seealso marker="#type-state_timeout"><c>state_timeout()</c></seealso>
+ to <c><anno>Time</anno></c> with <c><anno>EventContent</anno></c>.
+ </p>
+ </item>
</taglist>
</desc>
</datatype>
diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl
index 9f5573af86..bc33be22a2 100644
--- a/lib/stdlib/src/gen_statem.erl
+++ b/lib/stdlib/src/gen_statem.erl
@@ -75,7 +75,7 @@
-type event_type() ::
{'call',From :: from()} | 'cast' |
- 'info' | 'timeout' | 'internal'.
+ 'info' | 'timeout' | 'state_timeout' | 'internal'.
-type callback_mode_result() ::
callback_mode() | [callback_mode() | state_enter()].
@@ -95,6 +95,10 @@
%% Generate a ('timeout', EventContent, ...) event after Time
%% unless some other event is delivered
Time :: timeout().
+-type state_timeout() ::
+ %% Generate a ('state_timeout', EventContent, ...) event after Time
+ %% unless the state is changed
+ Time :: timeout().
-type action() ::
%% During a state change:
@@ -126,8 +130,10 @@
{'hibernate', Hibernate :: hibernate()} |
%%
(Timeout :: event_timeout()) | % {timeout,Timeout}
- {'timeout', % Set the event timeout option
+ {'timeout', % Set the event_timeout option
Time :: event_timeout(), EventContent :: term()} |
+ {'state_timeout', % Set the state_timeout option
+ Time :: state_timeout(), EventContent :: term()} |
%%
reply_action().
-type reply_action() ::
@@ -593,7 +599,8 @@ enter(Module, Opts, State, Data, Server, Actions, Parent) ->
%% The values should already have been type checked
Name = gen:get_proc_name(Server),
Debug = gen:debug_options(Name, Opts),
- P = Events = [],
+ Events = [],
+ P = [],
Event = {internal,init_state},
%% We enforce {postpone,false} to ensure that
%% our fake Event gets discarded, thought it might get logged
@@ -609,9 +616,12 @@ enter(Module, Opts, State, Data, Server, Actions, Parent) ->
state_enter => false,
module => Module,
name => Name,
+ state => State,
+ data => Data,
+ 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
- %% in loop_events_done/8
+ %% loop_event_actions/9 when it finally loops back to loop/3
+ %% in loop_events_done/9
%%
%% Marker for initial state, cleared immediately when used
init_state => true
@@ -619,13 +629,14 @@ enter(Module, Opts, State, Data, Server, Actions, Parent) ->
NewDebug = sys_debug(Debug, S, State, {enter,Event,State}),
case call_callback_mode(S) of
{ok,NewS} ->
+ StateTimer = undefined,
loop_event_actions(
- Parent, NewDebug, NewS, Events,
- State, Data, P, Event, State, NewActions);
+ Parent, NewDebug, NewS, StateTimer,
+ Events, Event, State, Data, NewActions);
{Class,Reason,Stacktrace} ->
terminate(
Class, Reason, Stacktrace,
- NewDebug, S, [Event|Events], State, Data, P)
+ NewDebug, S, [Event|Events])
end.
%%%==========================================================================
@@ -647,7 +658,9 @@ init_it(Starter, Parent, ServerRef, Module, Args, Opts) ->
proc_lib:init_ack(Starter, {error,Reason}),
error_info(
Class, Reason, Stacktrace,
- #{name => Name, callback_mode => undefined},
+ #{name => Name,
+ callback_mode => undefined,
+ state_enter => false},
[], [], undefined),
erlang:raise(Class, Reason, Stacktrace)
end.
@@ -678,7 +691,9 @@ init_result(Starter, Parent, ServerRef, Module, Result, Opts) ->
proc_lib:init_ack(Starter, {error,Error}),
error_info(
error, Error, ?STACKTRACE(),
- #{name => Name, callback_mode => undefined},
+ #{name => Name,
+ callback_mode => undefined,
+ state_enter => false},
[], [], undefined),
exit(Error)
end.
@@ -689,12 +704,10 @@ init_result(Starter, Parent, ServerRef, Module, Result, Opts) ->
system_continue(Parent, Debug, S) ->
loop(Parent, Debug, S).
-system_terminate(
- Reason, _Parent, Debug,
- #{state := State, data := Data, postponed := P} = S) ->
+system_terminate(Reason, _Parent, Debug, S) ->
terminate(
exit, Reason, ?STACKTRACE(),
- Debug, S, [], State, Data, P).
+ Debug, S, []).
system_code_change(
#{module := Module,
@@ -731,7 +744,7 @@ system_replace_state(
format_status(
Opt,
[PDict,SysState,Parent,Debug,
- #{name := Name, postponed := P, state := State, data := Data} = S]) ->
+ #{name := Name, postponed := P} = S]) ->
Header = gen:format_status_header("Status for state machine", Name),
Log = sys:get_debug(log, Debug, []),
[{header,Header},
@@ -740,7 +753,7 @@ format_status(
{"Parent",Parent},
{"Logged Events",Log},
{"Postponed",P}]} |
- case format_status(Opt, PDict, S, State, Data) of
+ case format_status(Opt, PDict, S) of
L when is_list(L) -> L;
T -> [T]
end].
@@ -816,7 +829,8 @@ loop(Parent, Debug, #{hibernate := Hibernate} = S) ->
end.
%% Entry point for wakeup_from_hibernate/3
-loop_receive(Parent, Debug, #{timer := Timer} = S) ->
+loop_receive(
+ Parent, Debug, #{timer := Timer, state_timer := StateTimer} = S) ->
receive
Msg ->
case Msg of
@@ -827,34 +841,23 @@ loop_receive(Parent, Debug, #{timer := Timer} = S) ->
sys:handle_system_msg(
Req, Pid, Parent, ?MODULE, Debug, S, Hibernate);
{'EXIT',Parent,Reason} = EXIT ->
- #{state := State, data := Data, postponed := P} = S,
%% EXIT is not a 2-tuple and therefore
%% not an event and has no event_type(),
%% but this will stand out in the crash report...
terminate(
- exit, Reason, ?STACKTRACE(),
- Debug, S, [EXIT], State, Data, P);
- {timeout,Timer,Content} when Timer =/= undefined ->
+ exit, Reason, ?STACKTRACE(), Debug, S, [EXIT]);
+ {timeout,Timer,Content}
+ when Timer =/= undefined ->
loop_receive_result(
- Parent, Debug, S, {timeout,Content});
+ Parent, Debug, S, StateTimer,
+ {timeout,Content});
+ {timeout,StateTimer,Content}
+ when StateTimer =/= undefined ->
+ loop_receive_result(
+ Parent, Debug, S, undefined,
+ {state_timeout,Content});
_ ->
- %% Cancel Timer if running
- case Timer of
- undefined ->
- ok;
- _ ->
- case erlang:cancel_timer(Timer) of
- TimeLeft when is_integer(TimeLeft) ->
- ok;
- false ->
- receive
- {timeout,Timer,_} ->
- ok
- after 0 ->
- ok
- end
- end
- end,
+ cancel_timer(Timer),
Event =
case Msg of
{'$gen_call',From,Request} ->
@@ -864,71 +867,93 @@ loop_receive(Parent, Debug, #{timer := Timer} = S) ->
_ ->
{info,Msg}
end,
- loop_receive_result(Parent, Debug, S, Event)
+ loop_receive_result(
+ Parent, Debug, S, StateTimer, Event)
end
end.
-loop_receive_result(
- Parent, Debug,
- #{state := State,
- data := Data,
- postponed := P} = S,
- Event) ->
- %% The engine state map S is now dismantled
- %% and will not be restored until we return to loop/3.
- %%
- %% The fields 'callback_mode', 'module', and 'name' are still valid.
- %% The fields 'state', 'data', and 'postponed' are held in arguments.
- %% The fields 'timer' and 'hibernate' will be recalculated.
+loop_receive_result(Parent, Debug, #{state := State} = S, StateTimer, Event) ->
+ %% The fields 'timer', 'state_timer' and 'hibernate'
+ %% are now invalid in state map S - they will be recalculated
+ %% and restored when we return to loop/3
%%
NewDebug = sys_debug(Debug, S, State, {in,Event}),
%% Here the queue of not yet handled events is created
Events = [],
Hibernate = false,
- loop_event(
- Parent, NewDebug, S, Events, State, Data, P, Event, Hibernate).
+ loop_event(Parent, NewDebug, S, StateTimer, Events, Event, Hibernate).
%% Process the event queue, or if it is empty
%% loop back to loop/3 to receive a new event
loop_events(
- Parent, Debug, S, [Event|Events],
- State, Data, P, Hibernate, _Timeout) ->
+ Parent, Debug, S, StateTimeout,
+ [Event|Events], _Timeout, State, Data, P, Hibernate) ->
%%
- %% If there was a state timer requested we just ignore that
+ %% If there was an event timer requested we just ignore that
%% since we have events to handle which cancels the timer
loop_event(
- Parent, Debug, S, Events, State, Data, P, Event, Hibernate);
+ Parent, Debug, S, StateTimeout,
+ Events, Event, State, Data, P, Hibernate);
+loop_events(
+ Parent, Debug, S, {state_timeout,Time,EventContent},
+ [] = Events, Timeout, State, Data, P, Hibernate) ->
+ if
+ Time =:= 0 ->
+ %% Simulate an immediate timeout
+ %% so we do not get the timeout message
+ %% after any received event
+ %%
+ %% This faked event will cancel
+ %& any not yet started event timer
+ Event = {state_timeout,EventContent},
+ StateTimer = undefined,
+ loop_event(
+ Parent, Debug, S, StateTimer,
+ Events, Event, State, Data, P, Hibernate);
+ true ->
+ StateTimer = erlang:start_timer(Time, self(), EventContent),
+ loop_events(
+ Parent, Debug, S, StateTimer,
+ Events, Timeout, State, Data, P, Hibernate)
+ end;
loop_events(
- Parent, Debug, S, [],
- State, Data, P, Hibernate, Timeout) ->
+ Parent, Debug, S, StateTimer,
+ [] = Events, Timeout, State, Data, P, Hibernate) ->
case Timeout of
{timeout,0,EventContent} ->
- %% Immediate timeout - simulate it
+ %% Simulate an immediate timeout
%% so we do not get the timeout message
%% after any received event
+ %%
+ Event = {timeout,EventContent},
loop_event(
- Parent, Debug, S, [],
- State, Data, P, {timeout,EventContent}, Hibernate);
+ Parent, Debug, S, StateTimer,
+ Events, Event, State, Data, P, Hibernate);
{timeout,Time,EventContent} ->
- %% Actually start a timer
Timer = erlang:start_timer(Time, self(), EventContent),
loop_events_done(
- Parent, Debug, S, Timer, State, Data, P, Hibernate);
+ Parent, Debug, S, StateTimer,
+ State, Data, P, Hibernate, Timer);
undefined ->
- %% No state timeout has been requested
+ %% No event timeout has been requested
Timer = undefined,
loop_events_done(
- Parent, Debug, S, Timer, State, Data, P, Hibernate)
+ Parent, Debug, S, StateTimer,
+ State, Data, P, Hibernate, Timer)
end.
-%%
-loop_events_done(Parent, Debug, S, Timer, State, Data, P, Hibernate) ->
+
+%% Back to the top
+loop_events_done(
+ Parent, Debug, S, StateTimer,
+ State, Data, P, Hibernate, Timer) ->
NewS =
S#{
- state => State,
- data => Data,
- postponed => P,
+ state := State,
+ data := Data,
+ postponed := P,
hibernate => Hibernate,
- timer => Timer},
+ timer => Timer,
+ state_timer => StateTimer},
loop(Parent, Debug, NewS).
@@ -936,10 +961,10 @@ loop_events_done(Parent, Debug, S, Timer, State, Data, P, Hibernate) ->
call_callback_mode(#{module := Module} = S) ->
try Module:callback_mode() of
CallbackMode ->
- call_callback_mode_result(S, CallbackMode)
+ callback_mode_result(S, CallbackMode)
catch
CallbackMode ->
- call_callback_mode_result(S, CallbackMode);
+ callback_mode_result(S, CallbackMode);
error:undef ->
%% Process undef to check for the simple mistake
%% of calling a nonexistent state function
@@ -957,7 +982,7 @@ call_callback_mode(#{module := Module} = S) ->
{Class,Reason,erlang:get_stacktrace()}
end.
-call_callback_mode_result(S, CallbackMode) ->
+callback_mode_result(S, CallbackMode) ->
case
parse_callback_mode(
if
@@ -1060,15 +1085,26 @@ call_state_function(
{Class,Reason,erlang:get_stacktrace()}
end.
+%% Update S and continue
loop_event(
- Parent, Debug, S, Events,
- State, Data, P, {Type,Content} = Event, Hibernate) ->
+ Parent, Debug, S, StateTimer,
+ Events, Event, State, Data, P, Hibernate) ->
+ NewS =
+ S#{
+ state := State,
+ data := Data,
+ postponed := P},
+ loop_event(Parent, Debug, NewS, StateTimer, Events, Event, Hibernate).
+
+loop_event(
+ Parent, Debug, #{state := State, data := Data} = S, StateTimer,
+ Events, {Type,Content} = Event, Hibernate) ->
%%
%% If Hibernate is true here it can only be
%% because it was set from an event action
%% and we did not go into hibernation since there
%% were events in queue, so we do what the user
- %% might depend on i.e collect garbage which
+ %% might rely on i.e collect garbage which
%% would have happened if we actually hibernated
%% and immediately was awakened
Hibernate andalso garbage_collect(),
@@ -1076,45 +1112,40 @@ loop_event(
{ok,Result,NewS} ->
{NewData,NextState,Actions} =
parse_event_result(
- Parent, Debug, NewS, Events,
- State, Data, P, Event,
- Result, true),
+ true, Debug, NewS, Result,
+ Events, Event, State, Data),
loop_event_actions(
- Parent, Debug, S, Events,
- State, NewData, P, Event, NextState, Actions);
+ Parent, Debug, S, StateTimer,
+ Events, Event, NextState, NewData, Actions);
{Class,Reason,Stacktrace} ->
terminate(
- Class, Reason, Stacktrace,
- Debug, S, [Event|Events], State, Data, P)
+ Class, Reason, Stacktrace, Debug, S, [Event|Events])
end.
%% Interpret all callback return variants
parse_event_result(
- _Parent, Debug, S, Events, State, Data, P, Event,
- Result, AllowStateChange) ->
+ AllowStateChange, Debug, S, Result, Events, Event, State, Data) ->
case Result of
stop ->
terminate(
- exit, normal, ?STACKTRACE(),
- Debug, S, [Event|Events], State, Data, P);
+ exit, normal, ?STACKTRACE(), Debug, S, [Event|Events]);
{stop,Reason} ->
terminate(
- exit, Reason, ?STACKTRACE(),
- Debug, S, [Event|Events], State, Data, P);
+ exit, Reason, ?STACKTRACE(), Debug, S, [Event|Events]);
{stop,Reason,NewData} ->
terminate(
exit, Reason, ?STACKTRACE(),
- Debug, S, [Event|Events], State, NewData, P);
+ Debug, S#{data := NewData}, [Event|Events]);
{stop_and_reply,Reason,Replies} ->
Q = [Event|Events],
reply_then_terminate(
exit, Reason, ?STACKTRACE(),
- Debug, S, Q, State, Data, P, Replies);
+ Debug, S, Q, Replies);
{stop_and_reply,Reason,Replies,NewData} ->
Q = [Event|Events],
reply_then_terminate(
exit, Reason, ?STACKTRACE(),
- Debug, S, Q, State, NewData, P, Replies);
+ Debug, S#{data := NewData}, Q, Replies);
{next_state,State,NewData} ->
{NewData,State,[]};
{next_state,NextState,NewData} when AllowStateChange ->
@@ -1136,31 +1167,35 @@ parse_event_result(
error,
{bad_return_from_state_function,Result},
?STACKTRACE(),
- Debug, S, [Event|Events], State, Data, P)
+ Debug, S, [Event|Events])
end.
-parse_enter_actions(Debug, S, State, Actions, Hibernate, Timeout) ->
+parse_enter_actions(
+ Debug, S, State, Actions,
+ Hibernate, Timeout, StateTimeout) ->
Postpone = forbidden,
NextEvents = forbidden,
parse_actions(
Debug, S, State, listify(Actions),
- Hibernate, Timeout, Postpone, NextEvents).
+ Hibernate, Timeout, StateTimeout, Postpone, NextEvents).
parse_actions(Debug, S, State, Actions) ->
- Postpone = false,
Hibernate = false,
Timeout = undefined,
+ StateTimeout = undefined,
+ Postpone = false,
NextEvents = [],
parse_actions(
Debug, S, State, listify(Actions),
- Hibernate, Timeout, Postpone, NextEvents).
+ Hibernate, Timeout, StateTimeout, Postpone, NextEvents).
%%
parse_actions(
- Debug, _S, _State, [], Hibernate, Timeout, Postpone, NextEvents) ->
- {ok,Debug,Hibernate,Timeout,Postpone,NextEvents};
+ Debug, _S, _State, [],
+ Hibernate, Timeout, StateTimeout, Postpone, NextEvents) ->
+ {ok,Debug,Hibernate,Timeout,StateTimeout,Postpone,NextEvents};
parse_actions(
Debug, S, State, [Action|Actions],
- Hibernate, Timeout, Postpone, NextEvents) ->
+ Hibernate, Timeout, StateTimeout, Postpone, NextEvents) ->
case Action of
%% Actual actions
{reply,From,Reply} ->
@@ -1169,7 +1204,8 @@ parse_actions(
NewDebug = do_reply(Debug, S, State, From, Reply),
parse_actions(
NewDebug, S, State, Actions,
- Hibernate, Timeout, Postpone, NextEvents);
+ Hibernate, Timeout, StateTimeout,
+ Postpone, NextEvents);
false ->
{error,
{bad_action_from_state_function,Action},
@@ -1179,7 +1215,7 @@ parse_actions(
{hibernate,NewHibernate} when is_boolean(NewHibernate) ->
parse_actions(
Debug, S, State, Actions,
- NewHibernate, Timeout, Postpone, NextEvents);
+ NewHibernate, Timeout, StateTimeout, Postpone, NextEvents);
{hibernate,_} ->
{error,
{bad_action_from_state_function,Action},
@@ -1187,15 +1223,25 @@ parse_actions(
hibernate ->
parse_actions(
Debug, S, State, Actions,
- true, Timeout, Postpone, NextEvents);
+ true, Timeout, StateTimeout, Postpone, NextEvents);
+ {state_timeout,Time,_} = NewStateTimeout
+ when is_integer(Time), Time >= 0;
+ Time =:= infinity ->
+ parse_actions(
+ Debug, S, State, Actions,
+ Hibernate, Timeout, NewStateTimeout, Postpone, NextEvents);
+ {state_timeout,_,_} ->
+ {error,
+ {bad_action_from_state_function,Action},
+ ?STACKTRACE()};
{timeout,infinity,_} -> % Clear timer - it will never trigger
parse_actions(
Debug, S, State, Actions,
- Hibernate, undefined, Postpone, NextEvents);
+ Hibernate, undefined, StateTimeout, Postpone, NextEvents);
{timeout,Time,_} = NewTimeout when is_integer(Time), Time >= 0 ->
parse_actions(
Debug, S, State, Actions,
- Hibernate, NewTimeout, Postpone, NextEvents);
+ Hibernate, NewTimeout, StateTimeout, Postpone, NextEvents);
{timeout,_,_} ->
{error,
{bad_action_from_state_function,Action},
@@ -1203,17 +1249,17 @@ parse_actions(
infinity -> % Clear timer - it will never trigger
parse_actions(
Debug, S, State, Actions,
- Hibernate, undefined, Postpone, NextEvents);
+ Hibernate, undefined, StateTimeout, Postpone, NextEvents);
Time when is_integer(Time), Time >= 0 ->
NewTimeout = {timeout,Time,Time},
parse_actions(
Debug, S, State, Actions,
- Hibernate, NewTimeout, Postpone, NextEvents);
+ Hibernate, NewTimeout, StateTimeout, Postpone, NextEvents);
{postpone,NewPostpone}
when is_boolean(NewPostpone), Postpone =/= forbidden ->
parse_actions(
Debug, S, State, Actions,
- Hibernate, Timeout, NewPostpone, NextEvents);
+ Hibernate, Timeout, StateTimeout, NewPostpone, NextEvents);
{postpone,_} ->
{error,
{bad_action_from_state_function,Action},
@@ -1221,7 +1267,7 @@ parse_actions(
postpone when Postpone =/= forbidden ->
parse_actions(
Debug, S, State, Actions,
- Hibernate, Timeout, true, NextEvents);
+ Hibernate, Timeout, StateTimeout, true, NextEvents);
{next_event,Type,Content} ->
case event_type(Type) of
true when NextEvents =/= forbidden ->
@@ -1229,8 +1275,8 @@ parse_actions(
sys_debug(Debug, S, State, {in,{Type,Content}}),
parse_actions(
NewDebug, S, State, Actions,
- Hibernate, Timeout, Postpone,
- [{Type,Content}|NextEvents]);
+ Hibernate, Timeout, StateTimeout,
+ Postpone, [{Type,Content}|NextEvents]);
_ ->
{error,
{bad_action_from_state_function,Action},
@@ -1243,94 +1289,143 @@ parse_actions(
end.
loop_event_actions(
- Parent, Debug, #{state_enter := StateEnter} = S, Events,
- State, NewData, P, Event, NextState, Actions) ->
+ Parent, Debug,
+ #{state := State, state_enter := StateEnter} = S, StateTimer,
+ Events, Event, NextState, NewData, Actions) ->
case parse_actions(Debug, S, State, Actions) of
- {ok,NewDebug,Hibernate,Timeout,Postpone,NextEvents} ->
- case
- StateEnter andalso
- ((NextState =/= State)
- orelse maps:is_key(init_state, S)) of
- true ->
+ {ok,NewDebug,Hibernate,Timeout,StateTimeout,Postpone,NextEvents} ->
+ if
+ StateEnter, NextState =/= State ->
loop_event_enter(
- Parent, NewDebug, S, Events,
- State, NewData, P, Event, NextState,
- Hibernate, Timeout, Postpone, NextEvents);
- false ->
+ Parent, NewDebug, S, StateTimer,
+ Events, Event, NextState, NewData,
+ Hibernate, Timeout, StateTimeout, Postpone, NextEvents);
+ 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, StateTimer,
+ Events, Event, NextState, NewData,
+ Hibernate, Timeout, StateTimeout,
+ Postpone, NextEvents);
+ false ->
+ loop_event_result(
+ Parent, NewDebug, S, StateTimer,
+ Events, Event, NextState, NewData,
+ Hibernate, Timeout, StateTimeout,
+ Postpone, NextEvents)
+ end;
+ true ->
loop_event_result(
- Parent, NewDebug, S, Events,
- State, NewData, P, Event, NextState,
- Hibernate, Timeout, Postpone, NextEvents)
+ Parent, NewDebug, S, StateTimer,
+ Events, Event, NextState, NewData,
+ Hibernate, Timeout, StateTimeout, Postpone, NextEvents)
end;
{Class,Reason,Stacktrace} ->
terminate(
Class, Reason, Stacktrace,
- Debug, S, [Event|Events], State, NewData, P)
+ Debug, S#{data := NewData}, [Event|Events])
end.
loop_event_enter(
- Parent, Debug, S, Events,
- State, NewData, P, Event, NextState,
- Hibernate, Timeout, Postpone, NextEvents) ->
+ Parent, Debug, #{state := State} = S, StateTimer,
+ Events, Event, NextState, NewData,
+ Hibernate, Timeout, StateTimeout, Postpone, NextEvents) ->
case call_state_function(S, enter, State, NextState, NewData) of
{ok,Result,NewS} ->
{NewerData,_,Actions} =
parse_event_result(
- Parent, Debug, NewS, Events,
- NextState, NewData, P, Event,
- Result, false),
+ false, Debug, NewS, Result,
+ Events, Event, NextState, NewData),
loop_event_enter_actions(
- Parent, Debug, NewS, Events,
- State, NewerData, P, Event, NextState,
- Hibernate, Timeout, Postpone, NextEvents, Actions);
+ Parent, Debug, NewS, StateTimer,
+ Events, Event, NextState, NewerData,
+ Hibernate, Timeout, StateTimeout, Postpone, NextEvents, Actions);
{Class,Reason,Stacktrace} ->
terminate(
Class, Reason, Stacktrace,
- Debug, S, [Event|Events], NextState, NewData, P)
+ Debug, S#{state := NextState, data := NewData},
+ [Event|Events])
end.
loop_event_enter_actions(
- Parent, Debug, S, Events,
- State, NewData, P, Event, NextState,
- Hibernate, Timeout, Postpone, NextEvents, Actions) ->
+ Parent, Debug, S, StateTimer,
+ Events, Event, NextState, NewData,
+ Hibernate, Timeout, StateTimeout, Postpone, NextEvents, Actions) ->
case
- parse_enter_actions(Debug, S, NextState, Actions, Hibernate, Timeout)
+ parse_enter_actions(
+ Debug, S, NextState, Actions,
+ Hibernate, Timeout, StateTimeout)
of
- {ok,NewDebug,NewHibernate,NewTimeout,_,_} ->
+ {ok,NewDebug,NewHibernate,NewTimeout,NewStateTimeout,_,_} ->
loop_event_result(
- Parent, NewDebug, S, Events,
- State, NewData, P, Event, NextState,
- NewHibernate, NewTimeout, Postpone, NextEvents);
+ Parent, NewDebug, S, StateTimer,
+ Events, Event, NextState, NewData,
+ NewHibernate, NewTimeout, NewStateTimeout, Postpone, NextEvents);
{Class,Reason,Stacktrace} ->
terminate(
Class, Reason, Stacktrace,
- Debug, S, [Event|Events], NextState, NewData, P)
+ Debug, S#{state := NextState, data := NewData},
+ [Event|Events])
end.
loop_event_result(
- Parent, Debug, S, Events,
- State, NewData, P0, Event, NextState,
- Hibernate, Timeout, Postpone, NextEvents) ->
+ Parent, Debug,
+ #{state := State, postponed := P_0} = S, StateTimer,
+ Events, Event, NextState, NewData,
+ Hibernate, Timeout, StateTimeout, Postpone, NextEvents) ->
%%
%% All options have been collected and next_events are buffered.
%% Do the actual state transition.
%%
- P1 = % Move current event to postponed if Postpone
+ NewStateTimeout =
+ case StateTimeout of
+ {state_timeout,Time,_} ->
+ %% New timeout -> cancel timer
+ case StateTimer of
+ {state_timeout,_,_} ->
+ ok;
+ _ ->
+ cancel_timer(StateTimer)
+ end,
+ case Time of
+ infinity ->
+ undefined;
+ _ ->
+ StateTimeout
+ end;
+ undefined when NextState =/= State ->
+ %% State change -> cancel timer
+ case StateTimer of
+ {state_timeout,_,_} ->
+ ok;
+ _ ->
+ cancel_timer(StateTimer)
+ end,
+ undefined;
+ undefined ->
+ StateTimer
+ end,
+ %%
+ P_1 = % Move current event to postponed if Postpone
case Postpone of
true ->
- [Event|P0];
+ [Event|P_0];
false ->
- P0
+ P_0
end,
- {Q2,P} = % Move all postponed events to queue if state change
+ {Events_1,NewP} = % Move all postponed events to queue if state change
if
NextState =:= State ->
- {Events,P1};
+ {Events,P_1};
true ->
- {lists:reverse(P1, Events),[]}
+ {lists:reverse(P_1, Events),[]}
end,
%% Place next events first in queue
- Q = lists:reverse(NextEvents, Q2),
+ NewEvents = lists:reverse(NextEvents, Events_1),
%%
NewDebug =
sys_debug(
@@ -1341,46 +1436,44 @@ loop_event_result(
false ->
{consume,Event,State}
end),
+ %%
loop_events(
- Parent, NewDebug,
- %% Avoid infinite loop in initial state with state entry events
- maps:remove(init_state, S),
- Q, NextState, NewData, P, Hibernate, Timeout).
+ Parent, NewDebug, S, NewStateTimeout,
+ NewEvents, Timeout, NextState, NewData, NewP, Hibernate).
%%---------------------------------------------------------------------------
%% Server helpers
reply_then_terminate(
Class, Reason, Stacktrace,
- Debug, S, Q, State, Data, P, Replies) ->
+ Debug, #{state := State} = S, Q, Replies) ->
if
is_list(Replies) ->
do_reply_then_terminate(
Class, Reason, Stacktrace,
- Debug, S, Q, State, Data, P, Replies);
+ Debug, S, Q, Replies, State);
true ->
do_reply_then_terminate(
Class, Reason, Stacktrace,
- Debug, S, Q, State, Data, P, [Replies])
+ Debug, S, Q, [Replies], State)
end.
%%
do_reply_then_terminate(
- Class, Reason, Stacktrace, Debug, S, Q, State, Data, P, []) ->
- terminate(Class, Reason, Stacktrace, Debug, S, Q, State, Data, P);
+ Class, Reason, Stacktrace, Debug, S, Q, [], _State) ->
+ terminate(Class, Reason, Stacktrace, Debug, S, Q);
do_reply_then_terminate(
- Class, Reason, Stacktrace, Debug, S, Q, State, Data, P, [R|Rs]) ->
+ Class, Reason, Stacktrace, Debug, S, Q, [R|Rs], State) ->
case R of
{reply,{_To,_Tag}=From,Reply} ->
NewDebug = do_reply(Debug, S, State, From, Reply),
do_reply_then_terminate(
- Class, Reason, Stacktrace,
- NewDebug, S, Q, State, Data, P, Rs);
+ Class, Reason, Stacktrace, NewDebug, S, Q, Rs, State);
_ ->
terminate(
error,
{bad_reply_action_from_state_function,R},
?STACKTRACE(),
- Debug, S, Q, State, Data, P)
+ Debug, S, Q)
end.
do_reply(Debug, S, State, From, Reply) ->
@@ -1390,7 +1483,9 @@ do_reply(Debug, S, State, From, Reply) ->
terminate(
Class, Reason, Stacktrace,
- Debug, #{module := Module} = S, Q, State, Data, P) ->
+ Debug,
+ #{module := Module, state := State, data := Data, postponed := P} = S,
+ Q) ->
try Module:terminate(Reason, State, Data) of
_ -> ok
catch
@@ -1399,7 +1494,7 @@ terminate(
ST = erlang:get_stacktrace(),
error_info(
C, R, ST, S, Q, P,
- format_status(terminate, get(), S, State, Data)),
+ format_status(terminate, get(), S)),
sys:print_log(Debug),
erlang:raise(C, R, ST)
end,
@@ -1410,7 +1505,7 @@ terminate(
_ ->
error_info(
Class, Reason, Stacktrace, S, Q, P,
- format_status(terminate, get(), S, State, Data)),
+ format_status(terminate, get(), S)),
sys:print_log(Debug)
end,
case Stacktrace of
@@ -1502,7 +1597,9 @@ error_info(
%% Call Module:format_status/2 or return a default value
-format_status(Opt, PDict, #{module := Module}, State, Data) ->
+format_status(
+ Opt, PDict,
+ #{module := Module, state := State, data := Data}) ->
case erlang:function_exported(Module, format_status, 2) of
true ->
try Module:format_status(Opt, [PDict,State,Data])
@@ -1531,3 +1628,18 @@ listify(Item) when is_list(Item) ->
Item;
listify(Item) ->
[Item].
+
+cancel_timer(undefined) ->
+ ok;
+cancel_timer(TRef) ->
+ case erlang:cancel_timer(TRef) of
+ TimeLeft when is_integer(TimeLeft) ->
+ ok;
+ false ->
+ receive
+ {timeout,TRef,_} ->
+ ok
+ after 0 ->
+ ok
+ end
+ end.
diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl
index 48f93b1de7..28f9ab81fe 100644
--- a/lib/stdlib/test/gen_statem_SUITE.erl
+++ b/lib/stdlib/test/gen_statem_SUITE.erl
@@ -37,7 +37,8 @@ all() ->
{group, stop_handle_event},
{group, abnormal},
{group, abnormal_handle_event},
- shutdown, stop_and_reply, state_enter, event_order, code_change,
+ shutdown, stop_and_reply, state_enter, event_order,
+ state_timeout, code_change,
{group, sys},
hibernate, enter_loop].
@@ -709,6 +710,83 @@ event_order(_Config) ->
+state_timeout(_Config) ->
+ process_flag(trap_exit, true),
+
+ Machine =
+ #{init =>
+ fun () ->
+ {ok,start,0}
+ end,
+ start =>
+ fun
+ ({call,From}, {go,Time}, 0) ->
+ self() ! message_to_self,
+ {next_state, state1, {Time,From},
+ %% Verify that internal events goes before external
+ [{state_timeout,Time,1},
+ {next_event,internal,1}]}
+ end,
+ state1 =>
+ fun
+ (internal, 1, Data) ->
+ %% Verify that a state change cancels timeout 1
+ {next_state, state2, Data,
+ [{timeout,0,2},
+ {state_timeout,0,2},
+ {next_event,internal,2}]}
+ end,
+ state2 =>
+ fun
+ (internal, 2, Data) ->
+ %% Verify that {state_timeout,0,_}
+ %% comes after next_event and that
+ %% {timeout,0,_} is cancelled by
+ %% {state_timeout,0,_}
+ {keep_state, {ok,2,Data},
+ [{timeout,0,3}]};
+ (state_timeout, 2, {ok,2,{Time,From}}) ->
+ {next_state, state3, 3,
+ [{reply,From,ok},
+ {state_timeout,Time,3}]}
+ end,
+ state3 =>
+ fun
+ (info, message_to_self, 3) ->
+ {keep_state, '3'};
+ ({call,From}, check, '3') ->
+ {keep_state, From};
+ (state_timeout, 3, From) ->
+ {stop_and_reply, normal,
+ {reply,From,ok}}
+ end},
+
+ {ok,STM} = gen_statem:start_link(?MODULE, {map_statem,Machine,[]}, []),
+ TRef = erlang:start_timer(1000, self(), kull),
+ ok = gen_statem:call(STM, {go,500}),
+ ok = gen_statem:call(STM, check),
+ receive
+ {timeout,TRef,kull} ->
+ ct:fail(late_timeout)
+ after 0 ->
+ receive
+ {timeout,TRef,kull} ->
+ ok
+ after 1000 ->
+ ct:fail(no_check_timeout)
+ end
+ end,
+ receive
+ {'EXIT',STM,normal} ->
+ ok
+ after 500 ->
+ ct:fail(did_not_stop)
+ end,
+
+ verify_empty_msgq().
+
+
+
sys1(Config) ->
{ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []),
{status, Pid, {module,gen_statem}, _} = sys:get_status(Pid),
diff --git a/system/doc/design_principles/code_lock.dia b/system/doc/design_principles/code_lock.dia
index 8e6ff8a898..eaa2aca5b0 100644
--- a/system/doc/design_principles/code_lock.dia
+++ b/system/doc/design_principles/code_lock.dia
Binary files differ
diff --git a/system/doc/design_principles/code_lock.png b/system/doc/design_principles/code_lock.png
index 745fd91920..40bd35fc74 100644
--- a/system/doc/design_principles/code_lock.png
+++ b/system/doc/design_principles/code_lock.png
Binary files differ
diff --git a/system/doc/design_principles/code_lock_2.dia b/system/doc/design_principles/code_lock_2.dia
index 142909a2f5..3b9ba554d8 100644
--- a/system/doc/design_principles/code_lock_2.dia
+++ b/system/doc/design_principles/code_lock_2.dia
Binary files differ
diff --git a/system/doc/design_principles/code_lock_2.png b/system/doc/design_principles/code_lock_2.png
index ecf7b0d799..3aca9dd5aa 100644
--- a/system/doc/design_principles/code_lock_2.png
+++ b/system/doc/design_principles/code_lock_2.png
Binary files differ
diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml
index 69d1e8e9fa..9a50bef7b1 100644
--- a/system/doc/design_principles/statem.xml
+++ b/system/doc/design_principles/statem.xml
@@ -29,7 +29,7 @@
<rev></rev>
<file>statem.xml</file>
</header>
- <marker id="gen_statem Behaviour"></marker>
+ <marker id="gen_statem Behaviour" />
<p>
This section is to be read with the
<seealso marker="stdlib:gen_statem"><c>gen_statem(3)</c></seealso>
@@ -50,6 +50,7 @@
<!-- =================================================================== -->
<section>
+ <marker id="Event-Driven State Machines" />
<title>Event-Driven State Machines</title>
<p>
Established Automata Theory does not deal much with
@@ -94,7 +95,7 @@ State(S) x Event(E) -> Actions(A), State(S')</pre>
<!-- =================================================================== -->
<section>
- <marker id="callback_modes" />
+ <marker id="Callback Modes" />
<title>Callback Modes</title>
<p>
The <c>gen_statem</c> behavior supports two callback modes:
@@ -110,7 +111,12 @@ State(S) x Event(E) -> Actions(A), State(S')</pre>
<pre>
StateName(EventType, EventContent, Data) ->
... code for actions here ...
- {next_state, NewStateName, NewData}.</pre>
+ {next_state, NewStateName, NewData}.
+ </pre>
+ <p>
+ This form is used in most examples here for example in section
+ <seealso marker="#Example">Example</seealso>.
+ </p>
</item>
<item>
<p>
@@ -121,7 +127,13 @@ StateName(EventType, EventContent, Data) ->
<pre>
handle_event(EventType, EventContent, State, Data) ->
... code for actions here ...
- {next_state, NewState, NewData}</pre>
+ {next_state, NewState, NewData}
+ </pre>
+ <p>
+ Se section
+ <seealso marker="#One Event Handler">One Event Handler</seealso>
+ for an example.
+ </p>
</item>
</list>
<p>
@@ -134,10 +146,11 @@ handle_event(EventType, EventContent, State, Data) ->
</p>
<section>
+ <marker id="Choosing the Callback Mode" />
<title>Choosing the Callback Mode</title>
<p>
The two
- <seealso marker="#callback_modes">callback modes</seealso>
+ <seealso marker="#Callback Modes">callback modes</seealso>
give different possibilities
and restrictions, but one goal remains:
you want to handle all possible combinations of
@@ -195,7 +208,7 @@ handle_event(EventType, EventContent, State, Data) ->
<!-- =================================================================== -->
<section>
- <marker id="state_enter" />
+ <marker id="State Enter Calls" />
<title>State Enter Calls</title>
<p>
The <c>gen_statem</c> behavior can regardless of callback mode
@@ -230,10 +243,160 @@ StateName(EventType, EventContent, Data) ->
<!-- =================================================================== -->
<section>
+ <marker id="Actions" />
+ <title>Actions</title>
+ <p>
+ In the first section
+ <seealso marker="#Event-Driven State Machines">
+ Event-Driven State Machines
+ </seealso>
+ actions were mentioned as a part of
+ the general state machine model. These general actions
+ are implemented with the code that callback module
+ <c>gen_statem</c> executes in an event-handling
+ callback function before returning
+ to the <c>gen_statem</c> engine.
+ </p>
+ <p>
+ There are more specific state-transition actions
+ that a callback function can order the <c>gen_statem</c>
+ engine to do after the callback function return.
+ These are ordered by returning a list of
+ <seealso marker="stdlib:gen_statem#type-action">actions</seealso>
+ in the
+ <seealso marker="stdlib:gen_statem#type-state_function_result">return tuple</seealso>
+ from the
+ <seealso marker="stdlib:gen_statem#Module:StateName/3">callback function</seealso>.
+ These state transition actions affect the <c>gen_statem</c>
+ engine itself and can do the following:
+ </p>
+ <list type="bulleted">
+ <item>
+ <seealso marker="stdlib:gen_statem#type-postpone">
+ Postpone
+ </seealso>
+ the current event, see section
+ <seealso marker="#Postponing Events">Postponing Events</seealso>
+ </item>
+ <item>
+ <seealso marker="stdlib:gen_statem#type-hibernate">
+ Hibernate
+ </seealso>
+ the <c>gen_statem</c>, treated in
+ <seealso marker="#Hibernation">Hibernation</seealso>
+ </item>
+ <item>
+ Start a
+ <seealso marker="stdlib:gen_statem#type-state_timeout">
+ state time-out</seealso>,
+ read more in section
+ <seealso marker="#State Time-Outs">State Time-Outs</seealso>
+ </item>
+ <item>
+ Start an
+ <seealso marker="stdlib:gen_statem#type-event_timeout">event time-out</seealso>,
+ see more in section
+ <seealso marker="#Event Time-Outs">Event Time-Outs</seealso>
+ </item>
+ <item>
+ <seealso marker="stdlib:gen_statem#type-reply_action">
+ Reply
+ </seealso>
+ to a caller, mentioned at the end of section
+ <seealso marker="#All State Events">All State Events</seealso>
+ </item>
+ <item>
+ Generate the
+ <seealso marker="stdlib:gen_statem#type-action">
+ next event
+ </seealso>
+ to handle, see section
+ <seealso marker="#Self-Generated Events">Self-Generated Events</seealso>
+ </item>
+ </list>
+ <p>
+ For details, see the
+ <seealso marker="stdlib:gen_statem#type-action">
+ <c>gen_statem(3)</c>
+ </seealso>
+ manual page.
+ You can, for example, reply to many callers
+ and generate multiple next events to handle.
+ </p>
+ </section>
+
+<!-- =================================================================== -->
+
+ <section>
+ <marker id="Event Types" />
+ <title>Event Types</title>
+ <p>
+ Events are categorized in different
+ <seealso marker="stdlib:gen_statem#type-event_type">event types</seealso>.
+ Events of all types are handled in the same callback function,
+ for a given state, and the function gets
+ <c>EventType</c> and <c>EventContent</c> as arguments.
+ </p>
+ <p>
+ The following is a complete list of event types and where
+ they come from:
+ </p>
+ <taglist>
+ <tag><c>cast</c></tag>
+ <item>
+ Generated by
+ <seealso marker="stdlib:gen_statem#cast/2"><c>gen_statem:cast</c></seealso>.
+ </item>
+ <tag><c>{call,From}</c></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
+ when replying either through the state transition action
+ <c>{reply,From,Msg}</c> or by calling
+ <seealso marker="stdlib:gen_statem#reply/1"><c>gen_statem:reply</c></seealso>.
+ </item>
+ <tag><c>info</c></tag>
+ <item>
+ Generated by any regular process message sent to
+ the <c>gen_statem</c> process.
+ </item>
+ <tag><c>state_timeout</c></tag>
+ <item>
+ Generated by state transition action
+ <seealso marker="stdlib:gen_statem#type-state_timeout">
+ <c>{state_timeout,Time,EventContent}</c>
+ </seealso>
+ state timer timing out.
+ </item>
+ <tag><c>timeout</c></tag>
+ <item>
+ Generated by state transition action
+ <seealso marker="stdlib:gen_statem#type-event_timeout">
+ <c>{timeout,Time,EventContent}</c>
+ </seealso>
+ (or its short form <c>Time</c>)
+ event timer timing out.
+ </item>
+ <tag><c>internal</c></tag>
+ <item>
+ Generated by state transition
+ <seealso marker="stdlib:gen_statem#type-action">action</seealso>
+ <c>{next_event,internal,EventContent}</c>.
+ All event types above can also be generated using
+ <c>{next_event,EventType,EventContent}</c>.
+ </item>
+ </taglist>
+ </section>
+
+<!-- =================================================================== -->
+
+ <section>
+ <marker id="Example" />
<title>Example</title>
<p>
This example starts off as equivalent to the example in section
- <seealso marker="fsm"><c>gen_fsm</c>-Behavior</seealso>.
+ <seealso marker="fsm"><c>gen_fsm</c>&nbsp;Behavior</seealso>.
In later sections, additions and tweaks are made
using features in <c>gen_statem</c> that <c>gen_fsm</c> does not have.
The end of this chapter provides the example again
@@ -256,7 +419,6 @@ StateName(EventType, EventContent, Data) ->
This code lock state machine can be implemented using
<c>gen_statem</c> with the following callback module:
</p>
- <marker id="ex"></marker>
<code type="erl"><![CDATA[
-module(code_lock).
-behaviour(gen_statem).
@@ -276,7 +438,7 @@ button(Digit) ->
init(Code) ->
do_lock(),
Data = #{code => Code, remaining => Code},
- {ok,locked,Data}.
+ {ok, locked, Data}.
callback_mode() ->
state_functions.
@@ -287,19 +449,19 @@ locked(
case Remaining of
[Digit] ->
do_unlock(),
- {next_state,open,Data#{remaining := Code},10000};
+ {next_state, open, Data#{remaining := Code},
+ [{state_timeout,10000,lock}];
[Digit|Rest] -> % Incomplete
- {next_state,locked,Data#{remaining := Rest}};
+ {next_state, locked, Data#{remaining := Rest}};
_Wrong ->
- {next_state,locked,Data#{remaining := Code}}
+ {next_state, locked, Data#{remaining := Code}}
end.
-open(timeout, _, Data) ->
+open(state_timeout, lock, Data) ->
do_lock(),
- {next_state,locked,Data};
+ {next_state, locked, Data};
open(cast, {button,_}, Data) ->
- do_lock(),
- {next_state,locked,Data}.
+ {next_state, open, Data}.
do_lock() ->
io:format("Lock~n", []).
@@ -310,7 +472,7 @@ terminate(_Reason, State, _Data) ->
State =/= locked andalso do_lock(),
ok.
code_change(_Vsn, State, Data, _Extra) ->
- {ok,State,Data}.
+ {ok, State, Data}.
]]></code>
<p>The code is explained in the next sections.</p>
</section>
@@ -318,6 +480,7 @@ code_change(_Vsn, State, Data, _Extra) ->
<!-- =================================================================== -->
<section>
+ <marker id="Starting gen_statem" />
<title>Starting gen_statem</title>
<p>
In the example in the previous section, <c>gen_statem</c> is
@@ -380,7 +543,7 @@ start_link(Code) ->
<p>
If name registration succeeds, the new <c>gen_statem</c> process
calls callback function <c>code_lock:init(Code)</c>.
- This function is expected to return <c>{ok,State,Data}</c>,
+ This function is expected to return <c>{ok, State, Data}</c>,
where <c>State</c> is the initial state of the <c>gen_statem</c>,
in this case <c>locked</c>; assuming that the door is locked to begin
with. <c>Data</c> is the internal server data of the <c>gen_statem</c>.
@@ -421,7 +584,7 @@ callback_mode() ->
Function
<seealso marker="stdlib:gen_statem#Module:callback_mode/0"><c>Module:callback_mode/0</c></seealso>
selects the
- <seealso marker="#callback_modes"><c>CallbackMode</c></seealso>
+ <seealso marker="#Callback Modes"><c>CallbackMode</c></seealso>
for the callback module, in this case
<seealso marker="stdlib:gen_statem#type-callback_mode"><c>state_functions</c></seealso>.
That is, each state has got its own handler function.
@@ -432,6 +595,7 @@ callback_mode() ->
<!-- =================================================================== -->
<section>
+ <marker id="Handling Events" />
<title>Handling Events</title>
<p>The function notifying the code lock about a button event is
implemented using
@@ -451,11 +615,13 @@ button(Digit) ->
The event is made into a message and sent to the <c>gen_statem</c>.
When the event is received, the <c>gen_statem</c> calls
<c>StateName(cast, Event, Data)</c>, which is expected to
- return a tuple <c>{next_state,NewStateName,NewData}</c>.
+ return a tuple <c>{next_state, NewStateName, NewData}</c>,
+ or <c>{next_state, NewStateName, NewData, Actions}</c>.
<c>StateName</c> is the name of the current state and
<c>NewStateName</c> is the name of the next state to go to.
<c>NewData</c> is a new value for the server data of
- the <c>gen_statem</c>.
+ the <c>gen_statem</c>, and <c>Actions</c> is a list of
+ actions on the <c>gen_statem</c> engine.
</p>
<code type="erl"><![CDATA[
locked(
@@ -464,19 +630,19 @@ locked(
case Remaining of
[Digit] -> % Complete
do_unlock(),
- {next_state,open,Data#{remaining := Code},10000};
+ {next_state, open, Data#{remaining := Code},
+ [{state_timeout,10000,lock}]};
[Digit|Rest] -> % Incomplete
- {next_state,locked,Data#{remaining := Rest}};
+ {next_state, locked, Data#{remaining := Rest}};
[_|_] -> % Wrong
- {next_state,locked,Data#{remaining := Code}}
+ {next_state, locked, Data#{remaining := Code}}
end.
-open(timeout, _, Data) ->
+open(state_timeout, lock, Data) ->
do_lock(),
- {next_state,locked,Data};
+ {next_state, locked, Data};
open(cast, {button,_}, Data) ->
- do_lock(),
- {next_state,locked,Data}.
+ {next_state, open, Data}.
]]></code>
<p>
If the door is locked and a button is pressed, the pressed
@@ -490,38 +656,55 @@ open(cast, {button,_}, Data) ->
restarts from the start of the code sequence.
</p>
<p>
- In state <c>open</c>, any button locks the door, as
- any event cancels the event timer, so no
- time-out event occurs after a button event.
+ If the whole code is correct, the server changes states
+ to <c>open</c>.
+ </p>
+ <p>
+ In state <c>open</c>, a button event is ignored
+ by staying in the same state. This can also be done
+ by returning <c>{keep_state, Data}</c> or in this case
+ since <c>Data</c> unchanged even by returning
+ <c>keep_state_and_data</c>.
</p>
</section>
<section>
- <title>Event Time-Outs</title>
+ <marker id="State Time-Outs" />
+ <title>State Time-Outs</title>
<p>
When a correct code has been given, the door is unlocked and
the following tuple is returned from <c>locked/2</c>:
</p>
<code type="erl"><![CDATA[
-{next_state,open,Data#{remaining := Code},10000};
+{next_state, open, Data#{remaining := Code},
+ [{state_timeout,10000,lock}]};
]]></code>
<p>
10,000 is a time-out value in milliseconds.
After this time (10 seconds), a time-out occurs.
- Then, <c>StateName(timeout, 10000, Data)</c> is called.
+ Then, <c>StateName(state_timeout, lock, Data)</c> is called.
The time-out occurs when the door has been in state <c>open</c>
for 10 seconds. After that the door is locked again:
</p>
<code type="erl"><![CDATA[
-open(timeout, _, Data) ->
+open(state_timeout, lock, Data) ->
do_lock(),
- {next_state,locked,Data};
+ {next_state, locked, Data};
]]></code>
+ <p>
+ The timer for a state time-out is automatically cancelled
+ when the state machine changes states. You can restart
+ a state time-out by setting it to a new time, which cancels
+ the running timer and starts a new. This implies that
+ you can cancel a state time-out by restarting it with
+ time <c>infinity</c>.
+ </p>
</section>
<!-- =================================================================== -->
<section>
+ <marker id="All State Events" />
<title>All State Events</title>
<p>
Sometimes events can arrive in any state of the <c>gen_statem</c>.
@@ -554,21 +737,24 @@ open(EventType, EventContent, Data) ->
handle_event(EventType, EventContent, Data).
handle_event({call,From}, code_length, #{code := Code} = Data) ->
- {keep_state,Data,[{reply,From,length(Code)}]}.
+ {keep_state, Data, [{reply,From,length(Code)}]}.
]]></code>
<p>
This example uses
<seealso marker="stdlib:gen_statem#call/2"><c>gen_statem:call/2</c></seealso>,
which waits for a reply from the server.
The reply is sent with a <c>{reply,From,Reply}</c> tuple
- in an action list in the <c>{keep_state,...}</c> tuple
- that retains the current state.
+ in an action list in the <c>{keep_state, ...}</c> tuple
+ that retains the current state. This return form is convenient
+ when you want to stay in the current state but do not know or
+ care about what it is.
</p>
</section>
<!-- =================================================================== -->
<section>
+ <marker id="One Event Handler" />
<title>One Event Handler</title>
<p>
If mode <c>handle_event_function</c> is used,
@@ -592,19 +778,19 @@ handle_event(cast, {button,Digit}, State, #{code := Code} = Data) ->
case maps:get(remaining, Data) of
[Digit] -> % Complete
do_unlock(),
- {next_state,open,Data#{remaining := Code},10000};
+ {next_state, open, Data#{remaining := Code},
+ [{state_timeout,10000,lock}};
[Digit|Rest] -> % Incomplete
- {keep_state,Data#{remaining := Rest}};
+ {keep_state, Data#{remaining := Rest}};
[_|_] -> % Wrong
- {keep_state,Data#{remaining := Code}}
+ {keep_state, Data#{remaining := Code}}
end;
open ->
- do_lock(),
- {next_state,locked,Data}
+ keep_state_and_data
end;
-handle_event(timeout, _, open, Data) ->
+handle_event(state_timeout, lock, open, Data) ->
do_lock(),
- {next_state,locked,Data}.
+ {next_state, locked, Data}.
...
]]></code>
@@ -613,9 +799,11 @@ handle_event(timeout, _, open, Data) ->
<!-- =================================================================== -->
<section>
+ <marker id="Stopping" />
<title>Stopping</title>
<section>
+ <marker id="In a Supervision Tree" />
<title>In a Supervision Tree</title>
<p>
If the <c>gen_statem</c> is part of a supervision tree,
@@ -655,6 +843,7 @@ terminate(_Reason, State, _Data) ->
</section>
<section>
+ <marker id="Standalone gen_statem" />
<title>Standalone gen_statem</title>
<p>
If the <c>gen_statem</c> is not part of a supervision tree,
@@ -681,127 +870,77 @@ stop() ->
<!-- =================================================================== -->
<section>
- <title>Actions</title>
+ <marker id="Event Time-Outs" />
+ <title>Event Time-Outs</title>
<p>
- In the first sections actions were mentioned as a part of
- the general state machine model. These general actions
- are implemented with the code that callback module
- <c>gen_statem</c> executes in an event-handling
- callback function before returning
- to the <c>gen_statem</c> engine.
+ A timeout feature inherited from <c>gen_statem</c>'s predecessor
+ <seealso marker="stdlib:gen_fsm"><c>gen_fsm</c></seealso>,
+ is an event time-out, that is,
+ if an event arrives the timer is cancelled.
+ You get either an event or a time-out, but not both.
</p>
<p>
- There are more specific state-transition actions
- that a callback function can order the <c>gen_statem</c>
- engine to do after the callback function return.
- These are ordered by returning a list of
- <seealso marker="stdlib:gen_statem#type-action">actions</seealso>
- in the
- <seealso marker="stdlib:gen_statem#type-state_function_result">return tuple</seealso>
- from the
- <seealso marker="stdlib:gen_statem#Module:StateName/3">callback function</seealso>.
- These state transition actions affect the <c>gen_statem</c>
- engine itself and can do the following:
+ It is ordered by the state transition action
+ <c>{timeout,Time,EventContent}</c>, or just <c>Time</c>,
+ or even just <c>Time</c> instead of an action list
+ (the latter is a form inherited from <c>gen_fsm</c>.
</p>
- <list type="bulleted">
- <item>Postpone the current event</item>
- <item>Hibernate the <c>gen_statem</c></item>
- <item>Start an event time-out</item>
- <item>Reply to a caller</item>
- <item>Generate the next event to handle</item>
- </list>
<p>
- In the example earlier was mentioned the event time-out
- and replying to a caller.
- An example of event postponing is included later in this chapter.
- For details, see the
- <seealso marker="stdlib:gen_statem#type-action"><c>gen_statem(3)</c></seealso>
- manual page.
- You can, for example, reply to many callers
- and generate multiple next events to handle.
+ This type of time-out is useful to for example act on inactivity.
+ Let us start restart the code sequence
+ if no button is pressed for say 30 seconds:
</p>
- </section>
-
-<!-- =================================================================== -->
+ <code type="erl"><![CDATA[
+...
- <section>
- <title>Event Types</title>
+locked(
+ timeout, _,
+ #{code := Code, remaining := Remaining} = Data) ->
+ {next_state, locked, Data#{remaining := Code}};
+locked(
+ cast, {button,Digit},
+ #{code := Code, remaining := Remaining} = Data) ->
+...
+ [Digit|Rest] -> % Incomplete
+ {next_state, locked, Data#{remaining := Rest}, 30000};
+...
+ ]]></code>
<p>
- The previous sections mentioned a few
- <seealso marker="stdlib:gen_statem#type-event_type">event types</seealso>.
- Events of all types are handled in the same callback function,
- for a given state, and the function gets
- <c>EventType</c> and <c>EventContent</c> as arguments.
+ Whenever we receive a button event we start an event timeout
+ of 30 seconds, and if we get an event type <c>timeout</c>
+ we reset the remaining code sequence.
</p>
<p>
- The following is a complete list of event types and where
- they come from:
+ An event timeout is cancelled by any other event so you either
+ get some other event or the timeout event. It is therefore
+ not possible nor needed to cancel or restart an event timeout.
+ Whatever event you act on has already cancelled
+ the event timeout...
</p>
- <taglist>
- <tag><c>cast</c></tag>
- <item>
- Generated by
- <seealso marker="stdlib:gen_statem#cast/2"><c>gen_statem:cast</c></seealso>.
- </item>
- <tag><c>{call,From}</c></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
- when replying either through the state transition action
- <c>{reply,From,Msg}</c> or by calling
- <seealso marker="stdlib:gen_statem#reply/1"><c>gen_statem:reply</c></seealso>.
- </item>
- <tag><c>info</c></tag>
- <item>
- Generated by any regular process message sent to
- the <c>gen_statem</c> process.
- </item>
- <tag><c>timeout</c></tag>
- <item>
- Generated by state transition action
- <c>{timeout,Time,EventContent}</c> (or its short form <c>Time</c>)
- timer timing out.
- </item>
- <tag><c>internal</c></tag>
- <item>
- Generated by state transition action
- <c>{next_event,internal,EventContent}</c>.
- All event types above can also be generated using
- <c>{next_event,EventType,EventContent}</c>.
- </item>
- </taglist>
</section>
<!-- =================================================================== -->
<section>
- <title>State Time-Outs</title>
- <p>
- The time-out event generated by state transition action
- <c>{timeout,Time,EventContent}</c> is an event time-out,
- that is, if an event arrives the timer is cancelled.
- You get either an event or a time-out, but not both.
- </p>
+ <marker id="Erlang Timers" />
+ <title>Erlang Timers</title>
<p>
- Often you want a timer not to be cancelled by any event
- or you want to start a timer in one state and respond
- to the time-out in another. This can be accomplished
- with a regular Erlang timer:
- <seealso marker="erts:erlang#start_timer/4"><c>erlang:start_timer</c></seealso>.
+ The previous example of state time-outs only work if
+ the state machine stays in the same state during the
+ time-out time. And event time-outs only work if no
+ disturbing unrelated events occur.
</p>
<p>
- For the example so far in this chapter: using the
- <c>gen_statem</c> event timer has the consequence that
- if a button event is generated while in the <c>open</c> state,
- the time-out is cancelled and the button event is delivered.
- So, we choose to lock the door if this occurred.
+ You may want to start a timer in one state and respond
+ to the time-out in another, maybe cancel the time-out
+ without changing states, or perhaps run multiple
+ time-outs in parallel. All this can be accomplished
+ with Erlang Timers:
+ <seealso marker="erts:erlang#start_timer/4"><c>erlang:start_timer3,4</c></seealso>.
</p>
<p>
- Suppose that we do not want a button to lock the door,
- instead we want to ignore button events in the <c>open</c> state.
- Then we start a timer when entering the <c>open</c> state
- and wait for it to expire while ignoring button events:
+ Here is how to accomplish the state time-out
+ in the previous example by insted using an Erlang Timer:
</p>
<code type="erl"><![CDATA[
...
@@ -812,25 +951,37 @@ locked(
[Digit] ->
do_unlock(),
Tref = erlang:start_timer(10000, self(), lock),
- {next_state,open,Data#{remaining := Code, timer := Tref}};
+ {next_state, open, Data#{remaining := Code, timer => Tref}};
...
open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->
do_lock(),
- {next_state,locked,Data};
+ {next_state,locked,maps:remove(timer, Data)};
open(cast, {button,_}, Data) ->
{keep_state,Data};
...
]]></code>
<p>
+ Removing the <c>timer</c> key from the map when we
+ change to state <c>locked</c> is not strictly
+ necessary since we can only get into state <c>open</c>
+ with an updated <c>timer</c> map value. But it can be nice
+ to not have outdated values in the state <c>Data</c>!
+ </p>
+ <p>
If you need to cancel a timer because of some other event, you can use
<seealso marker="erts:erlang#cancel_timer/2"><c>erlang:cancel_timer(Tref)</c></seealso>.
- Notice that a time-out message cannot arrive after this,
- unless you have postponed it (see the next section) before,
+ Note that a time-out message cannot arrive after this,
+ unless you have postponed it before (see the next section),
so ensure that you do not accidentally postpone such messages.
+ Also note that a time-out message may have arrived
+ just before you cancelling it, so you may have to read out
+ such a message from the process mailbox depending on
+ the return value from
+ <seealso marker="erts:erlang#cancel_timer/2"><c>erlang:cancel_timer(Tref)</c></seealso>.
</p>
<p>
- Another way to cancel a timer is not to cancel it,
+ Another way to handle a late time-out can be to not cancel it,
but to ignore it if it arrives in a state
where it is known to be late.
</p>
@@ -839,6 +990,7 @@ open(cast, {button,_}, Data) ->
<!-- =================================================================== -->
<section>
+ <marker id="Postponing Events" />
<title>Postponing Events</title>
<p>
If you want to ignore a particular event in the current state
@@ -877,6 +1029,7 @@ open(cast, {button,_}, Data) ->
</p>
<section>
+ <marker id="Fuzzy State Diagrams" />
<title>Fuzzy State Diagrams</title>
<p>
It is not uncommon that a state diagram does not specify
@@ -893,6 +1046,7 @@ open(cast, {button,_}, Data) ->
</section>
<section>
+ <marker id="Selective Receive" />
<title>Selective Receive</title>
<p>
Erlang's selective receive statement is often used to
@@ -972,7 +1126,7 @@ do_unlock() ->
<!-- =================================================================== -->
<section>
- <marker id="State Entry Actions"></marker>
+ <marker id="State Entry Actions" />
<title>State Entry Actions</title>
<p>
Say you have a state machine specification
@@ -981,7 +1135,7 @@ do_unlock() ->
(described in the next section), especially if just
one or a few states has got state entry actions,
this is a perfect use case for the built in
- <seealso marker="#state_enter">state enter calls</seealso>.
+ <seealso marker="#State Enter Calls">state enter calls</seealso>.
</p>
<p>
You return a list containing <c>state_enter</c> from your
@@ -1012,11 +1166,10 @@ locked(
{next_state, open, Data};
...
-open(enter, _OldState, Data) ->
- Tref = erlang:start_timer(10000, self(), lock),
+open(enter, _OldState, _Data) ->
do_unlock(),
- {keep_state,Data#{timer => Tref}};
-open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->
+ {keep_state_and_data, [{state_timeout,10000,lock}]};
+open(state_timeout, lock, Data) ->
{next_state, locked, Data};
...
]]></code>
@@ -1025,6 +1178,7 @@ open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->
<!-- =================================================================== -->
<section>
+ <marker id="Self-Generated Events" />
<title>Self-Generated Events</title>
<p>
It can sometimes be beneficial to be able to generate events
@@ -1054,7 +1208,7 @@ open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->
to the main state machine.
</p>
<p>
- The following example use an input model where you give the lock
+ The following example uses an input model where you give the lock
characters with <c>put_chars(Chars)</c> and then call
<c>enter()</c> to finish the input.
</p>
@@ -1102,10 +1256,11 @@ handle_event({call,From}, enter, #{buf := Buf} = Data) ->
<!-- =================================================================== -->
<section>
+ <marker id="Example Revisited" />
<title>Example Revisited</title>
<p>
- This section includes the example after all mentioned modifications
- and some more using state enter calls,
+ This section includes the example after most of the mentioned
+ modifications and some more using state enter calls,
which deserves a new state diagram:
</p>
<image file="../design_principles/code_lock_2.png">
@@ -1121,6 +1276,7 @@ handle_event({call,From}, enter, #{buf := Buf} = Data) ->
</p>
<section>
+ <marker id="Callback Mode: state_functions" />
<title>Callback Mode: state_functions</title>
<p>
Using state functions:
@@ -1155,7 +1311,11 @@ callback_mode() ->
locked(enter, _OldState, #{code := Code} = Data) ->
do_lock(),
- {keep_state,Data#{remaining => Code}};
+ {keep_state, Data#{remaining => Code}};
+locked(
+ timeout, _,
+ #{code := Code, remaining := Remaining} = Data) ->
+ {keep_state, Data#{remaining := Code}};
locked(
cast, {button,Digit},
#{code := Code, remaining := Remaining} = Data) ->
@@ -1163,26 +1323,25 @@ locked(
[Digit] -> % Complete
{next_state, open, Data};
[Digit|Rest] -> % Incomplete
- {keep_state,Data#{remaining := Rest}};
+ {keep_state, Data#{remaining := Rest}, 30000};
[_|_] -> % Wrong
- {keep_state,Data#{remaining := Code}}
+ {keep_state, Data#{remaining := Code}}
end;
locked(EventType, EventContent, Data) ->
handle_event(EventType, EventContent, Data).
-open(enter, _OldState, Data) ->
- Tref = erlang:start_timer(10000, self(), lock),
+open(enter, _OldState, _Data) ->
do_unlock(),
- {keep_state,Data#{timer => Tref}};
-open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->
+ {keep_state_and_data, [{state_timeout,10000,lock}]};
+open(state_timeout, lock, Data) ->
{next_state, locked, Data};
open(cast, {button,_}, _) ->
- {keep_state_and_data,[postpone]};
+ {keep_state_and_data, [postpone]};
open(EventType, EventContent, Data) ->
handle_event(EventType, EventContent, Data).
handle_event({call,From}, code_length, #{code := Code}) ->
- {keep_state_and_data,[{reply,From,length(Code)}]}.
+ {keep_state_and_data, [{reply,From,length(Code)}]}.
do_lock() ->
io:format("Locked~n", []).
@@ -1198,6 +1357,7 @@ code_change(_Vsn, State, Data, _Extra) ->
</section>
<section>
+ <marker id="Callback Mode: handle_event_function" />
<title>Callback Mode: handle_event_function</title>
<p>
This section describes what to change in the example
@@ -1215,9 +1375,15 @@ callback_mode() ->
[handle_event_function,state_enter].
%% State: locked
-handle_event(enter, _OldState, locked, #{code := Code} = Data) ->
+handle_event(
+ enter, _OldState, locked,
+ #{code := Code} = Data) ->
do_lock(),
- {keep_state,Data#{remaining => Code}};
+ {keep_state, Data#{remaining => Code}};
+handle_event(
+ timeout, _, locked,
+ #{code := Code, remaining := Remaining} = Data) ->
+ {keep_state, Data#{remaining := Code}};
handle_event(
cast, {button,Digit}, locked,
#{code := Code, remaining := Remaining} = Data) ->
@@ -1225,31 +1391,30 @@ handle_event(
[Digit] -> % Complete
{next_state, open, Data};
[Digit|Rest] -> % Incomplete
- {keep_state,Data#{remaining := Rest}};
+ {keep_state, Data#{remaining := Rest}, 30000};
[_|_] -> % Wrong
- {keep_state,Data#{remaining := Code}}
+ {keep_state, Data#{remaining := Code}}
end;
%%
%% State: open
-handle_event(enter, _OldState, open, Data) ->
- Tref = erlang:start_timer(10000, self(), lock),
+handle_event(enter, _OldState, open, _Data) ->
do_unlock(),
- {keep_state,Data#{timer => Tref}};
-handle_event(info, {timeout,Tref,lock}, open, #{timer := Tref} = Data) ->
+ {keep_state_and_data, [{state_timeout,10000,lock}]};
+handle_event(state_timeout, lock, open, Data) ->
{next_state, locked, Data};
handle_event(cast, {button,_}, open, _) ->
{keep_state_and_data,[postpone]};
%%
%% Any state
handle_event({call,From}, code_length, _State, #{code := Code}) ->
- {keep_state_and_data,[{reply,From,length(Code)}]}.
+ {keep_state_and_data, [{reply,From,length(Code)}]}.
...
]]></code>
</section>
<p>
Notice that postponing buttons from the <c>locked</c> state
- to the <c>open</c> state feels like the wrong thing to do
+ to the <c>open</c> state feels like a strange thing to do
for a code lock, but it at least illustrates event postponing.
</p>
</section>
@@ -1257,6 +1422,7 @@ handle_event({call,From}, code_length, _State, #{code := Code}) ->
<!-- =================================================================== -->
<section>
+ <marker id="Filter the State" />
<title>Filter the State</title>
<p>
The example servers so far in this chapter
@@ -1317,12 +1483,13 @@ format_status(Opt, [_PDict,State,Data]) ->
<!-- =================================================================== -->
<section>
+ <marker id="Complex State" />
<title>Complex State</title>
<p>
The callback mode
<seealso marker="stdlib:gen_statem#type-callback_mode"><c>handle_event_function</c></seealso>
enables using a non-atom state as described in section
- <seealso marker="#callback_modes">Callback Modes</seealso>,
+ <seealso marker="#Callback Modes">Callback Modes</seealso>,
for example, a complex state term like a tuple.
</p>
<p>
@@ -1396,7 +1563,7 @@ set_lock_button(LockButton) ->
init({Code,LockButton}) ->
process_flag(trap_exit, true),
- Data = #{code => Code, remaining => undefined, timer => undefined},
+ Data = #{code => Code, remaining => undefined},
{ok, {locked,LockButton}, Data}.
callback_mode() ->
@@ -1405,29 +1572,31 @@ callback_mode() ->
handle_event(
{call,From}, {set_lock_button,NewLockButton},
{StateName,OldLockButton}, Data) ->
- {next_state,{StateName,NewLockButton},Data,
+ {next_state, {StateName,NewLockButton}, Data,
[{reply,From,OldLockButton}]};
handle_event(
{call,From}, code_length,
{_StateName,_LockButton}, #{code := Code}) ->
{keep_state_and_data,
- [{reply,From,length(Code)}]};
+ [{reply,From,length(Code)}]};
%%
%% State: locked
handle_event(
EventType, EventContent,
{locked,LockButton}, #{code := Code, remaining := Remaining} = Data) ->
- case {EventType,EventContent} of
- {enter,_OldState} ->
+ case {EventType, EventContent} of
+ {enter, _OldState} ->
do_lock(),
- {keep_state,Data#{remaining := Code}};
- {{call,From},{button,Digit}} ->
+ {keep_state, Data#{remaining := Code}};
+ {timeout, _} ->
+ {keep_state, Data#{remaining := Code}};
+ {{call,From}, {button,Digit}} ->
case Remaining of
[Digit] -> % Complete
{next_state, {open,LockButton}, Data,
[{reply,From,ok}]};
[Digit|Rest] -> % Incomplete
- {keep_state, Data#{remaining := Rest},
+ {keep_state, Data#{remaining := Rest, 30000},
[{reply,From,ok}]};
[_|_] -> % Wrong
{keep_state, Data#{remaining := Code},
@@ -1438,18 +1607,16 @@ handle_event(
%% State: open
handle_event(
EventType, EventContent,
- {open,LockButton}, #{timer := Timer} = Data) ->
- case {EventType,EventContent} of
- {enter,_OldState} ->
- Tref = erlang:start_timer(10000, self(), lock),
+ {open,LockButton}, Data) ->
+ case {EventType, EventContent} of
+ {enter, _OldState} ->
do_unlock(),
- {keep_state,Data#{timer := Tref}};
- {info,{timeout,Timer,lock}} ->
+ {keep_state_and_data, [{state_timeout,10000,lock}]};
+ {state_timeout, lock} ->
{next_state, {locked,LockButton}, Data};
- {{call,From},{button,Digit}} ->
+ {{call,From}, {button,Digit}} ->
if
Digit =:= LockButton ->
- erlang:cancel_timer(Timer),
{next_state, {locked,LockButton}, Data,
[{reply,From,locked}]);
true ->
@@ -1494,6 +1661,7 @@ format_status(Opt, [_PDict,State,Data]) ->
<!-- =================================================================== -->
<section>
+ <marker id="Hibernation" />
<title>Hibernation</title>
<p>
If you have many servers in one node
@@ -1519,20 +1687,21 @@ format_status(Opt, [_PDict,State,Data]) ->
</p>
<code type="erl"><![CDATA[
...
+%% State: open
handle_event(
EventType, EventContent,
- {open,LockButton}, #{timer := Timer} = Data) ->
- case {EventType,EventContent} of
- {enter,_OldState} ->
- Tref = erlang:start_timer(10000, self(), lock),
+ {open,LockButton}, Data) ->
+ case {EventType, EventContent} of
+ {enter, _OldState} ->
do_unlock(),
- {keep_state,Data#{timer := Tref},[hibernate]};
+ {keep_state_and_data,
+ [{state_timeout,10000,lock},hibernate]};
...
]]></code>
<p>
- The
- <seealso marker="stdlib:gen_statem#type-hibernate"><c>[hibernate]</c></seealso>
- action list on the last line
+ The atom
+ <seealso marker="stdlib:gen_statem#type-hibernate"><c>hibernate</c></seealso>
+ in the action list on the last line
when entering the <c>{open,_}</c> state is the only change.
If any event arrives in the <c>{open,_},</c> state, we
do not bother to rehibernate, so the server stays
@@ -1547,6 +1716,10 @@ handle_event(
<c>{open,_}</c> state, which would clutter the code.
</p>
<p>
+ Another not uncommon scenario is to use the event time-out
+ to triger hibernation after a certain time of inactivity.
+ </p>
+ <p>
This server probably does not use
heap memory worth hibernating for.
To gain anything from hibernation, your server would