diff options
author | Raimo Niskanen <[email protected]> | 2016-10-19 10:17:30 +0200 |
---|---|---|
committer | Raimo Niskanen <[email protected]> | 2016-10-19 10:17:30 +0200 |
commit | 826ebfba7b0624e52015fe6882aef3db758b9d03 (patch) | |
tree | 98a9cfa43f2197ddf3f23449c98d2533bdc7dada /system/doc | |
parent | 1cf45fc0862fa86fa1bece535907c8e81309c742 (diff) | |
parent | 5e2d802e29f0a8f81de297f9a3e3922f2d6cd6c0 (diff) | |
download | otp-826ebfba7b0624e52015fe6882aef3db758b9d03.tar.gz otp-826ebfba7b0624e52015fe6882aef3db758b9d03.tar.bz2 otp-826ebfba7b0624e52015fe6882aef3db758b9d03.zip |
Merge branch 'raimo/gen_statem-improvements/OTP-13929' into maint
* raimo/gen_statem-improvements/OTP-13929:
Fix race condition in cancel_timer/1
Use parameterized types
Implement state timeouts
Improve docs and types
Change state entry events into state enter calls
Improve docs
Improve docs
Implement state entry events
Implement call/3 dirty_timeout
Diffstat (limited to 'system/doc')
-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 | 786 |
5 files changed, 524 insertions, 262 deletions
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 57e47431b8..bece95e6b8 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: @@ -109,8 +110,13 @@ State(S) x Event(E) -> Actions(A), State(S')</pre> </p> <pre> StateName(EventType, EventContent, Data) -> - .. code for actions here ... - {next_state, NewStateName, NewData}.</pre> + ... code for actions here ... + {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> @@ -120,8 +126,14 @@ StateName(EventType, EventContent, Data) -> </p> <pre> handle_event(EventType, EventContent, State, Data) -> - .. code for actions here ... - {next_state, NewState, NewData}</pre> + ... code for actions here ... + {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,10 +208,195 @@ handle_event(EventType, EventContent, State, Data) -> <!-- =================================================================== --> <section> + <marker id="State Enter Calls" /> + <title>State Enter Calls</title> + <p> + The <c>gen_statem</c> behavior can regardless of callback mode + automatically + <seealso marker="stdlib:gen_statem#type-state_enter"> + call the state callback + </seealso> + with special arguments whenever the state changes + so you can write state entry actions + near the rest of the state transition rules. + It typically looks like this: + </p> + <pre> +StateName(enter, _OldState, Data) -> + ... code for state entry actions here ... + {keep_state, NewData}; +StateName(EventType, EventContent, Data) -> + ... code for actions here ... + {next_state, NewStateName, NewData}.</pre> + <p> + Depending on how your state machine is specified, + this can be a very useful feature, + but it forces you to handle the state enter calls in all states. + See also the + <seealso marker="#State Entry Actions"> + State Entry Actions + </seealso> + chapter. + </p> + </section> + +<!-- =================================================================== --> + + <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_callback_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 @@ -221,7 +419,6 @@ handle_event(EventType, EventContent, State, 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). @@ -241,7 +438,7 @@ button(Digit) -> init(Code) -> do_lock(), Data = #{code => Code, remaining => Code}, - {ok,locked,Data}. + {ok, locked, Data}. callback_mode() -> state_functions. @@ -252,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", []). @@ -275,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> @@ -283,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 @@ -345,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>. @@ -386,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. @@ -397,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 @@ -416,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( @@ -429,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 @@ -455,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>. @@ -519,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, @@ -557,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> @@ -578,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, @@ -620,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, @@ -646,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> + <marker id="Erlang Timers" /> + <title>Erlang Timers</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. + 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> - 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>. + 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> - 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. - </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[ ... @@ -777,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> @@ -804,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 @@ -842,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 @@ -858,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 @@ -937,6 +1126,59 @@ do_unlock() -> <!-- =================================================================== --> <section> + <marker id="State Entry Actions" /> + <title>State Entry Actions</title> + <p> + Say you have a state machine specification + that uses state entry actions. + Allthough you can code this using self-generated events + (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 Calls">state enter calls</seealso>. + </p> + <p> + You return a list containing <c>state_enter</c> from your + <seealso marker="stdlib:gen_statem#Module:callback_mode/0"><c>callback_mode/0</c></seealso> + function and the <c>gen_statem</c> engine will call your + state callback once with the arguments + <c>(enter, OldState, ...)</c> whenever the state changes. + Then you just need to handle these event-like calls in all states. + </p> + <code type="erl"><![CDATA[ +... +init(Code) -> + process_flag(trap_exit, true), + Data = #{code => Code}, + {ok, locked, Data}. + +callback_mode() -> + [state_functions,state_enter]. + +locked(enter, _OldState, Data) -> + do_lock(), + {keep_state,Data#{remaining => Code}}; +locked( + cast, {button,Digit}, + #{code := Code, remaining := Remaining} = Data) -> + case Remaining of + [Digit] -> + {next_state, open, Data}; +... + +open(enter, _OldState, _Data) -> + do_unlock(), + {keep_state_and_data, [{state_timeout,10000,lock}]}; +open(state_timeout, lock, Data) -> + {next_state, locked, Data}; +... + ]]></code> + </section> + +<!-- =================================================================== --> + + <section> + <marker id="Self-Generated Events" /> <title>Self-Generated Events</title> <p> It can sometimes be beneficial to be able to generate events @@ -954,64 +1196,71 @@ do_unlock() -> from your state machine to itself. </p> <p> - One example of using self-generated events can be when you have - a state machine specification that uses state entry actions. - You can code that using a dedicated function - to do the state transition. But if you want that code to be - visible besides the other state logic, you can insert - an <c>internal</c> event that does the entry actions. - This has the same unfortunate consequence as using - state transition functions: everywhere you go to - the state, you must explicitly - insert the <c>internal</c> event - or use a state transition function. + One example for this is to pre-process incoming data, for example + decrypting chunks or collecting characters up to a line break. + Purists may argue that this should be modelled with a separate + state machine that sends pre-processed events + to the main state machine. + But to decrease overhead the small pre-processing state machine + can be implemented in the common state event handling + of the main state machine using a few state data variables + that then sends the pre-processed events as internal events + to the main state machine. </p> <p> - The following is an implementation of entry actions - using <c>internal</c> events with content <c>enter</c> - using a helper function <c>enter/3</c> for state entry: + 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> <code type="erl"><![CDATA[ ... -init(Code) -> - process_flag(trap_exit, true), - Data = #{code => Code}, - enter(ok, locked, Data). +-export(put_chars/1, enter/0). +... +put_chars(Chars) when is_binary(Chars) -> + gen_statem:call(?NAME, {chars,Chars}). -callback_mode() -> - state_functions. +enter() -> + gen_statem:call(?NAME, enter). -locked(internal, enter, _Data) -> - do_lock(), - {keep_state,Data#{remaining => Code}}; -locked( - cast, {button,Digit}, - #{code := Code, remaining := Remaining} = Data) -> - case Remaining of - [Digit] -> - enter(next_state, open, Data); ... -open(internal, enter, _Data) -> - Tref = erlang:start_timer(10000, self(), lock), - do_unlock(), - {keep_state,Data#{timer => Tref}}; -open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> - enter(next_state, locked, Data); +locked(enter, _OldState, Data) -> + do_lock(), + {keep_state,Data#{remaining => Code, buf => []}}; ... -enter(Tag, State, Data) -> - {Tag,State,Data,[{next_event,internal,enter}]}. +handle_event({call,From}, {chars,Chars}, #{buf := Buf} = Data) -> + {keep_state, Data#{buf := [Chars|Buf], + [{reply,From,ok}]}; +handle_event({call,From}, enter, #{buf := Buf} = Data) -> + Chars = unicode:characters_to_binary(lists:reverse(Buf)), + try binary_to_integer(Chars) of + Digit -> + {keep_state, Data#{buf := []}, + [{reply,From,ok}, + {next_event,internal,{button,Chars}}]} + catch + error:badarg -> + {keep_state, Data#{buf := []}, + [{reply,From,{error,not_an_integer}}]} + end; +... ]]></code> + <p> + If you start this program with <c>code_lock:start([17])</c> + you can unlock with <c>code_lock:put_chars(<<"001">>), + code_lock:put_chars(<<"7">>), code_lock:enter()</c>. + </p> </section> <!-- =================================================================== --> <section> + <marker id="Example Revisited" /> <title>Example Revisited</title> <p> - This section includes the example after all mentioned modifications - and some more using the entry actions, + 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"> @@ -1027,6 +1276,7 @@ enter(Tag, State, Data) -> </p> <section> + <marker id="Callback Mode: state_functions" /> <title>Callback Mode: state_functions</title> <p> Using state functions: @@ -1054,43 +1304,44 @@ code_length() -> init(Code) -> process_flag(trap_exit, true), Data = #{code => Code}, - enter(ok, locked, Data). + {ok, locked, Data}. callback_mode() -> - state_functions. + [state_functions,state_enter]. -locked(internal, enter, #{code := Code} = Data) -> +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) -> case Remaining of [Digit] -> % Complete - enter(next_state, open, Data); + {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(internal, enter, 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) -> - enter(next_state, locked, 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)}]}. -enter(Tag, State, Data) -> - {Tag,State,Data,[{next_event,internal,enter}]}. + {keep_state_and_data, [{reply,From,length(Code)}]}. do_lock() -> io:format("Locked~n", []). @@ -1106,13 +1357,14 @@ 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 to use one <c>handle_event/4</c> function. The previously used approach to first branch depending on event - does not work that well here because of the generated - entry actions, so this example first branches depending on state: + does not work that well here because of the state enter calls, + so this example first branches depending on state: </p> <code type="erl"><![CDATA[ ... @@ -1120,44 +1372,49 @@ code_change(_Vsn, State, Data, _Extra) -> ... callback_mode() -> - handle_event_function. + [handle_event_function,state_enter]. %% State: locked -handle_event(internal, enter, 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) -> case Remaining of [Digit] -> % Complete - enter(next_state, open, Data); + {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(internal, enter, 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) -> - enter(next_state, locked, 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> @@ -1165,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 @@ -1225,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> @@ -1304,71 +1563,68 @@ set_lock_button(LockButton) -> init({Code,LockButton}) -> process_flag(trap_exit, true), - Data = #{code => Code, remaining => undefined, timer => undefined}, - enter(ok, {locked,LockButton}, Data, []). + Data = #{code => Code, remaining => undefined}, + {ok, {locked,LockButton}, Data}. callback_mode() -> - handle_event_function. + [handle_event_function,state_enter]. 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 - {internal,enter} -> + 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}]); + {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}, + {keep_state, Data#{remaining := Code}, [{reply,From,ok}]} end end; +%% +%% State: open handle_event( EventType, EventContent, - {open,LockButton}, #{timer := Timer} = Data) -> - case {EventType,EventContent} of - {internal,enter} -> - 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}} -> - next_state({locked,LockButton}, Data, []); - {{call,From},{button,Digit}} -> + {keep_state_and_data, [{state_timeout,10000,lock}]}; + {state_timeout, lock} -> + {next_state, {locked,LockButton}, Data}; + {{call,From}, {button,Digit}} -> if Digit =:= LockButton -> - erlang:cancel_timer(Timer), - next_state( - {locked,LockButton}, Data, - [{reply,From,locked}]); + {next_state, {locked,LockButton}, Data, + [{reply,From,locked}]); true -> {keep_state_and_data, [postpone]} end end. -next_state(State, Data, Actions) -> - enter(next_state, State, Data, Actions). -enter(Tag, State, Data, Actions) -> - {Tag,State,Data,[{next_event,internal,enter}|Actions]}. - do_lock() -> io:format("Locked~n", []). do_unlock() -> @@ -1405,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 @@ -1430,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 - {internal,enter} -> - 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 @@ -1458,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 |