diff options
author | Raimo Niskanen <[email protected]> | 2016-09-30 18:00:38 +0200 |
---|---|---|
committer | Raimo Niskanen <[email protected]> | 2016-10-12 11:27:34 +0200 |
commit | 77e175589b0ee3c1a4c94aef3cdcdf54cd84c53c (patch) | |
tree | c1f5fa31d25e4e3f8974448f131f980a798bfbeb | |
parent | 800265f49f912dcf66846b13aa8032bf2f380caf (diff) | |
download | otp-77e175589b0ee3c1a4c94aef3cdcdf54cd84c53c.tar.gz otp-77e175589b0ee3c1a4c94aef3cdcdf54cd84c53c.tar.bz2 otp-77e175589b0ee3c1a4c94aef3cdcdf54cd84c53c.zip |
Implement state timeouts
-rw-r--r-- | lib/stdlib/doc/src/gen_statem.xml | 105 | ||||
-rw-r--r-- | lib/stdlib/src/gen_statem.erl | 464 | ||||
-rw-r--r-- | lib/stdlib/test/gen_statem_SUITE.erl | 80 | ||||
-rw-r--r-- | system/doc/design_principles/code_lock.dia | bin | 2932 -> 2945 bytes | |||
-rw-r--r-- | system/doc/design_principles/code_lock.png | bin | 59160 -> 59827 bytes | |||
-rw-r--r-- | system/doc/design_principles/code_lock_2.dia | bin | 2621 -> 2956 bytes | |||
-rw-r--r-- | system/doc/design_principles/code_lock_2.png | bin | 48927 -> 55553 bytes | |||
-rw-r--r-- | system/doc/design_principles/statem.xml | 581 |
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 Binary files differindex 8e6ff8a898..eaa2aca5b0 100644 --- a/system/doc/design_principles/code_lock.dia +++ b/system/doc/design_principles/code_lock.dia diff --git a/system/doc/design_principles/code_lock.png b/system/doc/design_principles/code_lock.png Binary files differindex 745fd91920..40bd35fc74 100644 --- a/system/doc/design_principles/code_lock.png +++ b/system/doc/design_principles/code_lock.png diff --git a/system/doc/design_principles/code_lock_2.dia b/system/doc/design_principles/code_lock_2.dia Binary files differindex 142909a2f5..3b9ba554d8 100644 --- a/system/doc/design_principles/code_lock_2.dia +++ b/system/doc/design_principles/code_lock_2.dia diff --git a/system/doc/design_principles/code_lock_2.png b/system/doc/design_principles/code_lock_2.png Binary files differindex ecf7b0d799..3aca9dd5aa 100644 --- a/system/doc/design_principles/code_lock_2.png +++ b/system/doc/design_principles/code_lock_2.png 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> 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 |