From d86fd35ca0c65069955a34d6ae9fbc33b9663eb0 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 20 Mar 2018 03:34:23 -0700 Subject: Update User's Guide and pointers to it --- system/doc/design_principles/statem.xml | 543 ++++++++++++++++++++++++-------- 1 file changed, 412 insertions(+), 131 deletions(-) (limited to 'system/doc/design_principles') diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 5be2981f62..16f6ce8348 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -36,16 +36,6 @@ manual page in STDLIB, where all interface functions and callback functions are described in detail.

- -

- This is a new behavior in Erlang/OTP 19.0. - It has been thoroughly reviewed, is stable enough - to be used by at least two heavy OTP applications, and is here to stay. - Depending on user feedback, we do not expect - but can find it necessary to make minor - not backward compatible changes into Erlang/OTP 20.0. -

-
@@ -92,6 +82,31 @@ State(S) x Event(E) -> Actions(A), State(S')

+ + +
+ + Callback Module +

+ The callback module contains functions that implement + the state machine. + When an event occurs, + the gen_statem behaviour engine + calls a function in the callback module with the event, + current state and server data. + This function performs the actions for this event, + and returns the new state and server data + and also actions to be performed by the behaviour engine. +

+

+ The behaviour engine holds the state machine state, + server data, timer references, a queue of posponed messages + and other metadata. It receives all process messages, + handles the system messages, and calls the callback module + with machine specific events. +

+
+
@@ -100,54 +115,65 @@ State(S) x Event(E) -> Actions(A), State(S')

The gen_statem behavior supports two callback modes:

- + + + + state_functions + +

- In mode - state_functions, - the state transition rules are written as some Erlang - functions, which conform to the following convention: -

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

- This form is used in most examples here for example in section - Example. + Events are handled by one callback functions per state.

+ + + handle_event_function + +

- In mode - handle_event_function, - only one Erlang function provides all state transition rules: -

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

- See section - One Event Handler - for an example. + Events are handled by one single callback function.

-
+

- Both these modes allow other return tuples; see - Module:StateName/3 - in the gen_statem manual page. - These other return tuples can, for example, stop the machine, - execute state transition actions on the machine engine itself, - and send replies. + The callback mode is selected at server start + and may be changed with a code upgrade/downgrade. +

+

+ See the section + Event Handler + that describes the event handling callback function(s). +

+

+ The callback mode is selected by implementing a callback function + + Module:callback_mode() + + that returns one of the callback modes. +

+

+ The + + Module:callback_mode() + + function may also return a list containing the callback mode + and the atom state_enter in which case + state enter calls + are activated for the callback mode.

Choosing the Callback Mode +

+ The short version: choose state_functions - + it is the one most like gen_fsm. + But if you do not want the restriction that the state + must be an atom, or if having to write an event handler function + per state is not as you like it; please read on... +

The two callback modes @@ -186,7 +212,9 @@ handle_event(EventType, EventContent, State, Data) -> This mode works equally well when you want to focus on one event at the time or on one state at the time, but function - Module:handle_event/4 + + Module:handle_event/4 + quickly grows too large to handle without branching to helper functions.

@@ -208,36 +236,166 @@ handle_event(EventType, EventContent, State, Data) ->
- - State Enter Calls + + Event Handler

- The gen_statem behavior can regardless of callback mode - automatically - - call the state callback - - 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: + Which callback function that handles an event + depends on the callback mode:

-
-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}.
+ + state_functions + + The event is handled by:
+ + Module:StateName(EventType, EventContent, Data) + +

+ This form is the one mostly used in the + Example + section. +

+
+ handle_event_function + + The event is handled by:
+ + Module:handle_event(EventType, EventContent, State, Data) + +

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

+
+

- 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 - - State Entry Actions + The state is either the name of the function itself or an argument to it. + The other arguments are the EventType described in section + Event Types, + the event dependent EventContent, and the current server Data. +

+

+ State enter calls are also handled by the event handler and have + slightly different arguments. See the section + State Enter Calls. +

+

+ The event handler return values are defined in the description of + + Module:StateName/3 - chapter. + in the gen_statem manual page, but here is + a more readable list:

+ + + {next_state, NextState, NewData, Actions}
+ {next_state, NextState, NewData} +
+ +

+ Set next state and update the server data. + If the Actions field is used, execute state transition actions. + An empty Actions list is equivalent to not returning the field. +

+

+ See section + Actions for a list of possible + state transition actions. +

+

+ If NextState =/= State the state machine changes + to a new state. A + state enter call + is performed if enabled and all + postponed events + are retried. +

+
+ + {keep_state, NewData, Actions}
+ {keep_state, NewData} +
+ +

+ Same as the next_state values with + NextState =:= State, that is no state change. +

+
+ + {keep_state_and_data, Actions}
+ keep_state_and_data +
+ +

+ Same as the keep_state values with + NextData =:= Data, that is no change in server data. +

+
+ + {repeat_state, NewData, Actions}
+ {repeat_state, NewData}
+ {repeat_state_and_data, Actions}
+ repeat_state_and_data +
+ +

+ Same as the keep_state or keep_state_and_data values, + and if state enter calls + are enabled, repeat that call. +

+
+ + {stop, Reason, NewData}
+ {stop, Reason} +
+ +

+ Stop the server with reason Reason. + If the NewData field is used, first update the server data. +

+
+ + {stop_and_reply, Reason, NewData, Actions}
+ {stop_and_reply, Reason, Actions} +
+ +

+ Same as the stop values, but first execute the given + state transition actions that may only be reply actions. +

+
+
+ +
+ + The First State +

+ To decide the first state the + + Module:init(Args) + + callback function is called before any + event handler + is called. This function behaves exactly as an event handler + function, but gets its only argument Args from + the gen_statem + + start/3,4 + + or + + start_link/3,4 + + function, and returns {ok, State, Data} + or {ok, State, Data, Actions}. + If you use the + postpone + action from this function, that action is ignored, + since there is no event to postpone. +

+
+
@@ -246,10 +404,8 @@ StateName(EventType, EventContent, Data) -> Actions

- In the first section - - Event-Driven State Machines - + In the first + section actions were mentioned as a part of the general state machine model. These general actions are implemented with the code that callback module @@ -264,72 +420,97 @@ StateName(EventType, EventContent, Data) -> These are ordered by returning a list of actions in the - return tuple + + return value + from the callback function. - These state transition actions affect the gen_statem - engine itself and can do the following: + These are the possible state transition actions:

- - + + - Postpone + postpone - the current event, see section +
+ {postpone, Boolean} +
+ + If set postpone the current event, see section Postponing Events - + - Hibernate + hibernate - the gen_statem, treated in +
+ {hibernate, Boolean} +
+ + If set hibernate the gen_statem, treated in section Hibernation - - Start a + - state time-out, - read more in section + {state_timeout, Time} + +
+ {state_timeout, Time, Opts} +
+ + Start a state time-out, read more in section State Time-Outs - - Start a + - generic time-out, - read more in section + {{timeout, Name}, Time} + +
+ {{timeout, Name}, Time, Opts} +
+ + Start a generic time-out, read more in section Generic Time-Outs + + + {timeout, Time} + +
+ {timeout, Time, Opts}
+ Time +
- Start an - event time-out, - see more in section + Start an event time-out, see more in section Event Time-Outs - + - Reply + {reply, From, Reply} - to a caller, mentioned at the end of section + + + Reply to a caller, mentioned at the end of section All State Events - - Generate the + - next event + {next_event, EventType, EventContent} - to handle, see section - Self-Generated Events + + + Generate the next event to handle, see section + Inserted Events -
+

- For details, see the - - gen_statem(3) - - manual page. + For details, see the gen_statem(3) + manual page for type + action(). You can, for example, reply to many callers, generate multiple next events, - and set time-outs to relative or absolute times. + and set a time-out to use absolute instead of relative time + (using the Opts field).

@@ -341,8 +522,8 @@ StateName(EventType, EventContent, Data) ->

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 + Events of all types are for a given state + handled in the same callback function, and that function gets EventType and EventContent as arguments.

@@ -350,12 +531,20 @@ StateName(EventType, EventContent, Data) -> they come from:

- cast + + + cast + + Generated by gen_statem:cast. - {call,From} + + + {call,From} + + Generated by gen_statem:call, @@ -364,12 +553,20 @@ StateName(EventType, EventContent, Data) -> {reply,From,Msg} or by calling gen_statem:reply. - info + + + info + + Generated by any regular process message sent to the gen_statem process. - state_timeout + + + state_timeout + + Generated by state transition action @@ -377,7 +574,11 @@ StateName(EventType, EventContent, Data) -> state timer timing out. - {timeout,Name} + + + {timeout,Name} + + Generated by state transition action @@ -385,7 +586,11 @@ StateName(EventType, EventContent, Data) -> generic timer timing out. - timeout + + + timeout + + Generated by state transition action @@ -394,7 +599,11 @@ StateName(EventType, EventContent, Data) -> (or its short form Time) event timer timing out. - internal + + + internal + + Generated by state transition action @@ -405,6 +614,61 @@ StateName(EventType, EventContent, Data) ->
+ + +
+ + State Enter Calls +

+ The gen_statem behavior can if this is enabled, + regardless of callback mode, + automatically + + call the state callback + + with special arguments whenever the state changes + so you can write state enter actions + near the rest of the state transition rules. + It typically looks like this: +

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

+ Since the state enter call is not an event there are restrictions + on the allowed return value and + state transition actions. + You may not change the state, + postpone + this non-event, or + insert events. +

+

+ The first state that is entered will get a state enter call + with OldState equal to the current state. +

+

+ You may repeat the state enter call using the {repeat_state,...} + return value from the + event handler. + In this case OldState will also be equal to the current state. +

+

+ 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 + + State Enter Actions + + chapter. +

+
+
@@ -1196,14 +1460,14 @@ do_unlock() ->
- - State Entry Actions + + State Enter Actions

Say you have a state machine specification - that uses state entry actions. - Allthough you can code this using self-generated events + that uses state enter actions. + Allthough you can code this using inserted events (described in the next section), especially if just - one or a few states has got state entry actions, + one or a few states has got state enter actions, this is a perfect use case for the built in state enter calls.

@@ -1244,7 +1508,7 @@ open(state_timeout, lock, Data) -> ... ]]>

- You can repeat the state entry code by returning one of + You can repeat the state enter code by returning one of {repeat_state, ...}, {repeat_state_and_data,_} or repeat_state_and_data that otherwise behaves exactly like their keep_state siblings. @@ -1259,8 +1523,8 @@ open(state_timeout, lock, Data) ->

- - Self-Generated Events + + Inserted Events

It can sometimes be beneficial to be able to generate events to your own state machine. @@ -1279,14 +1543,18 @@ open(state_timeout, lock, Data) ->

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 + 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. + Using internal events also can make it easier + to synchronize the state machines.

The following example uses an input model where you give the lock @@ -1800,10 +2068,23 @@ handle_event(

Another not uncommon scenario is to use the event time-out - to triger hibernation after a certain time of inactivity. + to trigger hibernation after a certain time of inactivity. + There is also a server start option + + {hibernate_after, Timeout} + + for + + start/3,4 + + or + + start_link/3,4 + + that may be used to automatically hibernate the server.

- This server probably does not use + This particular server probably does not use heap memory worth hibernating for. To gain anything from hibernation, your server would have to produce some garbage during callback execution, -- cgit v1.2.3 From 2699cd204f8cc2b3a4f457ff6d25651508db42b3 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 27 Mar 2018 10:22:07 +0200 Subject: Improve doc, change images to .svg --- system/doc/design_principles/Makefile | 12 +- system/doc/design_principles/code_lock.dia | Bin 2945 -> 2605 bytes system/doc/design_principles/code_lock.png | Bin 59827 -> 0 bytes system/doc/design_principles/code_lock.svg | 132 +++++++ system/doc/design_principles/code_lock_2.dia | Bin 2956 -> 2854 bytes system/doc/design_principles/code_lock_2.png | Bin 55553 -> 0 bytes system/doc/design_principles/code_lock_2.svg | 140 +++++++ system/doc/design_principles/statem.xml | 550 ++++++++++++++++----------- 8 files changed, 610 insertions(+), 224 deletions(-) delete mode 100644 system/doc/design_principles/code_lock.png create mode 100644 system/doc/design_principles/code_lock.svg delete mode 100644 system/doc/design_principles/code_lock_2.png create mode 100644 system/doc/design_principles/code_lock_2.svg (limited to 'system/doc/design_principles') diff --git a/system/doc/design_principles/Makefile b/system/doc/design_principles/Makefile index 5743a50b47..1f570f5271 100644 --- a/system/doc/design_principles/Makefile +++ b/system/doc/design_principles/Makefile @@ -1,7 +1,7 @@ # # %CopyrightBegin% # -# Copyright Ericsson AB 1997-2016. All Rights Reserved. +# Copyright Ericsson AB 1997-2018. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -57,11 +57,11 @@ GIF_FILES = \ sup5.gif \ sup6.gif -PNG_FILES = \ - code_lock.png \ - code_lock_2.png +SVG_FILES = \ + code_lock.svg \ + code_lock_2.svg -IMAGE_FILES = $(GIF_FILES) $(PNG_FILES) +IMAGE_FILES = $(GIF_FILES) $(SVG_FILES) XML_FILES = \ $(BOOK_FILES) $(XML_CHAPTER_FILES) \ @@ -90,7 +90,7 @@ _create_dirs := $(shell mkdir -p $(HTMLDIR)) $(HTMLDIR)/%.gif: %.gif $(INSTALL_DATA) $< $@ -$(HTMLDIR)/%.png: %.png +$(HTMLDIR)/%.svg: %.svg $(INSTALL_DATA) $< $@ docs: html diff --git a/system/doc/design_principles/code_lock.dia b/system/doc/design_principles/code_lock.dia index eaa2aca5b0..fe43d6da2c 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 deleted file mode 100644 index 40bd35fc74..0000000000 Binary files a/system/doc/design_principles/code_lock.png and /dev/null differ diff --git a/system/doc/design_principles/code_lock.svg b/system/doc/design_principles/code_lock.svg new file mode 100644 index 0000000000..223e121486 --- /dev/null +++ b/system/doc/design_principles/code_lock.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + + locked + + + + + + + open + + + + + + + {button,Button} + + + + + + + Correct Code? + + + + + + + do_lock() + + + + + + + state_timeout + + + + + + + init + + + + + + + + + + + + + + + Y + + + N + + + + + + + + + + + {button,Digit} + + + + + + + + + + + + do_lock() + Clear Buttons + + + + + + + do_unlock() + Clear Buttons + state_timeout 10 s + + + + + + + + + + + + + + + + + + Collect Buttons + + + + + + + + + diff --git a/system/doc/design_principles/code_lock_2.dia b/system/doc/design_principles/code_lock_2.dia index 3b9ba554d8..31eb0fb6eb 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 deleted file mode 100644 index 3aca9dd5aa..0000000000 Binary files a/system/doc/design_principles/code_lock_2.png and /dev/null differ diff --git a/system/doc/design_principles/code_lock_2.svg b/system/doc/design_principles/code_lock_2.svg new file mode 100644 index 0000000000..d3e15e7577 --- /dev/null +++ b/system/doc/design_principles/code_lock_2.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + locked + + + + + + + open + + + + + + + {button,Button} + + + + + + + do_unlock() + state_timeout 10 s + + + + + + + do_lock() + Clear Buttons + + + + + + + state_timeout + + + + + + + init + + + + + + + + + + + + + + + + + + + + + + + + + + + state_timeout 30 s + + + + + + + state_timeout + + + + + + + Collect Buttons + + + + + + + + + + + + + Clear Buttons + + + + + + + + + + Correct Code? + + + + N + + + Y + + + + + + + + diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 16f6ce8348..5269d23487 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -356,8 +356,8 @@ State(S) x Event(E) -> Actions(A), State(S')

- {stop_and_reply, Reason, NewData, Actions}
- {stop_and_reply, Reason, Actions} + {stop_and_reply, Reason, NewData, ReplyActions}
+ {stop_and_reply, Reason, ReplyActions}

@@ -377,7 +377,7 @@ State(S) x Event(E) -> Actions(A), State(S') callback function is called before any event handler - is called. This function behaves exactly as an event handler + is called. This function behaves like an event handler function, but gets its only argument Args from the gen_statem @@ -678,13 +678,14 @@ StateName(EventType, EventContent, Data) -> A door with a code lock can be seen as a state machine. Initially, the door is locked. When someone presses a button, an event is generated. - Depending on what buttons have been pressed before, - the sequence so far can be correct, incomplete, or wrong. + The pressed buttons are collected, up to the number of buttons + in the correct code. If correct, the door is unlocked for 10 seconds (10,000 milliseconds). - If incomplete, we wait for another button to be pressed. If - wrong, we start all over, waiting for a new button sequence. + If not correct, we wait for a new button to be pressed.

- + + Code Lock State Diagram

@@ -698,37 +699,44 @@ StateName(EventType, EventContent, Data) -> -export([start_link/1]). -export([button/1]). --export([init/1,callback_mode/0,terminate/3,code_change/4]). +-export([init/1,callback_mode/0,terminate/3]). -export([locked/3,open/3]). start_link(Code) -> gen_statem:start_link({local,?NAME}, ?MODULE, Code, []). -button(Digit) -> - gen_statem:cast(?NAME, {button,Digit}). +button(Button) -> + gen_statem:cast(?NAME, {button,Button}). init(Code) -> do_lock(), - Data = #{code => Code, remaining => Code}, + Data = #{code => Code, length => length(Code), buttons => []}, {ok, locked, Data}. callback_mode() -> state_functions. - + ]]> + - case Remaining of - [Digit] -> + cast, {button,Button}, + #{code := Code, length := Length, buttons := Buttons} = Data) -> + NewButtons = + if + length(Buttons) < Length -> + Buttons; + true -> + tl(Buttons) + end ++ [Button], + if + NewButtons =:= Code -> % Correct do_unlock(), - {next_state, open, Data#{remaining := Code}, + {next_state, open, Data#{buttons := []}, [{state_timeout,10000,lock}]}; - [Digit|Rest] -> % Incomplete - {next_state, locked, Data#{remaining := Rest}}; - _Wrong -> - {next_state, locked, Data#{remaining := Code}} + true -> % Incomplete | Incorrect + {next_state, locked, Data#{buttons := NewButtons}} end. - + ]]> + do_lock(), {next_state, locked, Data}; @@ -743,8 +751,6 @@ do_unlock() -> terminate(_Reason, State, _Data) -> State =/= locked andalso do_lock(), ok. -code_change(_Vsn, State, Data, _Extra) -> - {ok, State, Data}. ]]>

The code is explained in the next sections.

@@ -820,17 +826,17 @@ start_link(Code) -> in this case locked; assuming that the door is locked to begin with. Data is the internal server data of the gen_statem. Here the server data is a map - with key code that stores - the correct button sequence, and key remaining - that stores the remaining correct button sequence - (the same as the code to begin with). + with key code that stores the correct button sequence, + key length store its length, + and key buttons that stores the collected buttons + up to the same length.

do_lock(), - Data = #{code => Code, remaining => Code}, - {ok,locked,Data}. + Data = #{code => Code, length => length(Code), buttons => []}, + {ok, locked, Data}. ]]>

Function gen_statem:start_link @@ -848,10 +854,6 @@ init(Code) -> a gen_statem that is not part of a supervision tree.

- - state_functions. - ]]>

Function Module:callback_mode/0 @@ -859,8 +861,12 @@ callback_mode() -> CallbackMode for the callback module, in this case state_functions. - That is, each state has got its own handler function. + That is, each state has got its own handler function:

+ + state_functions. + ]]>
@@ -884,7 +890,7 @@ button(Digit) -> {button,Digit} is the event content.

- The event is made into a message and sent to the gen_statem. + The event is 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}, @@ -893,44 +899,48 @@ button(Digit) -> NewStateName is the name of the next state to go to. NewData is a new value for the server data of the gen_statem, and Actions is a list of - actions on the gen_statem engine. + actions to be performed by the gen_statem engine.

+ - case Remaining of - [Digit] -> % Complete + cast, {button,Button}, + #{code := Code, length := Length, buttons := Buttons} = Data) -> + NewButtons = + if + length(Buttons) < Length -> + Buttons; + true -> + tl(Buttons) + end ++ [Button], + if + NewButtons =:= Code -> % Correct do_unlock(), - {next_state, open, Data#{remaining := Code}, + {next_state, open, Data#{buttons := []}, [{state_timeout,10000,lock}]}; - [Digit|Rest] -> % Incomplete - {next_state, locked, Data#{remaining := Rest}}; - [_|_] -> % Wrong - {next_state, locked, Data#{remaining := Code}} + true -> % Incomplete | Incorrect + {next_state, locked, Data#{buttons := NewButtons}} end. - -open(state_timeout, lock, Data) -> - do_lock(), - {next_state, locked, Data}; -open(cast, {button,_}, Data) -> - {next_state, open, Data}. ]]>

- If the door is locked and a button is pressed, the pressed - button is compared with the next correct button. + In state locked, when a button is pressed, + it is collected with the last pressed buttons + up to the length of the correct dode, + and compared with the correct code. Depending on the result, the door is either unlocked and the gen_statem goes to state open, or the door remains in state locked.

- If the pressed button is incorrect, the server data - restarts from the start of the code sequence. -

-

- If the whole code is correct, the server changes states - to open. + When changing to state open, the collected + buttons are reset, the lock unlocked, and a state timer + for 10 s is started.

+ + + {next_state, open, Data}. + ]]>

In state open, a button event is ignored by staying in the same state. This can also be done @@ -948,7 +958,7 @@ open(cast, {button,_}, Data) -> the following tuple is returned from locked/2:

@@ -986,9 +996,9 @@ open(state_timeout, lock, Data) ->

Consider a code_length/0 function that returns the length of the correct code - (that should not be sensitive to reveal). + (that should not be too sensitive to reveal). We dispatch all events that are not state-specific - to the common function handle_event/3: + to the common function handle_common/3:

... locked(...) -> ... ; locked(EventType, EventContent, Data) -> - handle_event(EventType, EventContent, Data). + handle_common(EventType, EventContent, Data). ... open(...) -> ... ; open(EventType, EventContent, Data) -> - handle_event(EventType, EventContent, Data). + handle_common(EventType, EventContent, Data). -handle_event({call,From}, code_length, #{code := Code} = Data) -> +handle_common({call,From}, code_length, #{code := Code} = Data) -> {keep_state, Data, [{reply,From,length(Code)}]}. ]]> + +

+ Another way to do it is through a convenience macro + ?HANDLE_COMMON/3: +

+ + gen_statem:call(?NAME, code_length). + +-define(HANDLE_COMMON(T, C, D), + ?FUNCTION_NAME(T, C, D) -> handle_common((T), (C), (D))). +%% +handle_common({call,From}, code_length, #{code := Code} = Data) -> + {keep_state, Data, [{reply,From,length(Code)}]}. + +... +locked(...) -> ... ; +?HANDLE_COMMON. + +... +open(...) -> ... ; +?HANDLE_COMMON. + ]]> +

This example uses gen_statem:call/2, @@ -1047,16 +1085,22 @@ callback_mode() -> handle_event(cast, {button,Digit}, State, #{code := Code} = Data) -> case State of locked -> - case maps:get(remaining, Data) of - [Digit] -> % Complete - do_unlock(), - {next_state, open, Data#{remaining := Code}, + #{length := Length, buttons := Buttons} = Data, + NewButtons = + if + length(Buttons) < Length -> + Buttons; + true -> + tl(Buttons) + end ++ [Button], + if + NewButtons =:= Code -> % Correct + do_unlock(), + {next_state, open, Data#{buttons := []}, [{state_timeout,10000,lock}]}; - [Digit|Rest] -> % Incomplete - {keep_state, Data#{remaining := Rest}}; - [_|_] -> % Wrong - {keep_state, Data#{remaining := Code}} - end; + true -> % Incomplete | Incorrect + {keep_state, Data#{buttons := NewButtons}} + end; open -> keep_state_and_data end; @@ -1165,16 +1209,15 @@ stop() -> - {next_state, locked, Data#{remaining := Code}}; +locked(timeout, _, Data) -> + {next_state, locked, Data#{buttons := []}}; locked( cast, {button,Digit}, - #{code := Code, remaining := Remaining} = Data) -> + #{code := Code, length := Length, buttons := Buttons} = Data) -> ... - [Digit|Rest] -> % Incomplete - {next_state, locked, Data#{remaining := Rest}, 30000}; + true -> % Incomplete | Incorrect + {next_state, locked, Data#{buttons := NewButtons}, + 30000} ... ]]>

@@ -1189,6 +1232,13 @@ locked( Whatever event you act on has already cancelled the event time-out...

+

+ Note that an event time-out does not work well with + when you have for example a status call as in + All State Events, + or handle unknown events, since all kinds of events + will cancel the event time-out. +

@@ -1222,12 +1272,13 @@ locked( ... locked( cast, {button,Digit}, - #{code := Code, remaining := Remaining} = Data) -> - case Remaining of - [Digit] -> + #{code := Code, length := Length, buttons := Buttons} = Data) -> +... + if + NewButtons =:= Code -> % Correct do_unlock(), - {next_state, open, Data#{remaining := Code}, - [{{timeout,open_tm},10000,lock}]}; + {next_state, open, Data#{buttons := []}, + [{{timeout,open_tm},10000,lock}]}; ... open({timeout,open_tm}, lock, Data) -> @@ -1273,12 +1324,13 @@ open(cast, {button,_}, Data) -> ... locked( cast, {button,Digit}, - #{code := Code, remaining := Remaining} = Data) -> - case Remaining of - [Digit] -> + #{code := Code, length := Length, buttons := Buttons} = Data) -> +... + if + NewButtons =:= Code -> % Correct do_unlock(), Tref = erlang:start_timer(10000, self(), lock), - {next_state, open, Data#{remaining := Code, timer => Tref}}; + {next_state, open, Data#{buttons := [], timer => Tref}}; ... open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> @@ -1398,28 +1450,38 @@ start_link(Code) -> fun () -> true = register(?NAME, self()), do_lock(), - locked(Code, Code) + locked(Code, length(Code), []) end). -button(Digit) -> - ?NAME ! {button,Digit}. - -locked(Code, [Digit|Remaining]) -> +button(Button) -> + ?NAME ! {button,Button}. + ]]> + receive - {button,Digit} when Remaining =:= [] -> - do_unlock(), - open(Code); - {button,Digit} -> - locked(Code, Remaining); - {button,_} -> - locked(Code, Code) + {button,Button} -> + NewButtons = + if + length(Buttons) < Length -> + Buttons; + true -> + tl(Buttons) + end ++ [Button], + if + NewButtons =:= Code -> % Correct + do_unlock(), + open(Code, Length); + true -> % Incomplete | Incorrect + locked(Code, Length, NewButtons) + end end. - -open(Code) -> + ]]> + receive after 10000 -> do_lock(), - locked(Code, Code) + locked(Code, Length, []) end. do_lock() -> @@ -1483,7 +1545,7 @@ do_unlock() -> ... init(Code) -> process_flag(trap_exit, true), - Data = #{code => Code}, + Data = #{code => Code, length = length(Code)}, {ok, locked, Data}. callback_mode() -> @@ -1491,13 +1553,14 @@ callback_mode() -> locked(enter, _OldState, Data) -> do_lock(), - {keep_state,Data#{remaining => Code}}; + {keep_state,Data#{buttons => []}}; locked( cast, {button,Digit}, - #{code := Code, remaining := Remaining} = Data) -> - case Remaining of - [Digit] -> - {next_state, open, Data}; + #{code := Code, length := Length, buttons := Buttons} = Data) -> +... + if + NewButtons =:= Code -> % Correct + {next_state, open, Data}; ... open(enter, _OldState, _Data) -> @@ -1557,48 +1620,50 @@ open(state_timeout, lock, Data) -> to synchronize the state machines.

- 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. + The following example uses an input model where the buttons + generate up/down events and the lock responds to an up + event after the corresponding down event.

- gen_statem:call(?NAME, {chars,Chars}). +down(button) -> + gen_statem:cast(?NAME, {down,Button}). -enter() -> - gen_statem:call(?NAME, enter). +up(button) -> + gen_statem:cast(?NAME, {up,Button}). ... locked(enter, _OldState, Data) -> do_lock(), {keep_state,Data#{remaining => Code, buf => []}}; +locked( + internal, {button,Digit}, + #{code := Code, length := Length, buttons := Buttons} = Data) -> ... - -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}}]} + ]]> + + {keep_state, Data#{button := Button}; +handle_common(cast, {up,Button}, Data) -> + case Data of + #{button := Button} -> + {keep_state,maps:remove(button, Data), + [{next_event,internal,{button,Button}}]}; + #{} -> + keep_state_and_data end; ... + +open(internal, {button,_}, Data) -> + {keep_state,Data,[postpone]}; +... ]]>

If you start this program with code_lock:start([17]) - you can unlock with code_lock:put_chars(<<"001">>), - code_lock:put_chars(<<"7">>), code_lock:enter(). + you can unlock with code_lock:down(17), code_lock:up(17).

@@ -1612,13 +1677,15 @@ handle_event({call,From}, enter, #{buf := Buf} = Data) -> modifications and some more using state enter calls, which deserves a new state diagram:

- + + Code Lock State Diagram Revisited

Notice that this state diagram does not specify how to handle a button event in the state open. So, you need to - read somewhere else that unspecified events + read here that unspecified events must be ignored as in not consumed but handled in some other state. Also, the state diagram does not show that the code_length/0 call must be handled in every state. @@ -1636,8 +1703,8 @@ handle_event({call,From}, enter, #{buf := Buf} = Data) -> -define(NAME, code_lock_2). -export([start_link/1,stop/0]). --export([button/1,code_length/0]). --export([init/1,callback_mode/0,terminate/3,code_change/4]). +-export([down/1,up/1,code_length/0]). +-export([init/1,callback_mode/0,terminate/3]). -export([locked/3,open/3]). start_link(Code) -> @@ -1645,52 +1712,74 @@ start_link(Code) -> stop() -> gen_statem:stop(?NAME). -button(Digit) -> - gen_statem:cast(?NAME, {button,Digit}). +down(Digit) -> + gen_statem:cast(?NAME, {down,Digit}). +up(Digit) -> + gen_statem:cast(?NAME, {up,Digit}). code_length() -> gen_statem:call(?NAME, code_length). - + ]]> + process_flag(trap_exit, true), - Data = #{code => Code}, + Data = #{code => Code, length => Length, buttons => []}, {ok, locked, Data}. callback_mode() -> [state_functions,state_enter]. -locked(enter, _OldState, #{code := Code} = Data) -> +-define(HANDLE_COMMON, + ?FUNCTION_NAME(T, C, D) -> handle_common((T), (C), (D))). +%% +handle_common(cast, {down,Button}, Data) -> + {keep_state, Data#{button => Button}}; +handle_common(cast, {up,Button}, Data) -> + case Data of + #{button := Button} -> + {keep_state, maps:remove(button, Data), + [{next_event,internal,{button,Data}}]}; + #{} -> + keep_state_and_data + end; +handle_common({call,From}, code_length, #{code := Code}) -> + {keep_state_and_data, [{reply,From,length(Code)}]}. + ]]> + do_lock(), - {keep_state, Data#{remaining => Code}}; -locked( - timeout, _, - #{code := Code, remaining := Remaining} = Data) -> - {keep_state, Data#{remaining := Code}}; + {keep_state, Data#{buttons := []}}; +locked(state_timeout, button, Data) -> + {keep_state, Data#{buttons := []}}; locked( - cast, {button,Digit}, - #{code := Code, remaining := Remaining} = Data) -> - case Remaining of - [Digit] -> % Complete - {next_state, open, Data}; - [Digit|Rest] -> % Incomplete - {keep_state, Data#{remaining := Rest}, 30000}; - [_|_] -> % Wrong - {keep_state, Data#{remaining := Code}} + internal, {button,Digit}, + #{code := Code, length := Length, buttons := Buttons} = Data) -> + NewButtons = + if + length(Buttons) < Length -> + Buttons; + true -> + tl(Buttons) + end ++ [Button], + if + NewButtons =:= Code -> % Correct + do_unlock(), + {next_state, open, Data, + [{state_timeout,10000,lock}]}; + true -> % Incomplete | Incorrect + {keep_state, Data#{buttons := NewButtons}, + [{state_timeout,30000,button}]} end; -locked(EventType, EventContent, Data) -> - handle_event(EventType, EventContent, Data). - +?HANDLE_COMMON. + ]]> + do_unlock(), {keep_state_and_data, [{state_timeout,10000,lock}]}; open(state_timeout, lock, Data) -> {next_state, locked, Data}; -open(cast, {button,_}, _) -> +open(internal, {button,_}, _) -> {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)}]}. +?HANDLE_COMMON. do_lock() -> io:format("Locked~n", []). @@ -1700,8 +1789,6 @@ do_unlock() -> terminate(_Reason, State, _Data) -> State =/= locked andalso do_lock(), ok. -code_change(_Vsn, State, Data, _Extra) -> - {ok,State,Data}. ]]> @@ -1724,26 +1811,32 @@ callback_mode() -> [handle_event_function,state_enter]. %% State: locked -handle_event( - enter, _OldState, locked, - #{code := Code} = Data) -> +handle_event(enter, _OldState, locked, Data) -> do_lock(), - {keep_state, Data#{remaining => Code}}; + {keep_state, Data#{buttons := []}}; +handle_event(state_timeout, button, locked, Data) -> + {keep_state, Data#{buttons := []}}; 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 - {next_state, open, Data}; - [Digit|Rest] -> % Incomplete - {keep_state, Data#{remaining := Rest}, 30000}; - [_|_] -> % Wrong - {keep_state, Data#{remaining := Code}} + internal, {button,Digit}, locked, + #{code := Code, length := Length, buttons := Buttons} = Data) -> + NewButtons = + if + length(Buttons) < Length -> + Buttons; + true -> + tl(Buttons) + end ++ [Button], + if + NewButtons =:= Code -> % Correct + do_unlock(), + {next_state, open, Data, + [{state_timeout,10000,lock}]}; + true -> % Incomplete | Incorrect + {keep_state, Data#{buttons := NewButtons}, + [{state_timeout,30000,button}]} end; + ]]> + @@ -1753,10 +1846,22 @@ handle_event(state_timeout, lock, open, Data) -> {next_state, locked, Data}; handle_event(cast, {button,_}, open, _) -> {keep_state_and_data,[postpone]}; + ]]> + - {keep_state_and_data, [{reply,From,length(Code)}]}. +handle_event(cast, {down,Button}, _State, Data) -> + {keep_state, Data#{button => Button}}; +handle_event(cast, {up,Button}, _State, Data) -> + case Data of + #{button := Button} -> + {keep_state, maps:remove(button, Data), + [{state_timeout,30000,button}]}; + #{} -> + keep_state_and_data + end; +handle_event({call,From}, code_length, _State, #{length := Length}) -> + {keep_state_and_data, [{reply,From,Length}]}. ... ]]> @@ -1800,7 +1905,7 @@ handle_event({call,From}, code_length, _State, #{code := Code}) ->

@@ -1808,7 +1913,6 @@ format_status(Opt, [_PDict,State,Data]) -> {State, maps:filter( fun (code, _) -> false; - (remaining, _) -> false; (_, _) -> true end, Data)}, @@ -1896,7 +2000,7 @@ format_status(Opt, [_PDict,State,Data]) -> -export([start_link/2,stop/0]). -export([button/1,code_length/0,set_lock_button/1]). --export([init/1,callback_mode/0,terminate/3,code_change/4,format_status/2]). +-export([init/1,callback_mode/0,terminate/3,format_status/2]). -export([handle_event/4]). start_link(Code, LockButton) -> @@ -1911,10 +2015,11 @@ code_length() -> gen_statem:call(?NAME, code_length). set_lock_button(LockButton) -> gen_statem:call(?NAME, {set_lock_button,LockButton}). - + ]]> + process_flag(trap_exit, true), - Data = #{code => Code, remaining => undefined}, + Data = #{code => Code, length => length(Code), buttons => []}, {ok, {locked,LockButton}, Data}. callback_mode() -> @@ -1927,33 +2032,41 @@ handle_event( [{reply,From,OldLockButton}]}; handle_event( {call,From}, code_length, - {_StateName,_LockButton}, #{code := Code}) -> + {_StateName,_LockButton}, #{length := Length}) -> {keep_state_and_data, - [{reply,From,length(Code)}]}; + [{reply,From,Length}]}; + ]]> + +handle_event(EventType, EventContent, {locked,LockButton}, Data) -> case {EventType, EventContent} of {enter, _OldState} -> do_lock(), - {keep_state, Data#{remaining := Code}}; - {timeout, _} -> - {keep_state, Data#{remaining := Code}}; + {keep_state, Data#{buttons := []}}; + {state_timeout, button} -> + {keep_state, Data#{buttons := []}}; {{call,From}, {button,Digit}} -> - case Remaining of - [Digit] -> % Complete + #{length := Length, buttons := Buttons} = Data, + NewButtons = + if + length(Buttons) < Length -> + Buttons; + true -> + tl(Buttons) + end ++ [Button], + case Data of + #{code := NewButtons} -> {next_state, {open,LockButton}, Data, [{reply,From,ok}]}; - [Digit|Rest] -> % Incomplete - {keep_state, Data#{remaining := Rest}, - [{reply,From,ok}, 30000]}; - [_|_] -> % Wrong - {keep_state, Data#{remaining := Code}, - [{reply,From,ok}]} - end + #{} -> + {keep_state, Data#{buttons := NewButtons}, + [{reply,From,ok}, + {state_timeout,30000,button}]} + end end; + ]]> + do_unlock(), - {keep_state_and_data, [{state_timeout,10000,lock}]}; + {keep_state_and_data, + [{state_timeout,10000,lock}]}; {state_timeout, lock} -> {next_state, {locked,LockButton}, Data}; {{call,From}, {button,Digit}} -> @@ -1975,7 +2089,8 @@ handle_event( [postpone]} end end. - + ]]> + io:format("Locked~n", []). do_unlock() -> @@ -1984,8 +2099,6 @@ do_unlock() -> terminate(_Reason, State, _Data) -> State =/= locked andalso do_lock(), ok. -code_change(_Vsn, State, Data, _Extra) -> - {ok,State,Data}. format_status(Opt, [_PDict,State,Data]) -> StateData = {State, @@ -2046,7 +2159,8 @@ handle_event( {enter, _OldState} -> do_unlock(), {keep_state_and_data, - [{state_timeout,10000,lock},hibernate]}; + [{state_timeout,10000,lock}, + hibernate]}; ... ]]>

-- cgit v1.2.3 From 3ed7d729cab697b9f668dadb563d629de10f593d Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 10 Apr 2018 10:50:41 +0200 Subject: Fix timeout parsing and doc feedback --- system/doc/design_principles/statem.xml | 372 +++++++++++++++----------------- 1 file changed, 176 insertions(+), 196 deletions(-) (limited to 'system/doc/design_principles') diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 5269d23487..b44e169a9a 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -67,8 +67,8 @@ State(S) x Event(E) -> Actions(A), State(S')

As A and S' depend only on S and E, the kind of state machine described - here is a Mealy Machine - (see, for example, the corresponding Wikipedia article). + here is a Mealy machine + (see, for example, the Wikipedia article "Mealy machine").

Like most gen_ behaviors, gen_statem keeps @@ -78,7 +78,7 @@ State(S) x Event(E) -> Actions(A), State(S') or on the number of distinct input events, a state machine implemented with this behavior is in fact Turing complete. - But it feels mostly like an Event-Driven Mealy Machine. + But it feels mostly like an Event-Driven Mealy machine.

@@ -300,7 +300,10 @@ State(S) x Event(E) -> Actions(A), State(S')

See section - Actions for a list of possible + + State Transition Actions + + for a list of possible state transition actions.

@@ -401,8 +404,8 @@ State(S) x Event(E) -> Actions(A), State(S')

- - Actions + + State Transition Actions

In the first section @@ -415,9 +418,9 @@ State(S) x Event(E) -> Actions(A), State(S')

There are more specific state-transition actions - that a callback function can order the gen_statem + that a callback function can command the gen_statem engine to do after the callback function return. - These are ordered by returning a list of + These are commanded by returning a list of actions in the @@ -641,7 +644,7 @@ StateName(EventType, EventContent, Data) ->

Since the state enter call is not an event there are restrictions on the allowed return value and - state transition actions. + state transition actions. You may not change the state, postpone this non-event, or @@ -742,7 +745,8 @@ open(state_timeout, lock, Data) -> {next_state, locked, Data}; open(cast, {button,_}, Data) -> {next_state, open, Data}. - + ]]> + io:format("Lock~n", []). do_unlock() -> @@ -925,7 +929,7 @@ locked(

In state locked, when a button is pressed, it is collected with the last pressed buttons - up to the length of the correct dode, + up to the length of the correct code, and compared with the correct code. Depending on the result, the door is either unlocked and the gen_statem goes to state open, @@ -960,7 +964,7 @@ open(cast, {button,_}, Data) -> + ]]>

10,000 is a time-out value in milliseconds. After this time (10 seconds), a time-out occurs. @@ -1024,7 +1028,7 @@ handle_common({call,From}, code_length, #{code := Code} = Data) ->

Another way to do it is through a convenience macro - ?HANDLE_COMMON/3: + ?HANDLE_COMMON/0:

code_length() -> gen_statem:call(?NAME, code_length). --define(HANDLE_COMMON(T, C, D), - ?FUNCTION_NAME(T, C, D) -> handle_common((T), (C), (D))). +-define(HANDLE_COMMON, + ?FUNCTION_NAME(T, C, D) -> handle_common(T, C, D)). %% handle_common({call,From}, code_length, #{code := Code} = Data) -> {keep_state, Data, [{reply,From,length(Code)}]}. @@ -1047,7 +1051,7 @@ locked(...) -> ... ; ... open(...) -> ... ; ?HANDLE_COMMON. - ]]> +]]>

This example uses @@ -1059,6 +1063,14 @@ open(...) -> ... ; when you want to stay in the current state but do not know or care about what it is.

+

+ If the common event handler needs to know the current state + a function handle_common/4 can be used instead: +

+ handle_common(T, C, ?FUNCTION_NAME, D)). + ]]>
@@ -1109,7 +1121,7 @@ handle_event(state_timeout, lock, open, Data) -> {next_state, locked, Data}. ... - ]]> +]]> @@ -1141,7 +1153,7 @@ init(Args) -> process_flag(trap_exit, true), do_lock(), ... - ]]> + ]]>

When ordered to shut down, the gen_statem then calls callback function terminate(shutdown, State, Data). @@ -1155,7 +1167,7 @@ init(Args) -> terminate(_Reason, State, _Data) -> State =/= locked andalso do_lock(), ok. - ]]> + ]]>

@@ -1174,7 +1186,7 @@ terminate(_Reason, State, _Data) -> ... stop() -> gen_statem:stop(?NAME). - ]]> + ]]>

This makes the gen_statem call callback function terminate/3 just like for a supervised server @@ -1197,12 +1209,12 @@ stop() ->

It is ordered by the state transition action - {timeout,Time,EventContent}, or just Time, - or even just Time instead of an action list + {timeout,Time,EventContent}, or just an integer Time, + even without the enclosing actions list (the latter is a form inherited from gen_fsm.

- This type of time-out is useful to for example act on inactivity. + This type of time-out is useful for example to act on inactivity. Let us restart the code sequence if no button is pressed for say 30 seconds:

@@ -1219,7 +1231,7 @@ locked( {next_state, locked, Data#{buttons := NewButtons}, 30000} ... - ]]> +]]>

Whenever we receive a button event we start an event time-out of 30 seconds, and if we get an event type timeout @@ -1266,7 +1278,7 @@ locked(

Here is how to accomplish the state time-out in the previous example by instead using a generic time-out - named open_tm: + named for example open:

% Correct do_unlock(), {next_state, open, Data#{buttons := []}, - [{{timeout,open_tm},10000,lock}]}; + [{{timeout,open},10000,lock}]}; ... -open({timeout,open_tm}, lock, Data) -> +open({timeout,open}, lock, Data) -> do_lock(), {next_state,locked,Data}; open(cast, {button,_}, Data) -> {keep_state,Data}; ... - ]]> +]]>

- Just as - state time-outs - you can restart or cancel a specific generic time-out + An specific generic time-out can just as a + state time-out + be restarted or cancelled by setting it to a new time or infinity.

- 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. + In this particular case we do not need to cancel the timeout + since the timeout event is the only possible reason to + change the state from open to locked. +

+

+ Instead of bothering with when to cancel a time-out, + a late time-out event can be handled by ignoring it + if it arrives in a state where it is known to be late.

@@ -1309,7 +1326,7 @@ open(cast, {button,_}, Data) ->

The most versatile way to handle time-outs is to use Erlang Timers; see - erlang:start_timer3,4. + erlang:start_timer/3,4. Most time-out tasks can be performed with the time-out features in gen_statem, but an example of one that can not is if you should need @@ -1339,7 +1356,7 @@ open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> open(cast, {button,_}, Data) -> {keep_state,Data}; ... - ]]> +]]>

Removing the timer key from the map when we change to state locked is not strictly @@ -1379,7 +1396,9 @@ open(cast, {button,_}, Data) ->

Postponing is ordered by the state transition - action + + state transition action + postpone.

@@ -1392,14 +1411,17 @@ open(cast, {button,_}, Data) -> open(cast, {button,_}, Data) -> {keep_state,Data,[postpone]}; ... - ]]> +]]>

Since a postponed event is only retried after a state change, you have to think about where to keep a state data item. You can keep it in the server Data or in the State itself, for example by having two more or less identical states - to keep a boolean value, or by using a complex state with + to keep a boolean value, or by using a complex state + (see section + Complex State) + with callback mode handle_event_function. If a change in the value changes the set of events that is handled, @@ -1504,8 +1526,10 @@ do_unlock() -> passing non-system messages to the callback module.

- The state transition - action + The + + state transition action + postpone is designed to model selective receives. A selective receive implicitly postpones any not received events, but the postpone @@ -1569,7 +1593,7 @@ open(enter, _OldState, _Data) -> open(state_timeout, lock, Data) -> {next_state, locked, Data}; ... - ]]> +]]>

You can repeat the state enter code by returning one of {repeat_state, ...}, {repeat_state_and_data,_} @@ -1591,8 +1615,10 @@ open(state_timeout, lock, Data) ->

It can sometimes be beneficial to be able to generate events to your own state machine. - This can be done with the state transition - action + This can be done with the + + state transition action + {next_event,EventType,EventContent}.

@@ -1643,10 +1669,10 @@ locked( internal, {button,Digit}, #{code := Code, length := Length, buttons := Buttons} = Data) -> ... - ]]> +]]> - {keep_state, Data#{button := Button}; + {keep_state, Data#{button := Button}}; handle_common(cast, {up,Button}, Data) -> case Data of #{button := Button} -> @@ -1660,7 +1686,7 @@ handle_common(cast, {up,Button}, Data) -> open(internal, {button,_}, Data) -> {keep_state,Data,[postpone]}; ... - ]]> +]]>

If you start this program with code_lock:start([17]) you can unlock with code_lock:down(17), code_lock:up(17). @@ -1685,8 +1711,8 @@ open(internal, {button,_}, Data) ->

Notice that this state diagram does not specify how to handle a button event in the state open. So, you need to - read here that unspecified events - must be ignored as in not consumed but handled in some other state. + read in some side notes, that is, here: that unspecified events + shall be postponed (handled in some later state). Also, the state diagram does not show that the code_length/0 call must be handled in every state.

@@ -1719,17 +1745,17 @@ up(Digit) -> code_length() -> gen_statem:call(?NAME, code_length). ]]> - process_flag(trap_exit, true), - Data = #{code => Code, length => Length, buttons => []}, + Data = #{code => Code, length => length(Code), buttons => []}, {ok, locked, Data}. callback_mode() -> [state_functions,state_enter]. -define(HANDLE_COMMON, - ?FUNCTION_NAME(T, C, D) -> handle_common((T), (C), (D))). + ?FUNCTION_NAME(T, C, D) -> handle_common(T, C, D)). %% handle_common(cast, {down,Button}, Data) -> {keep_state, Data#{button => Button}}; @@ -1763,14 +1789,13 @@ locked( if NewButtons =:= Code -> % Correct do_unlock(), - {next_state, open, Data, - [{state_timeout,10000,lock}]}; + {next_state, open, Data}; true -> % Incomplete | Incorrect {keep_state, Data#{buttons := NewButtons}, [{state_timeout,30000,button}]} end; ?HANDLE_COMMON. - ]]> +]]> do_unlock(), @@ -1789,7 +1814,7 @@ do_unlock() -> terminate(_Reason, State, _Data) -> State =/= locked andalso do_lock(), ok. - ]]> + ]]>
@@ -1803,13 +1828,14 @@ terminate(_Reason, State, _Data) -> so this example first branches depending on state:

+ [handle_event_function,state_enter]. - + ]]> + do_lock(), @@ -1829,14 +1855,13 @@ handle_event( if NewButtons =:= Code -> % Correct do_unlock(), - {next_state, open, Data, - [{state_timeout,10000,lock}]}; + {next_state, open, Data}; true -> % Incomplete | Incorrect {keep_state, Data#{buttons := NewButtons}, [{state_timeout,30000,button}]} end; ]]> - @@ -1844,12 +1869,11 @@ handle_event(enter, _OldState, open, _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, _) -> +handle_event(internal, {button,_}, open, _) -> {keep_state_and_data,[postpone]}; ]]> - {keep_state, Data#{button => Button}}; handle_event(cast, {up,Button}, _State, Data) -> @@ -1862,13 +1886,11 @@ handle_event(cast, {up,Button}, _State, Data) -> end; handle_event({call,From}, code_length, _State, #{length := Length}) -> {keep_state_and_data, [{reply,From,Length}]}. - -... - ]]> + ]]>

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

@@ -1951,7 +1973,7 @@ format_status(Opt, [_PDict,State,Data]) -> state time-out, or one that affects the event handling in combination with postponing events. - We will complicate the previous example + We will go for the latter and complicate the previous example by introducing a configurable lock button (this is the state item in question), which in the open state immediately locks the door, @@ -1960,33 +1982,33 @@ format_status(Opt, [_PDict,State,Data]) ->

Suppose now that we call set_lock_button while the door is open, - and have already postponed a button event - that until now was not the lock button. - The sensible thing can be to say that - the button was pressed too early so it is - not to be recognized as the lock button. - However, then it can be surprising that a button event - that now is the lock button event arrives (as retried postponed) - immediately after the state transits to locked. -

-

- So we make the button/1 function synchronous - by using - gen_statem:call - and still postpone its events in the open state. - Then a call to button/1 during the open - state does not return until the state transits to locked, - as it is there the event is handled and the reply is sent. -

-

- If a process now calls set_lock_button/1 - to change the lock button while another process - hangs in button/1 with the new lock button, - it can be expected that the hanging lock button call - immediately takes effect and locks the lock. - Therefore, we make the current lock button a part of the state, - so that when we change the lock button, the state changes - and all postponed events are retried. + and we have already postponed a button event + that was the new lock button: +

+ code_lock:start_link([a,b,c], x). +{ok,<0.666.0>} +2> code_lock:button(a). +ok +3> code_lock:button(b). +ok +4> code_lock:button(c). +ok +Open +5> code_lock:button(y). +ok +6> code_lock:set_lock_button(y). +x +% What should happen here? Immediate lock or nothing? +]]> +

+ We could say that the button was pressed too early + so it is not to be recognized as the lock button. + Or we can make the lock button part of the state so + when we then change the lock button in the locked state, + the change becomes a state change + and all postponed events are retried, + therefore the lock is immediately locked!

We define the state as {StateName,LockButton}, @@ -1999,8 +2021,8 @@ format_status(Opt, [_PDict,State,Data]) -> -define(NAME, code_lock_3). -export([start_link/2,stop/0]). --export([button/1,code_length/0,set_lock_button/1]). --export([init/1,callback_mode/0,terminate/3,format_status/2]). +-export([button/1,set_lock_button/1]). +-export([init/1,callback_mode/0,terminate/3]). -export([handle_event/4]). start_link(Code, LockButton) -> @@ -2009,10 +2031,8 @@ start_link(Code, LockButton) -> stop() -> gen_statem:stop(?NAME). -button(Digit) -> - gen_statem:call(?NAME, {button,Digit}). -code_length() -> - gen_statem:call(?NAME, code_length). +button(Button) -> + gen_statem:cast(?NAME, {button,Button}). set_lock_button(LockButton) -> gen_statem:call(?NAME, {set_lock_button,LockButton}). ]]> @@ -2025,70 +2045,53 @@ init({Code,LockButton}) -> callback_mode() -> [handle_event_function,state_enter]. +%% State: locked +handle_event(enter, _OldState, {locked,_}, Data) -> + do_lock(), + {keep_state, Data#{buttons := []}}; +handle_event(state_timeout, button, {locked,_}, Data) -> + {keep_state, Data#{buttons := []}}; handle_event( - {call,From}, {set_lock_button,NewLockButton}, - {StateName,OldLockButton}, Data) -> - {next_state, {StateName,NewLockButton}, Data, - [{reply,From,OldLockButton}]}; -handle_event( - {call,From}, code_length, - {_StateName,_LockButton}, #{length := Length}) -> - {keep_state_and_data, - [{reply,From,Length}]}; + cast, {button,Digit}, {locked,LockButton}, + #{code := Code, length := Length, buttons := Buttons} = Data) -> + NewButtons = + if + length(Buttons) < Length -> + Buttons; + true -> + tl(Buttons) + end ++ [Button], + if + NewButtons =:= Code -> % Correct + do_unlock(), + {next_state, {open,LockButton}, Data}; + true -> % Incomplete | Incorrect + {keep_state, Data#{buttons := NewButtons}, + [{state_timeout,30000,button}]} + end; ]]> - case {EventType, EventContent} of - {enter, _OldState} -> - do_lock(), - {keep_state, Data#{buttons := []}}; - {state_timeout, button} -> - {keep_state, Data#{buttons := []}}; - {{call,From}, {button,Digit}} -> - #{length := Length, buttons := Buttons} = Data, - NewButtons = - if - length(Buttons) < Length -> - Buttons; - true -> - tl(Buttons) - end ++ [Button], - case Data of - #{code := NewButtons} -> - {next_state, {open,LockButton}, Data, - [{reply,From,ok}]}; - #{} -> - {keep_state, Data#{buttons := NewButtons}, - [{reply,From,ok}, - {state_timeout,30000,button}]} - end - end; +%% State: open +handle_event(enter, _OldState, {open,_}, _Data) -> + do_unlock(), + {keep_state_and_data, + [{state_timeout,10000,lock}]}; +handle_event(state_timeout, lock, {open,_}, Data) -> + {next_state, locked, Data}; +handle_event(cast, {button,LockButton}, {open,LockButton}, Data) -> + {next_state, {locked,LockButton}, Data}; +handle_event(cast, {button,_}, {open,_}, Data) -> + {keep_state_and_data,[postpone]}; ]]> - case {EventType, EventContent} of - {enter, _OldState} -> - do_unlock(), - {keep_state_and_data, - [{state_timeout,10000,lock}]}; - {state_timeout, lock} -> - {next_state, {locked,LockButton}, Data}; - {{call,From}, {button,Digit}} -> - if - Digit =:= LockButton -> - {next_state, {locked,LockButton}, Data, - [{reply,From,locked}]}; - true -> - {keep_state_and_data, - [postpone]} - end - end. + {call,From}, {set_lock_button,NewLockButton}, + {StateName,OldLockButton}, Data) -> + {next_state, {StateName,NewLockButton}, Data, + [{reply,From,OldLockButton}]}. ]]> @@ -2099,27 +2102,7 @@ do_unlock() -> terminate(_Reason, State, _Data) -> State =/= locked andalso do_lock(), ok. -format_status(Opt, [_PDict,State,Data]) -> - StateData = - {State, - maps:filter( - fun (code, _) -> false; - (remaining, _) -> false; - (_, _) -> true - end, - Data)}, - case Opt of - terminate -> - StateData; - normal -> - [{data,[{"State",StateData}]}] - end. ]]> -

- It can be an ill-fitting model for a physical code lock - that the button/1 call can hang until the lock - is locked. But for an API in general it is not that strange. -

@@ -2151,18 +2134,15 @@ format_status(Opt, [_PDict,State,Data]) ->

- case {EventType, EventContent} of - {enter, _OldState} -> - do_unlock(), - {keep_state_and_data, - [{state_timeout,10000,lock}, - hibernate]}; +handle_event(enter, _OldState, {open,_}, _Data) -> + do_unlock(), + {keep_state_and_data, + [{state_timeout,10000,lock}, + hibernate]}; ... - ]]> +]]>

The atom hibernate @@ -2175,9 +2155,8 @@ handle_event(

To change that we would need to insert action hibernate in more places. - For example, for the state-independent set_lock_button - and code_length operations that then would have to - be aware of using hibernate while in the + For example, the state-independent set_lock_button + operation would have to use hibernate but only in the {open,_} state, which would clutter the code.

@@ -2201,7 +2180,8 @@ handle_event( This particular server probably does not use heap memory worth hibernating for. To gain anything from hibernation, your server would - have to produce some garbage during callback execution, + have to produce non-insignificant garbage + during callback execution, for which this example server can serve as a bad example.

-- cgit v1.2.3 From b2a68e1e20b9ae41490e5d2777ed5c4f1147b26b Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 12 Apr 2018 11:01:55 +0200 Subject: Add a 'When to use' section --- system/doc/design_principles/statem.xml | 107 ++++++++++++++++++++++++++------ 1 file changed, 89 insertions(+), 18 deletions(-) (limited to 'system/doc/design_principles') diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index b44e169a9a..c44f240098 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -82,6 +82,69 @@ State(S) x Event(E) -> Actions(A), State(S')

+ + +
+ + When to use gen_statem +

+ If your process logic is convenient to describe as a state machine, + and you want any of these gen_statem key features: +

+ + + Gathered callback code for each state, + regardless of + Event Types. + (such as calls, casts and infos) + + + + Postponing Events + + (a substitute for selective receive) + + + + Inserted Events + + (in particular purely internal events) + + + + State Enter Calls + + (callback on state entry gathered with the rest + of the state callback code) + + + Higher level timeouts + ( + State Time-Outs, + Event Time-Outs + or + Generic Time-outs + (named time-outs)) + + +

+ If so, or if possibly needed in future versions, + then you should consider using gen_statem over + gen_server. +

+

+ For simple state machines not needing these fetures + gen_server + works just fine. + It also has got smaller call overhead, + but we are talking about something like 2 vs 3.3 microseconds + call roundtrip time here, so if the server callback + does just a little bit more than just replying, + or if the callback is not extremely frequent, + that difference will be hard to notice. +

+
+
@@ -160,7 +223,7 @@ State(S) x Event(E) -> Actions(A), State(S') function may also return a list containing the callback mode and the atom state_enter in which case - state enter calls + State Enter Calls are activated for the callback mode.

@@ -176,7 +239,7 @@ State(S) x Event(E) -> Actions(A), State(S')

The two - callback modes + Callback Modes give different possibilities and restrictions, but one goal remains: you want to handle all possible combinations of @@ -221,6 +284,8 @@ State(S) x Event(E) -> Actions(A), State(S')

The mode enables the use of non-atom states, for example, complex states or even hierarchical states. + See section + Complex State. If, for example, a state diagram is largely alike for the client side and the server side of a protocol, you can have a state {StateName,server} or @@ -344,7 +409,10 @@ State(S) x Event(E) -> Actions(A), State(S')

Same as the keep_state or keep_state_and_data values, - and if state enter calls + and if + + State Enter Calls + are enabled, repeat that call.

@@ -379,7 +447,7 @@ State(S) x Event(E) -> Actions(A), State(S') Module:init(Args) callback function is called before any - event handler + Event Handler is called. This function behaves like an event handler function, but gets its only argument Args from the gen_statem @@ -407,8 +475,10 @@ State(S) x Event(E) -> Actions(A), State(S') State Transition Actions

- In the first - section + 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 @@ -644,7 +714,7 @@ StateName(EventType, EventContent, Data) ->

Since the state enter call is not an event there are restrictions on the allowed return value and - state transition actions. + State Transition Actions. You may not change the state, postpone this non-event, or @@ -657,7 +727,7 @@ StateName(EventType, EventContent, Data) ->

You may repeat the state enter call using the {repeat_state,...} return value from the - event handler. + Event Handler. In this case OldState will also be equal to the current state.

@@ -1301,8 +1371,8 @@ open(cast, {button,_}, Data) -> ... ]]>

- An specific generic time-out can just as a - state time-out + Specific generic time-outs can just as + State Time-Outs be restarted or cancelled by setting it to a new time or infinity.

@@ -1397,7 +1467,7 @@ open(cast, {button,_}, Data) ->

Postponing is ordered by the state transition - state transition action + State Transition Action postpone.

@@ -1422,7 +1492,7 @@ open(cast, {button,_}, Data) -> (see section Complex State) with - callback mode + Callback Mode handle_event_function. If a change in the value changes the set of events that is handled, then the value should be kept in the State. @@ -1527,8 +1597,8 @@ do_unlock() ->

The - - state transition action + + State Transition Action postpone is designed to model selective receives. A selective receive implicitly postpones @@ -1555,7 +1625,7 @@ do_unlock() -> (described in the next section), especially if just one or a few states has got state enter 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 @@ -1617,7 +1687,7 @@ open(state_timeout, lock, Data) -> to your own state machine. This can be done with the - state transition action + State Transition Action {next_event,EventType,EventContent}.

@@ -1970,7 +2040,7 @@ format_status(Opt, [_PDict,State,Data]) ->

One reason to use this is when you have a state item that when changed should cancel the - state time-out, + State Time-Out, or one that affects the event handling in combination with postponing events. We will go for the latter and complicate the previous example @@ -2160,7 +2230,8 @@ handle_event(enter, _OldState, {open,_}, _Data) -> {open,_} state, which would clutter the code.

- Another not uncommon scenario is to use the event time-out + Another not uncommon scenario is to use the + Event Time-Out to trigger hibernation after a certain time of inactivity. There is also a server start option -- cgit v1.2.3 From 549f6b20ef9c881d8c186739207be69cd8d2f7f7 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Mon, 16 Apr 2018 11:07:04 +0200 Subject: Fix after feedback on 'When to use' --- system/doc/design_principles/statem.xml | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) (limited to 'system/doc/design_principles') diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index c44f240098..ed6338e306 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -93,10 +93,10 @@ State(S) x Event(E) -> Actions(A), State(S')

- Gathered callback code for each state, + Co-located callback code for each state, regardless of - Event Types. - (such as calls, casts and infos) + Event Type + (such as call, cast and info) @@ -114,15 +114,14 @@ State(S) x Event(E) -> Actions(A), State(S') State Enter Calls - (callback on state entry gathered with the rest + (callback on state entry co-located with the rest of the state callback code) - Higher level timeouts - ( - State Time-Outs, + Easy-to-use timeouts + (State Time-Outs, Event Time-Outs - or + and Generic Time-outs (named time-outs)) @@ -133,14 +132,14 @@ State(S) x Event(E) -> Actions(A), State(S') gen_server.

- For simple state machines not needing these fetures + For simple state machines not needing these features gen_server works just fine. It also has got smaller call overhead, but we are talking about something like 2 vs 3.3 microseconds call roundtrip time here, so if the server callback does just a little bit more than just replying, - or if the callback is not extremely frequent, + or if the call is not extremely frequent, that difference will be hard to notice.

@@ -256,7 +255,7 @@ State(S) x Event(E) -> Actions(A), State(S') With state_functions, you are restricted to use atom-only states, and the gen_statem engine branches depending on state name for you. - This encourages the callback module to gather + This encourages the callback module to co-locate the implementation of all event actions particular to one state in the same place in the code, hence to focus on one state at the time. @@ -1686,7 +1685,7 @@ open(state_timeout, lock, Data) -> It can sometimes be beneficial to be able to generate events to your own state machine. This can be done with the - + State Transition Action {next_event,EventType,EventContent}. -- cgit v1.2.3 From 728bc036aa72a83080e933722f8ad409ede69f70 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Wed, 18 Apr 2018 16:26:58 +0200 Subject: Fix after feedback --- system/doc/design_principles/statem.xml | 121 ++++++++++++++++++++------------ 1 file changed, 76 insertions(+), 45 deletions(-) (limited to 'system/doc/design_principles') diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index ed6338e306..80ee9c992f 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -62,7 +62,8 @@ State(S) x Event(E) -> Actions(A), State(S')

These relations are interpreted as follows: if we are in state S and event E occurs, we are to perform actions A and make a transition to - state S'. Notice that S' can be equal to S. + state S'. Notice that S' can be equal to S + and that A can be empty.

As A and S' depend only on @@ -108,6 +109,7 @@ State(S) x Event(E) -> Actions(A), State(S') Inserted Events + that is: events from the state machine to itself (in particular purely internal events) @@ -115,7 +117,7 @@ State(S) x Event(E) -> Actions(A), State(S') State Enter Calls (callback on state entry co-located with the rest - of the state callback code) + of each state's callback code) Easy-to-use timeouts @@ -185,7 +187,7 @@ State(S) x Event(E) -> Actions(A), State(S')

- Events are handled by one callback functions per state. + Events are handled by one callback function per state.

@@ -209,7 +211,8 @@ State(S) x Event(E) -> Actions(A), State(S') that describes the event handling callback function(s).

- The callback mode is selected by implementing a callback function + The callback mode is selected by implementing + a mandatory callback function Module:callback_mode() @@ -233,16 +236,15 @@ State(S) x Event(E) -> Actions(A), State(S') The short version: choose state_functions - it is the one most like gen_fsm. But if you do not want the restriction that the state - must be an atom, or if having to write an event handler function - per state is not as you like it; please read on... + must be an atom, or if you do not want to write + one event handler function per state; please read on...

The two Callback Modes - give different possibilities - and restrictions, but one goal remains: - you want to handle all possible combinations of - events and states. + give different possibilities and restrictions, + with one common goal: + to handle all possible combinations of events and states.

This can be done, for example, by focusing on one state at the time @@ -386,7 +388,7 @@ State(S) x Event(E) -> Actions(A), State(S')

Same as the next_state values with - NextState =:= State, that is no state change. + NextState =:= State, that is, no state change.

@@ -396,7 +398,7 @@ State(S) x Event(E) -> Actions(A), State(S')

Same as the keep_state values with - NextData =:= Data, that is no change in server data. + NextData =:= Data, that is, no change in server data.

@@ -412,7 +414,8 @@ State(S) x Event(E) -> Actions(A), State(S') State Enter Calls - are enabled, repeat that call. + are enabled, repeat the state enter call + as if this state was entered again.

@@ -752,7 +755,7 @@ StateName(EventType, EventContent, Data) -> an event is generated. The pressed buttons are collected, up to the number of buttons in the correct code. - If correct, the door is unlocked for 10 seconds (10,000 milliseconds). + If correct, the door is unlocked for 10 seconds. If not correct, we wait for a new button to be pressed.