From 77e175589b0ee3c1a4c94aef3cdcdf54cd84c53c Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 30 Sep 2016 18:00:38 +0200 Subject: Implement state timeouts --- lib/stdlib/doc/src/gen_statem.xml | 105 +++-- lib/stdlib/src/gen_statem.erl | 464 +++++++++++++-------- lib/stdlib/test/gen_statem_SUITE.erl | 80 +++- system/doc/design_principles/code_lock.dia | Bin 2932 -> 2945 bytes system/doc/design_principles/code_lock.png | Bin 59160 -> 59827 bytes system/doc/design_principles/code_lock_2.dia | Bin 2621 -> 2956 bytes system/doc/design_principles/code_lock_2.png | Bin 48927 -> 55553 bytes 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 info originates from regular process messages sent to the gen_statem. Also, the state machine implementation can generate events of types - timeout, enter and internal to itself. + timeout, state_timeout, enter, + and internal to itself.

@@ -657,8 +658,7 @@ handle_event(_, _, State, Data) -> All events stored with action() next_event - are inserted in the queue to be processed before - other events. + are inserted to be processed before the other queued events.

@@ -668,35 +668,36 @@ handle_event(_, _, State, Data) -> are used, the gen_statem calls the new state function with arguments (enter, OldState, Data). - If this call returns any + Any actions - that sets transition options - they are merged with the current - That is: hibernate and timeout overrides - the current and reply 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.

- If an - event_timeout() - is set through - action() - timeout, - an event timer is started if the value is less than - infinity or a time-out zero event - is enqueued if the value is zero. + If there are enqueued events the (possibly new) + state function + is called with the oldest enqueued event, + and we start again from the top of this list.

- The (possibly new) + Timeout timers + state_timeout() + and + event_timeout() + are handled. This may lead to a time-out zero event + being generated to the state function - is called with the oldest enqueued event if there is any, - otherwise the gen_statem goes into receive + and we start again from the top of this list. +

+
+ +

+ Otherwise the gen_statem goes into receive or hibernation (if hibernate() @@ -704,8 +705,11 @@ handle_event(_, _, State, Data) -> to wait for the next message. In hibernation the next non-system event awakens the gen_statem, or rather the next incoming message awakens the gen_statem, - 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 + state function + is called with the corresponding event, + and we start again from the top of this list.

@@ -747,20 +751,20 @@ handle_event(_, _, State, Data) -> event_type() timeout 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.

If the value is infinity, no timer is started, as - it never triggers anyway. + it never would trigger anyway.

- If the value is 0, 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 0 event - is processed before any not yet received external event. + If the value is 0 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.

Note that it is not possible or needed to cancel this time-out, @@ -768,6 +772,34 @@ handle_event(_, _, State, Data) ->

+ + + +

+ Generates an event of + event_type() + state_timeout + after this time (in milliseconds) unless the gen_statem + changes states (NewState =/= OldState) + which case this time-out is cancelled. +

+

+ If the value is infinity, no timer is started, as + it never would trigger anyway. +

+

+ If the value is 0 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. +

+

+ 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 infinity. +

+
+
@@ -886,6 +918,15 @@ handle_event(_, _, State, Data) -> to Time with EventContent.

+ state_timeout + +

+ Sets the + transition_option() + state_timeout() + to Time with EventContent. +

+
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 Binary files a/system/doc/design_principles/code_lock.dia and b/system/doc/design_principles/code_lock.dia differ diff --git a/system/doc/design_principles/code_lock.png b/system/doc/design_principles/code_lock.png index 745fd91920..40bd35fc74 100644 Binary files a/system/doc/design_principles/code_lock.png and b/system/doc/design_principles/code_lock.png 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 Binary files a/system/doc/design_principles/code_lock_2.dia and b/system/doc/design_principles/code_lock_2.dia 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 Binary files a/system/doc/design_principles/code_lock_2.png and b/system/doc/design_principles/code_lock_2.png 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 @@ statem.xml - +

This section is to be read with the gen_statem(3) @@ -50,6 +50,7 @@

+ Event-Driven State Machines

Established Automata Theory does not deal much with @@ -94,7 +95,7 @@ State(S) x Event(E) -> Actions(A), State(S')

- + Callback Modes

The gen_statem behavior supports two callback modes: @@ -110,7 +111,12 @@ State(S) x Event(E) -> Actions(A), State(S')

 StateName(EventType, EventContent, Data) ->
     ... code for actions here ...
-    {next_state, NewStateName, NewData}.
+ {next_state, NewStateName, NewData}. + +

+ This form is used in most examples here for example in section + Example. +

@@ -121,7 +127,13 @@ StateName(EventType, EventContent, Data) ->

 handle_event(EventType, EventContent, State, Data) ->
     ... code for actions here ...
-    {next_state, NewState, NewData}
+ {next_state, NewState, NewData} + +

+ Se section + One Event Handler + for an example. +

@@ -134,10 +146,11 @@ handle_event(EventType, EventContent, State, Data) ->

+ Choosing the Callback Mode

The two - callback modes + callback modes 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) ->

- + State Enter Calls

The gen_statem behavior can regardless of callback mode @@ -230,10 +243,160 @@ StateName(EventType, EventContent, Data) ->

+ + Actions +

+ In the first section + + Event-Driven State Machines + + actions were mentioned as a part of + the general state machine model. These general actions + are implemented with the code that callback module + gen_statem executes in an event-handling + callback function before returning + to the gen_statem engine. +

+

+ There are more specific state-transition actions + that a callback function can order the gen_statem + engine to do after the callback function return. + These are ordered by returning a list of + actions + in the + return tuple + from the + callback function. + These state transition actions affect the gen_statem + engine itself and can do the following: +

+ + + + Postpone + + the current event, see section + Postponing Events + + + + Hibernate + + the gen_statem, treated in + Hibernation + + + Start a + + state time-out, + read more in section + State Time-Outs + + + Start an + event time-out, + see more in section + Event Time-Outs + + + + Reply + + to a caller, mentioned at the end of section + All State Events + + + Generate the + + next event + + to handle, see section + Self-Generated Events + + +

+ For details, see the + + gen_statem(3) + + manual page. + You can, for example, reply to many callers + and generate multiple next events to handle. +

+
+ + + +
+ + Event Types +

+ Events are categorized in different + event types. + Events of all types are handled in the same callback function, + for a given state, and the function gets + EventType and EventContent as arguments. +

+

+ The following is a complete list of event types and where + they come from: +

+ + cast + + Generated by + gen_statem:cast. + + {call,From} + + Generated by + gen_statem:call, + where From is the reply address to use + when replying either through the state transition action + {reply,From,Msg} or by calling + gen_statem:reply. + + info + + Generated by any regular process message sent to + the gen_statem process. + + state_timeout + + Generated by state transition action + + {state_timeout,Time,EventContent} + + state timer timing out. + + timeout + + Generated by state transition action + + {timeout,Time,EventContent} + + (or its short form Time) + event timer timing out. + + internal + + Generated by state transition + action + {next_event,internal,EventContent}. + All event types above can also be generated using + {next_event,EventType,EventContent}. + + +
+ + + +
+ Example

This example starts off as equivalent to the example in section - gen_fsm-Behavior. + gen_fsm Behavior. In later sections, additions and tweaks are made using features in gen_statem that gen_fsm 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 gen_statem with the following callback module:

- 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}. ]]>

The code is explained in the next sections.

@@ -318,6 +480,7 @@ code_change(_Vsn, State, Data, _Extra) ->
+ Starting gen_statem

In the example in the previous section, gen_statem is @@ -380,7 +543,7 @@ start_link(Code) ->

If name registration succeeds, the new gen_statem process calls callback function code_lock:init(Code). - This function is expected to return {ok,State,Data}, + This function is expected to return {ok, State, Data}, where State is the initial state of the gen_statem, in this case locked; assuming that the door is locked to begin with. Data is the internal server data of the gen_statem. @@ -421,7 +584,7 @@ callback_mode() -> Function Module:callback_mode/0 selects the - CallbackMode + CallbackMode for the callback module, in this case state_functions. That is, each state has got its own handler function. @@ -432,6 +595,7 @@ callback_mode() ->

+ Handling Events

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 gen_statem. When the event is received, the gen_statem calls StateName(cast, Event, Data), which is expected to - return a tuple {next_state,NewStateName,NewData}. + return a tuple {next_state, NewStateName, NewData}, + or {next_state, NewStateName, NewData, Actions}. StateName is the name of the current state and NewStateName is the name of the next state to go to. NewData is a new value for the server data of - the gen_statem. + the gen_statem, and Actions is a list of + actions on the gen_statem engine.

% 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}. ]]>

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.

- In state open, 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 open. +

+

+ In state open, a button event is ignored + by staying in the same state. This can also be done + by returning {keep_state, Data} or in this case + since Data unchanged even by returning + keep_state_and_data.

- Event Time-Outs + + State Time-Outs

When a correct code has been given, the door is unlocked and the following tuple is returned from locked/2:

10,000 is a time-out value in milliseconds. After this time (10 seconds), a time-out occurs. - Then, StateName(timeout, 10000, Data) is called. + Then, StateName(state_timeout, lock, Data) is called. The time-out occurs when the door has been in state open for 10 seconds. After that the door is locked again:

+open(state_timeout, lock, Data) -> do_lock(), - {next_state,locked,Data}; + {next_state, locked, Data}; ]]> +

+ 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 infinity. +

+ All State Events

Sometimes events can arrive in any state of the gen_statem. @@ -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)}]}. ]]>

This example uses gen_statem:call/2, which waits for a reply from the server. The reply is sent with a {reply,From,Reply} tuple - in an action list in the {keep_state,...} tuple - that retains the current state. + in an action list in the {keep_state, ...} 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.

+ One Event Handler

If mode handle_event_function 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}. ... ]]> @@ -613,9 +799,11 @@ handle_event(timeout, _, open, Data) ->

+ Stopping
+ In a Supervision Tree

If the gen_statem is part of a supervision tree, @@ -655,6 +843,7 @@ terminate(_Reason, State, _Data) ->

+ Standalone gen_statem

If the gen_statem is not part of a supervision tree, @@ -681,127 +870,77 @@ stop() ->

- Actions + + Event Time-Outs

- 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 - gen_statem executes in an event-handling - callback function before returning - to the gen_statem engine. + A timeout feature inherited from gen_statem's predecessor + gen_fsm, + 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.

- There are more specific state-transition actions - that a callback function can order the gen_statem - engine to do after the callback function return. - These are ordered by returning a list of - actions - in the - return tuple - from the - callback function. - These state transition actions affect the gen_statem - engine itself and can do the following: + It is ordered by the state transition action + {timeout,Time,EventContent}, or just Time, + or even just Time instead of an action list + (the latter is a form inherited from gen_fsm.

- - Postpone the current event - Hibernate the gen_statem - Start an event time-out - Reply to a caller - Generate the next event to handle -

- 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 - gen_statem(3) - 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:

-
- - + - Event Types +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}; +... + ]]>

- The previous sections mentioned a few - event types. - Events of all types are handled in the same callback function, - for a given state, and the function gets - EventType and EventContent as arguments. + Whenever we receive a button event we start an event timeout + of 30 seconds, and if we get an event type timeout + we reset the remaining code sequence.

- 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...

- - cast - - Generated by - gen_statem:cast. - - {call,From} - - Generated by - gen_statem:call, - where From is the reply address to use - when replying either through the state transition action - {reply,From,Msg} or by calling - gen_statem:reply. - - info - - Generated by any regular process message sent to - the gen_statem process. - - timeout - - Generated by state transition action - {timeout,Time,EventContent} (or its short form Time) - timer timing out. - - internal - - Generated by state transition action - {next_event,internal,EventContent}. - All event types above can also be generated using - {next_event,EventType,EventContent}. - -
- State Time-Outs -

- The time-out event generated by state transition action - {timeout,Time,EventContent} 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. -

+ + Erlang Timers

- 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: - erlang:start_timer. + 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.

- For the example so far in this chapter: using the - gen_statem event timer has the consequence that - if a button event is generated while in the open 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: + erlang:start_timer3,4.

- Suppose that we do not want a button to lock the door, - instead we want to ignore button events in the open state. - Then we start a timer when entering the open 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:

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}; ... ]]> +

+ Removing the timer key from the map when we + change to state locked is not strictly + necessary since we can only get into state open + with an updated timer map value. But it can be nice + to not have outdated values in the state Data! +

If you need to cancel a timer because of some other event, you can use erlang:cancel_timer(Tref). - 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 + erlang:cancel_timer(Tref).

- 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.

@@ -839,6 +990,7 @@ open(cast, {button,_}, Data) ->
+ Postponing Events

If you want to ignore a particular event in the current state @@ -877,6 +1029,7 @@ open(cast, {button,_}, Data) ->

+ Fuzzy State Diagrams

It is not uncommon that a state diagram does not specify @@ -893,6 +1046,7 @@ open(cast, {button,_}, Data) ->

+ Selective Receive

Erlang's selective receive statement is often used to @@ -972,7 +1126,7 @@ do_unlock() ->

- + State Entry Actions

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 - state enter calls. + state enter calls.

You return a list containing state_enter 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}; ... ]]> @@ -1025,6 +1178,7 @@ open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->

+ Self-Generated Events

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.

- 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 put_chars(Chars) and then call enter() to finish the input.

@@ -1102,10 +1256,11 @@ handle_event({call,From}, enter, #{buf := Buf} = Data) ->
+ Example Revisited

- 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:

@@ -1121,6 +1276,7 @@ handle_event({call,From}, enter, #{buf := Buf} = Data) ->

+ Callback Mode: state_functions

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) ->

+ Callback Mode: handle_event_function

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)}]}. ... ]]>

Notice that postponing buttons from the locked state - to the open state feels like the wrong thing to do + to the open state feels like a strange thing to do for a code lock, but it at least illustrates event postponing.

@@ -1257,6 +1422,7 @@ handle_event({call,From}, code_length, _State, #{code := Code}) ->
+ Filter the State

The example servers so far in this chapter @@ -1317,12 +1483,13 @@ format_status(Opt, [_PDict,State,Data]) ->

+ Complex State

The callback mode handle_event_function enables using a non-atom state as described in section - Callback Modes, + Callback Modes, for example, a complex state term like a tuple.

@@ -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]) ->

+ Hibernation

If you have many servers in one node @@ -1519,20 +1687,21 @@ format_status(Opt, [_PDict,State,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]}; ... ]]>

- The - [hibernate] - action list on the last line + The atom + hibernate + in the action list on the last line when entering the {open,_} state is the only change. If any event arrives in the {open,_}, state, we do not bother to rehibernate, so the server stays @@ -1546,6 +1715,10 @@ handle_event( be aware of using hibernate while in the {open,_} state, which would clutter the code.

+

+ Another not uncommon scenario is to use the event time-out + to triger hibernation after a certain time of inactivity. +

This server probably does not use heap memory worth hibernating for. -- cgit v1.2.3