From d840b24857a1d54419953661f70716c449c11864 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 3 Mar 2016 10:54:01 +0100 Subject: Fix most of the system docs and emacs mode --- system/doc/definitions/term.defs | 1 + system/doc/design_principles/appup_cookbook.xml | 5 +++-- system/doc/design_principles/sup_princ.xml | 6 ++++-- system/doc/reference_manual/modules.xml | 3 ++- 4 files changed, 10 insertions(+), 5 deletions(-) (limited to 'system') diff --git a/system/doc/definitions/term.defs b/system/doc/definitions/term.defs index 6091a46a20..921175a7f0 100644 --- a/system/doc/definitions/term.defs +++ b/system/doc/definitions/term.defs @@ -76,6 +76,7 @@ the module Erlang in the application kernel","kenneth"}, {"gen_event","gen_event","A behaviour used for programming event handling mechanisms, such as alarm handlers, error loggers, and plug-and-play handlers.","mbj"}, {"gen_fsm","gen_fsm","A behaviour used for programming finite state machines.","mbj"}, {"gen_server","gen_server","A behaviour used for programming client-server processes.","mbj"}, +{"gen_statem","gen_statem","A behaviour used for programming generic state machines.","raimo"}, {"gterm","Global Glossary Database","A glossary database used to list common acronymns and defintions etc.","jocke"}, {"xref","xref","A cross reference tool that can be used for finding dependencies between functions, modules, applications and releases. Part of the Tools application.","gunilla"}, {"GSlong","Graphics System","A library module which provides a graphics interface for Erlang.","mbj"}, diff --git a/system/doc/design_principles/appup_cookbook.xml b/system/doc/design_principles/appup_cookbook.xml index 31335197d7..4f23c42c59 100644 --- a/system/doc/design_principles/appup_cookbook.xml +++ b/system/doc/design_principles/appup_cookbook.xml @@ -4,7 +4,7 @@
- 20032014 + 20032016 Ericsson AB. All Rights Reserved. @@ -50,7 +50,8 @@

In a system implemented according to the OTP design principles, all processes, except system processes and special processes, reside in one of the behaviours supervisor, - gen_server, gen_fsm, or gen_event. These + gen_server, gen_fsm, + gen_statem or gen_event. These belong to the STDLIB application and upgrading/downgrading normally requires an emulator restart.

OTP thus provides no support for changing residence modules except diff --git a/system/doc/design_principles/sup_princ.xml b/system/doc/design_principles/sup_princ.xml index 5e2f6ba9cb..a6b7dbb68d 100644 --- a/system/doc/design_principles/sup_princ.xml +++ b/system/doc/design_principles/sup_princ.xml @@ -4,7 +4,7 @@

- 19972014 + 19972016 Ericsson AB. All Rights Reserved. @@ -213,6 +213,7 @@ child_spec() = #{id => child_id(), % mandatory supervisor:start_link gen_server:start_link gen_fsm:start_link + gen_statem:start_link gen_event:start_link A function compliant with these functions. For details, see the supervisor(3) manual page. @@ -276,7 +277,8 @@ child_spec() = #{id => child_id(), % mandatory

modules are to be a list with one element [Module], where Module is the name of the callback module, if the child process is a supervisor, - gen_server or gen_fsm. If the child process is a gen_event, + gen_server, gen_fsm or gen_statem. + If the child process is a gen_event, the value shall be dynamic.

This information is used by the release handler during upgrades and downgrades, see diff --git a/system/doc/reference_manual/modules.xml b/system/doc/reference_manual/modules.xml index 5f2ac2a67d..96968b547e 100644 --- a/system/doc/reference_manual/modules.xml +++ b/system/doc/reference_manual/modules.xml @@ -4,7 +4,7 @@

- 20032015 + 20032016 Ericsson AB. All Rights Reserved. @@ -144,6 +144,7 @@ fact(0) -> % | gen_server gen_fsm + gen_statem gen_event supervisor -- cgit v1.2.3 From 7563c1929cd9bfceced790d0784210ea287d536e Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Wed, 6 Apr 2016 17:13:08 +0200 Subject: Write Design Principles chapter --- system/doc/design_principles/Makefile | 4 +- system/doc/design_principles/code_lock.dia | Bin 0 -> 2968 bytes system/doc/design_principles/code_lock.gif | Bin 0 -> 35031 bytes system/doc/design_principles/code_lock_2.dia | Bin 0 -> 2646 bytes system/doc/design_principles/code_lock_2.gif | Bin 0 -> 29420 bytes system/doc/design_principles/part.xml | 3 +- system/doc/design_principles/statem.xml | 1082 ++++++++++++++++++++++++++ system/doc/design_principles/xmlfiles.mk | 3 +- 8 files changed, 1089 insertions(+), 3 deletions(-) create mode 100644 system/doc/design_principles/code_lock.dia create mode 100644 system/doc/design_principles/code_lock.gif create mode 100644 system/doc/design_principles/code_lock_2.dia create mode 100644 system/doc/design_principles/code_lock_2.gif create mode 100644 system/doc/design_principles/statem.xml (limited to 'system') diff --git a/system/doc/design_principles/Makefile b/system/doc/design_principles/Makefile index 29df484279..2ccf477e83 100644 --- a/system/doc/design_principles/Makefile +++ b/system/doc/design_principles/Makefile @@ -1,7 +1,7 @@ # # %CopyrightBegin% # -# Copyright Ericsson AB 1997-2012. All Rights Reserved. +# Copyright Ericsson AB 1997-2016. 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. @@ -48,6 +48,8 @@ BOOK_FILES = book.xml GIF_FILES = \ note.gif \ clientserver.gif \ + code_lock.gif \ + code_lock_2.gif \ dist1.gif \ dist2.gif \ dist3.gif \ diff --git a/system/doc/design_principles/code_lock.dia b/system/doc/design_principles/code_lock.dia new file mode 100644 index 0000000000..d8262138a3 Binary files /dev/null and b/system/doc/design_principles/code_lock.dia differ diff --git a/system/doc/design_principles/code_lock.gif b/system/doc/design_principles/code_lock.gif new file mode 100644 index 0000000000..adcfee7eff Binary files /dev/null and b/system/doc/design_principles/code_lock.gif differ diff --git a/system/doc/design_principles/code_lock_2.dia b/system/doc/design_principles/code_lock_2.dia new file mode 100644 index 0000000000..4e82a9e1d6 Binary files /dev/null and b/system/doc/design_principles/code_lock_2.dia differ diff --git a/system/doc/design_principles/code_lock_2.gif b/system/doc/design_principles/code_lock_2.gif new file mode 100644 index 0000000000..150515c3b6 Binary files /dev/null and b/system/doc/design_principles/code_lock_2.gif differ diff --git a/system/doc/design_principles/part.xml b/system/doc/design_principles/part.xml index 42fd3beb6a..6495211e04 100644 --- a/system/doc/design_principles/part.xml +++ b/system/doc/design_principles/part.xml @@ -4,7 +4,7 @@
- 19972013 + 19972016 Ericsson AB. All Rights Reserved. @@ -31,6 +31,7 @@ + diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml new file mode 100644 index 0000000000..c10afe482d --- /dev/null +++ b/system/doc/design_principles/statem.xml @@ -0,0 +1,1082 @@ + + + + +
+ + 2016 + Ericsson AB. 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. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + gen_statem Behaviour + + + + + statem.xml +
+ +

+ This section is to be read with the + gen_statem(3) + manual page in STDLIB, where all interface functions and callback + functions are described in detail. +

+ + + +
+ Event Driven State Machines +

+ Established Automata theory does not deal much with + how a state transition is triggered, + but in general assumes that the output is a function + of the input (and the state) and that they are + some kind of values. +

+

+ For an Event Driven State Machine the input is an event + that triggers a state transition and the output + is actions executed during the state transition. + It can analogously to the mathematical model of a + Finite State Machine be described as + a set of relations of the form: +

+
+State(S) x Event(E) -> Actions(A), State(S')
+

These relations are interpreted as meaning:

+

+ If we are in state S and event E occurs, we + are to perform actions A and make a transition to + state S'. +

+

+ Note that S' may be equal to S. +

+

+ Since A and S' depend only on + S and E the kind of state machine described + here is a Mealy Machine. +

+

+ Like most gen_ behaviours, gen_statem keeps + a server Data besides the state. This and the fact that + there is no restriction on the number of states + (assuming enough virtual machine memory) + or on the number of distinct input events actually makes + a state machine implemented with this behaviour Turing complete. + But it feels mostly like an Event Driven Mealy Machine. +

+

+ The gen_statem behaviour supports two different + callback modes. In the mode state_functions, + the state transition rules are written as a number of Erlang + functions, which conform to the following convention: +

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

+ In the mode handle_event_function there is only one + Erlang function that implements all state transition rules: +

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

+ Both these modes allow other return tuples + that you can find in the + + reference manual. + + These other return tuples can for example stop the machine, + execute state transition actions on the machine engine itself + and send replies. +

+
+ + + +
+ Example +

+ This is an example starting off as equivalent to the the example in the + gen_fsm behaviour + description. In later chapters additions and tweaks are made + using features in gen_statem that gen_fsm does not have. + At the end of this section you can find the example again + with all the added features. +

+

+ A door with a code lock can be viewed as a state machine. + Initially, the door is locked. Anytime someone presses a button, + this generates an event. + Depending on what buttons have been pressed before, + the sequence so far can be correct, incomplete, or wrong. +

+

+ If it is correct, the door is unlocked for 10 seconds (10000 ms). + If it is incomplete, we wait for another button to be pressed. If + it is is wrong, we start all over, + waiting for a new button sequence. +

+ + Code lock state diagram + +

+ We can implement such a code lock state machine using + gen_statem with the following callback module: +

+ + + gen_statem:start_link({local,code_lock}, ?MODULE, Code, []). + +button(Digit) -> + gen_statem:cast(code_lock, {button,Digit}). + + +init(Code) -> + do_lock(), + Data = #{code => Code, remaining => Code}, + {state_functions,locked,Data}. + +locked( + cast, {button,Digit}, + #{code := Code, remaining := Remaining} = Data) -> + case Remaining of + [Digit] -> + do_unlock(), + {next_state,open,Data#{remaining := Code},10000}; + [Digit|Rest] -> % Incomplete + {next_state,locked,Data#{remaining := Rest}}; + _Wrong -> + {next_state,locked,Data#{remaining := Code}} + end. + +open(timeout, _, Data) -> + do_lock(), + {next_state,locked,Data}; +open(cast, {button,_}, Data) -> + do_lock(), + {next_state,locked,Data}. + +do_lock() -> + io:format("Lock~n", []). +do_unlock() -> + io:format("Unlock~n", []). + +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.

+
+ + + +
+ Starting gen_statem +

+ In the example in the previous section, the gen_statem is + started by calling code_lock:start_link(Code): +

+ + gen_statem:start_link({local,code_lock}, ?MODULE, Code, []). + ]]> +

+ start_link calls the function + + gen_statem:start_link/4 + + which spawns and links to a new process; a gen_statem. +

+ + +

+ The first argument, {local,code_lock}, specifies + the name. In this case, the gen_statem is locally + registered as code_lock. +

+

+ If the name is omitted, the gen_statem is not registered. + Instead its pid must be used. The name can also be given + as {global,Name}, in which case the gen_statem is + registered using + + global:register_name/2. + +

+
+ +

+ The second argument, ?MODULE, is the name of + the callback module, that is; the module where the callback + functions are located, which is this module. +

+

+ The interface functions (start_link/1 and button/1) + are located in the same module as the callback functions + (init/1, locked/3, and open/3). + It is normally good programming practice to have the client + side and the server side code contained in one module. +

+
+ +

+ The third argument, Code, is a list of digits that + is the correct unlock code which is passsed + to the callback function init/1. +

+
+ +

+ The fourth argument, [], is a list of options. See the + + gen_statem:start_link/3 + + manual page for available options. +

+
+
+

+ If name registration succeeds, the new gen_statem process + calls the callback function code_lock:init(Code). + This function is expected to return {CallbackMode,State,Data}, + where CallbackMode selects callback module state function + mode, in this case state_functions that is each state + has got its own handler function. + State is the initial state of the gen_statem, + in this case locked; assuming 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 the key code that stores + the correct button sequence and the key remaining + that stores the remaining correct button sequence + (the same as the code to begin with). +

+ + do_lock(), + Data = #{code => Code, remaining => Code}, + {state_functions,locked,Data}. + ]]> +

+ + gen_statem:start_link + + is synchronous. It does not return until the gen_statem + has been initialized and is ready to receive events. +

+

+ + gen_statem:start_link + + must be used if the gen_statem + is part of a supervision tree, that is; started by a supervisor. + There is another function; + + gen_statem:start + + to start a standalone gen_statem, that is; + a gen_statem that is not part of a supervision tree. +

+
+ + + +
+ Events and Handling them +

The function notifying the code lock about a button event is + implemented using + + gen_statem:cast/2: + +

+ + gen_statem:cast(code_lock, {button,Digit}). + ]]> +

+ code_lock is the name of the gen_statem and must + agree with the name used to start it. + {button,Digit} is the actual event content. +

+

+ The event is made into a message and sent to the gen_statem. + When the event is received, the gen_statem calls + StateName(cast, Event, Data), which is expected to + return a tuple {next_state,NewStateName,NewData}. + StateName is the name of the current state and + NewStateName is the name of the next state to go to. + NewData is a new value for the server data of + the gen_statem. +

+ + case Remaining of + [Digit] -> % Complete + do_unlock(), + {next_state,open,Data#{remaining := Code},10000}; + [Digit|Rest] -> % Incomplete + {next_state,locked,Data#{remaining := Rest}}; + [_|_] -> % Wrong + {next_state,locked,Data#{remaining := Code}} + end. + +open(timeout, _, Data) -> + do_lock(), + {next_state,locked,Data}; +open(cast, {button,_}, Data) -> + do_lock(), + {next_state,locked,Data}. + ]]> +

+ If the door is locked and a button is pressed, the pressed + button is compared with the next correct button and, + 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. +

+

+ In state open any button locks the door since + any event cancels the event timer so we will not get + a timeout event after a button event. +

+
+ +
+ Event Time-Outs +

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

+ +

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

+ + do_lock(), + {next_state,locked,Data}; + ]]> +
+ + + +
+ All State Events +

+ Sometimes an event can arrive in any state of the gen_statem. + It is convenient to handle these in a common state handler function + that all state functions call for events not specific to the state. +

+

+ Let's introduce a code_length/0 function that returns + the length of the correct code + (that should not be sensitive to reveal...). + We'll dispatch all events that are not state specific + to the common function handle_event/3. +

+ + gen_statem:call(code_lock, code_length). + +... +locked(...) -> ... ; +locked(EventType, EventContent, Data) -> + handle_event(EventType, EventContent, Data). + +... +open(...) -> ... ; +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)}]}. + ]]> +

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

+
+ + + +
+ One Event Handler +

+ If you use the mode handle_event_function + all events are handled in handle_event/4 and we + may (but do not have to) use an event-centered approach + where we dispatch on event first and then state: +

+ + Data = #{code => Code, remaining => Code}, + {handle_event_function,locked,Data}. + +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},10000}; + [Digit|Rest] -> % Incomplete + {keep_state,Data#{remaining := Rest}}; + [_|_] -> % Wrong + {keep_state,Data#{remaining := Code}} + end; + open -> + do_lock(), + {next_state,locked,Data} + end; +handle_event(timeout, _, open, Data) -> + do_lock(), + {next_state,locked,Data}. + +... + ]]> +
+ + + +
+ Stopping + +
+ In a Supervision Tree +

+ If the gen_statem is part of a supervision tree, + no stop function is needed. + The gen_statem is automatically terminated by its supervisor. + Exactly how this is done is defined by a + shutdown strategy + set in the supervisor. +

+

+ If it is necessary to clean up before termination, the shutdown + strategy must be a time-out value and the gen_statem must + in the init/1 function set itself to trap exit signals + by calling + + process_flag(trap_exit, true). + + When ordered to shutdown, the gen_statem then calls + the callback function + terminate(shutdown, State, Data): +

+ + process_flag(trap_exit, true), + ... + {CallbackMode,State,Data}. + ]]> +

+ In this example we let the terminate/3 function + lock the door if it is open so we do not accidentally leave the door + open when the supervision tree terminates. +

+ + State =/= locked andalso do_lock(), + ok. + ]]> +
+ +
+ Standalone gen_statem +

+ If the gen_statem is not part of a supervision tree, + it can be stopped using + + gen_statem:stop, + + preferably through an API function: +

+ + gen_statem:stop(code_lock). + ]]> +

+ This makes the gen_statem call the terminate/3 + callback function just like for a supervised server + and waits for the process to terminate. +

+
+
+ + + +
+ Actions +

+ In the first chapters we mentioned actions as a part of + the general state machine model, and these actions + are implemented with the code the gen_statem + callback module executes in an event handling + callback function before returning + to the gen_statem engine. +

+

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

+ + Postpone the current event. + Hibernate the gen_statem. + Start an event timeout. + Reply to a caller. + Generate the next event to handle. + +

+ We have mentioned the event timeout + and replying to a caller in the example above. + An example of event postponing comes in later in this chapter. + See the + + documentation + + for details. You can for example actually reply to several callers + and generate multiple next events to handle. +

+
+ + + +
+ Event Types +

+ So far we have mentioned a few + + event types. + + Events of all types are handled in the same callback function, + for a given state, and the function gets + EventType and EventContent as arguments. +

+

+ Here is the complete list of event types and where + they come from: +

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

+ The timeout event generated by the state transition action + {timeout,Time,EventContent} is an event timeout, + that is; if an event arrives the timer is cancelled. + You get either an event or a timeout but not both. +

+

+ Often you want a timer to not be cancelled by any event + or you want to start a timer in one state and respond + to the timeout in another. This can be accomplished + with a regular erlang timer: + + erlang:start_timer. + +

+

+ Looking at the example in this chapter so far; using the + gen_statem event timer has the consequence that + if a button event is generated while in the open state, + the timeout is cancelled and the button event is delivered. + Therefore we chose to lock the door if this happended. +

+

+ Suppose we do not want a button to lock the door, + instead we want to ignore button events in the open state. + Then we start a timer when entering the open state + and wait for it to expire while ignoring button events: +

+ + case Remaining of + [Digit] -> + do_unlock(), + Tref = erlang:start_timer(10000, self(), lock), + {next_state,open,Data#{remaining := Code, timer := Tref}}; +... + +open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> + do_lock(), + {next_state,locked,Data}; +open(cast, {button,_}, Data) -> + {keep_state,Data}; +... + ]]> +

+ If you need to cancel a timer due to some other event you can use + + erlang:cancel_timer(Tref). + + Note that a timeout message can not arrive after this, + unless you have postponed it before (why on earth one would do that). +

+

+ Another way to cancel a timer is to not cancel it, + but instead to ignore it if it arrives in a state + where it is known to be late. +

+
+ + + +
+ Postponing Events +

+ If you want to ignore a particular event in the current state + and handle it in a future state, you can postpone the event. + A postponed event is retried after the state has + changed i.e OldState =/= NewState. +

+

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

+

+ In this example, instead of ignoring button events + while in the open state we can postpone them + and they will be queued and later handled in the locked state: +

+ + {keep_state,Data,[postpone]}; +... + ]]> +
+ Fuzzy State Diagrams +

+ It is not uncommon that a state diagram does not specify + how to handle events that are not illustrated + in a particular state in the diagram. + Hopefully this is described in an associated text + or from the context. +

+

+ Possible actions may be; ignore as in drop the event + (maybe log it) or deal with the event in some other state + as in postpone it. +

+
+
+ Selective Receive +

+ Erlang's selective receive statement is often used to + describe simple state machine examples in straightforward + Erlang code. Here is a possible implementation of + the first example: +

+ + spawn( + fun () -> + true = register(code_lock, self()), + do_lock(), + locked(Code, Code) + end). + +button(Digit) -> + code_lock ! {button,Digit}. + +locked(Code, [Digit|Remaining]) -> + receive + {button,Digit} when Remaining =:= [] -> + do_unlock(), + open(Code); + {button,Digit} -> + locked(Code, Remaining); + {button,_} -> + locked(Code, Code) + end. + +open(Code) -> + receive + after 10000 -> + do_lock(), + locked(Code, Code) + end. + +do_lock() -> + io:format("Locked~n", []). +do_unlock() -> + io:format("Open~n", []). + ]]> +

+ The selective receive in this case causes open + to implicitly postpone any events to the locked state. +

+

+ The + + state transition action + + postpone is designed to be able to model + selective receive. Selective receive implicitly postpones + any not received events, but the postpone + state transition action explicitly postpones a received event. +

+

+ Other than that both mechanisms have got the same theoretical + time and memory complexity, while the selective receive + language construct has got smaller constant factors. +

+
+
+ + + +
+ Self Generated Events +

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

+

+ You can generate events of any existing + + type, + + but the internal type can only be generated through the + next_event action and hence can not come from an external source, + so you can be certain that an internal event is an event + from your state machine to itself. +

+

+ One example of using self generated events may be when you have + a state machine specification that uses state entry actions. + That you could code 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 internal event that does the entry actions. + This has the same unfortunate consequence as using + state transition functions that everywhere you go to + the state in question you will have to explicitly + insert the internal event + or use state transition function. +

+

+ Here is an implementation of entry actions + using internal events with content enter + utilizing a helper function enter/3 for state entry: +

+ + Data = #{code => Code}, + enter(state_functions, locked, Data). + +... +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); +... + +enter(Tag, State, Data) -> + {Tag,State,Data,[{next_event,internal,enter}]}. + ]]> +
+ + + +
+ Example Revisited +

+ Here is the example after all mentioned modifications + and some more utilizing the entry actions, + which deserves a new state diagram: +

+ + Code lock state diagram revisited + +

+ Note that this state diagram does not specify how to handle + a button event in the state open, so you will have to + read some other place that is here that unspecified events + shall be ignored as in not consumed but handled in some other state. + Nor does it show that the code_length/0 call shall be + handled in every state. +

+
+ Callback Mode: state_functions +

+ Using state functions: +

+ + gen_statem:start_link({local,code_lock}, ?MODULE, Code, []). +stop() -> + gen_statem:stop(code_lock). + +button(Digit) -> + gen_statem:cast(code_lock, {button,Digit}). +code_length() -> + gen_statem:call(code_lock, code_length). + +init(Code) -> + Data = #{code => Code}, + enter(state_functions, locked, Data). + +locked(internal, enter, #{code := Code} = Data) -> + do_lock(), + {keep_state,Data#{remaining => Code}}; +locked( + cast, {button,Digit}, + #{code := Code, remaining := Remaining} = Data) -> + case Remaining of + [Digit] -> % Complete + enter(next_state, open, Data); + [Digit|Rest] -> % Incomplete + {keep_state,Data#{remaining := Rest}}; + [_|_] -> % Wrong + {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), + do_unlock(), + {keep_state,Data#{timer => Tref}}; +open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) -> + enter(next_state, locked, Data); +open(cast, {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)}]}. +enter(Tag, State, Data) -> + {Tag,State,Data,[{next_event,internal,enter}]}. + +do_lock() -> + io:format("Locked~n", []). +do_unlock() -> + io:format("Open~n", []). + +terminate(_Reason, State, _Data) -> + State =/= locked andalso do_lock(), + ok. +code_change(_Vsn, State, Data, _Extra) -> + {ok,State,Data}. + ]]> +
+
+ Callback Mode: handle_event_function +

+ What to change to use one handle_event/4 function. + Here a clean first-dispatch-on-event approach + does not work that well due to the generated + entry actions: +

+ + process_flag(trap_exit, true), + Data = #{code => Code}, + enter(handle_event_function, locked, Data). + +... + +%% State: locked +handle_event(internal, enter, locked, #{code := Code} = Data) -> + do_lock(), + {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, []); + [Digit|Rest] -> % Incomplete + {keep_state,Data#{remaining := Rest}}; + [_|_] -> % Wrong + {keep_state,Data#{remaining := Code}} + end; +%% +%% State: open +handle_event(internal, enter, open, Data) -> + Tref = erlang:start_timer(10000, self(), lock), + do_unlock(), + {keep_state,Data#{timer => Tref}}; +handle_event(info, {timeout,Tref,lock}, open, #{timer := Tref} = Data) -> + enter(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)}]}. + +... + ]]> +
+

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

+
+ +
diff --git a/system/doc/design_principles/xmlfiles.mk b/system/doc/design_principles/xmlfiles.mk index 3032ac3ab9..e476255d62 100644 --- a/system/doc/design_principles/xmlfiles.mk +++ b/system/doc/design_principles/xmlfiles.mk @@ -1,7 +1,7 @@ # # %CopyrightBegin% # -# Copyright Ericsson AB 2009. All Rights Reserved. +# Copyright Ericsson AB 2009-2016. 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. @@ -25,6 +25,7 @@ DESIGN_PRINCIPLES_CHAPTER_FILES = \ distributed_applications.xml \ events.xml \ fsm.xml \ + statem.xml \ gen_server_concepts.xml \ included_applications.xml \ release_handling.xml \ -- cgit v1.2.3 From 20b44db2843e280474a45665f2584f9130eb686b Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 15 Apr 2016 10:40:00 +0200 Subject: Use .png pictures instead of .gif --- system/doc/design_principles/Makefile | 17 ++++++++++++----- system/doc/design_principles/code_lock.gif | Bin 35031 -> 0 bytes system/doc/design_principles/code_lock.png | Bin 0 -> 58906 bytes system/doc/design_principles/code_lock_2.gif | Bin 29420 -> 0 bytes system/doc/design_principles/code_lock_2.png | Bin 0 -> 51848 bytes system/doc/design_principles/statem.xml | 4 ++-- 6 files changed, 14 insertions(+), 7 deletions(-) delete mode 100644 system/doc/design_principles/code_lock.gif create mode 100644 system/doc/design_principles/code_lock.png delete mode 100644 system/doc/design_principles/code_lock_2.gif create mode 100644 system/doc/design_principles/code_lock_2.png (limited to 'system') diff --git a/system/doc/design_principles/Makefile b/system/doc/design_principles/Makefile index 2ccf477e83..937b3e28c8 100644 --- a/system/doc/design_principles/Makefile +++ b/system/doc/design_principles/Makefile @@ -48,8 +48,6 @@ BOOK_FILES = book.xml GIF_FILES = \ note.gif \ clientserver.gif \ - code_lock.gif \ - code_lock_2.gif \ dist1.gif \ dist2.gif \ dist3.gif \ @@ -60,6 +58,12 @@ GIF_FILES = \ sup5.gif \ sup6.gif +PNG_FILES = \ + code_lock.png \ + code_lock_2.png + +IMAGE_FILES = $(GIF_FILES) $(PNG_FILES) + XML_FILES = \ $(BOOK_FILES) $(XML_CHAPTER_FILES) \ $(XML_PART_FILES) @@ -87,13 +91,16 @@ _create_dirs := $(shell mkdir -p $(HTMLDIR)) $(HTMLDIR)/%.gif: %.gif $(INSTALL_DATA) $< $@ +$(HTMLDIR)/%.png: %.png + $(INSTALL_DATA) $< $@ + docs: html local_docs: PDFDIR=../../pdf -html: $(HTML_UG_FILE) gifs +html: $(HTML_UG_FILE) images -gifs: $(GIF_FILES:%=$(HTMLDIR)/%) +images: $(IMAGE_FILES:%=$(HTMLDIR)/%) debug opt: @@ -111,7 +118,7 @@ release_docs_spec: docs # $(INSTALL_DIR) "$(RELEASE_PATH)/pdf" # $(INSTALL_DATA) $(TOP_PDF_FILE) "$(RELEASE_PATH)/pdf" $(INSTALL_DIR) $(RELSYSDIR) - $(INSTALL_DATA) $(GIF_FILES) $(HTMLDIR)/*.html \ + $(INSTALL_DATA) $(IMAGE_FILES) $(HTMLDIR)/*.html \ $(RELSYSDIR) diff --git a/system/doc/design_principles/code_lock.gif b/system/doc/design_principles/code_lock.gif deleted file mode 100644 index adcfee7eff..0000000000 Binary files a/system/doc/design_principles/code_lock.gif and /dev/null differ diff --git a/system/doc/design_principles/code_lock.png b/system/doc/design_principles/code_lock.png new file mode 100644 index 0000000000..5e5744ecc2 Binary files /dev/null and b/system/doc/design_principles/code_lock.png differ diff --git a/system/doc/design_principles/code_lock_2.gif b/system/doc/design_principles/code_lock_2.gif deleted file mode 100644 index 150515c3b6..0000000000 Binary files a/system/doc/design_principles/code_lock_2.gif and /dev/null differ diff --git a/system/doc/design_principles/code_lock_2.png b/system/doc/design_principles/code_lock_2.png new file mode 100644 index 0000000000..138fbdef6c Binary files /dev/null and b/system/doc/design_principles/code_lock_2.png differ diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index c10afe482d..65daeac782 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -136,7 +136,7 @@ handle_event(EventType, EventContent, State, Data) -> it is is wrong, we start all over, waiting for a new button sequence.

- + Code lock state diagram

@@ -934,7 +934,7 @@ enter(Tag, State, Data) -> and some more utilizing the entry actions, which deserves a new state diagram:

- + Code lock state diagram revisited

-- cgit v1.2.3 From a2f3c685b5aeac798e12302cf8fe7df13184b669 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 15 Apr 2016 10:09:54 +0200 Subject: Introduce corrections from Fred Hebert and Ingela --- system/doc/design_principles/statem.xml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) (limited to 'system') diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 65daeac782..02754bd23d 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -90,7 +90,7 @@ State(S) x Event(E) -> Actions(A), State(S')

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

In the mode handle_event_function there is only one Erlang function that implements all state transition rules: @@ -612,7 +612,7 @@ stop() -> An example of event postponing comes in later in this chapter. See the - documentation + reference manual for details. You can for example actually reply to several callers and generate multiple next events to handle. @@ -735,7 +735,8 @@ open(cast, {button,_}, Data) -> erlang:cancel_timer(Tref). Note that a timeout message can not arrive after this, - unless you have postponed it before (why on earth one would do that). + unless you have postponed it (see the next section) before, + so make sure you do not accidentally postpone such messages.

Another way to cancel a timer is to not cancel it, @@ -837,15 +838,25 @@ do_unlock() -> The selective receive in this case causes open to implicitly postpone any events to the locked state.

+

+ A selective receive can not be used from a gen_statem + behaviour just as for any gen_* behavior + since the receive statement is within the gen_* engine itself. + It has to be there because all + sys + compatible behaviours must respond to system messages and therefore + do that in their engine receive loop, + passing non-system messages to the callback module. +

The state transition action postpone is designed to be able to model - selective receive. Selective receive implicitly postpones + selective receives. A selective receive implicitly postpones any not received events, but the postpone - state transition action explicitly postpones a received event. + state transition action explicitly postpones one received event.

Other than that both mechanisms have got the same theoretical -- cgit v1.2.3 From 2591124991bf601321d44de37d6c32e49075e68f Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Mon, 18 Apr 2016 15:39:06 +0200 Subject: Introduce Fred Herbert suggested additions --- system/doc/design_principles/statem.xml | 247 +++++++++++++++++++++++++++++++- 1 file changed, 243 insertions(+), 4 deletions(-) (limited to 'system') diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 02754bd23d..26e4840640 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -71,6 +71,7 @@ State(S) x Event(E) -> Actions(A), State(S') Since 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)

Like most gen_ behaviours, gen_statem keeps @@ -81,9 +82,19 @@ State(S) x Event(E) -> Actions(A), State(S') a state machine implemented with this behaviour Turing complete. But it feels mostly like an Event Driven Mealy Machine.

+ + + + +
+ + Callback Modes

The gen_statem behaviour supports two different - callback modes. In the mode state_functions, + + callback modes. + + In the mode state_functions, the state transition rules are written as a number of Erlang functions, which conform to the following convention:

@@ -109,6 +120,64 @@ handle_event(EventType, EventContent, State, Data) -> execute state transition actions on the machine engine itself and send replies.

+ +
+ Choosing Callback Mode +

+ The two + + callback modes + + gives different possibilities + and restrictions, but one goal remains: + you want to handle all possible combinations of + events and states. +

+

+ You can for example do this by focusing on one state at the time + and for every state ensure that all events are handled, + or the other way around focus on one event at the time + and ensure that it is handled in every state, + or mix these strategies. +

+

+ With state_functions you are restricted to use + atom only states, and the gen_statem engine dispatches + on state name for you. This encourages the callback module + to gather the implementation of all event actions particular + to one state in the same place in the code + hence focus on one state at the time. +

+

+ This mode fits well when you have a regular state diagram + like the ones in this chapter that describes all events and actions + belonging to a state visually around that state, + and each state has its unique name. +

+

+ With handle_event_function you are free to mix strategies + as you like because all events and states + are handled in the the same callback function. +

+

+ This mode works equally well when you want to focus on + one event at the time or when you want to focus on + one state at the time, but the handle_event/4 function + quickly grows too large to handle without introducing dispatching. +

+

+ The mode enables the use of non-atom states for example + complex states or even hiearchical states. + If, for example, a state diagram is largely alike + for the client and for the server side of a protocol; + then you can have a state {StateName,server} or + {StateName,client} and since you do the dispatching + yourself you make StateName decide where in the code + to handle most events in the state. + The second element of the tuple is then used to select + whether to handle special client side or server side events. +

+
@@ -773,6 +842,20 @@ open(cast, {button,_}, Data) -> {keep_state,Data,[postpone]}; ... ]]> +

+ The fact that a postponed event is only retried after a state change + translates into a requirement on the event and state space: + if you have a choice between storing a state data item + in the State or in the Data; + should a change in the item value affect which events that + are handled, then this item ought to be part of the state. +

+

+ What you want to avoid is that you maybe much later decide + to postpone an event in one state and by misfortune it is never retried + because the code only changes the Data but not the State. +

+
Fuzzy State Diagrams

@@ -788,6 +871,7 @@ open(cast, {button,_}, Data) -> as in postpone it.

+
Selective Receive

@@ -956,6 +1040,7 @@ enter(Tag, State, Data) -> Nor does it show that the code_length/0 call shall be handled in every state.

+
Callback Mode: state_functions

@@ -1029,6 +1114,7 @@ code_change(_Vsn, State, Data, _Extra) -> {ok,State,Data}. ]]>

+
Callback Mode: handle_event_function

@@ -1059,7 +1145,7 @@ handle_event( #{code := Code, remaining := Remaining} = Data) -> case Remaining of [Digit] -> % Complete - enter(next_state, open, Data, []); + enter(next_state, open, Data); [Digit|Rest] -> % Incomplete {keep_state,Data#{remaining := Rest}}; [_|_] -> % Wrong @@ -1072,7 +1158,7 @@ handle_event(internal, enter, open, Data) -> do_unlock(), {keep_state,Data#{timer => Tref}}; handle_event(info, {timeout,Tref,lock}, open, #{timer := Tref} = Data) -> - enter(next_state, locked, Data, []); + enter(next_state, locked, Data); handle_event(cast, {button,_}, open, _) -> {keep_state_and_data,[postpone]}; %% @@ -1086,8 +1172,161 @@ handle_event({call,From}, code_length, _State, #{code := Code}) ->

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

+ + +
+ Complex State +

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

+

+ One reason to use this is when you have + a state item that affects the event handling + in particular when combining that with postponing events. + Let us complicate the previous example + by introducing a configurable lock button + (this is the state item in question) + that in the open state immediately locks the door, + and an API function set_lock_button/1 to set the lock button. +

+

+ Suppose now that we call set_lock_button + while the door is open, + and have already postponed a button event + that up until now was not the lock button; + the sensible thing might be to say that + the button was pressed too early so it should + not be recognized as the lock button, + but then it might 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 let us 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 will not return until the state transits to locked + since it is there the event is handled and the reply is sent. +

+

+ If now one process calls set_lock_button/1 + to change the lock button while some other process + hangs in button/1 with the new lock button + it could 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 when we change the lock button the state will change + and all postponed events will be retried. +

+

+ We define the state as {StateName,LockButton} + where StateName is as before + and LockButton is the current lock button: +

+ + gen_statem:start_link( + {local,?NAME}, ?MODULE, {Code,LockButton}, []). +stop() -> + gen_statem:stop(?NAME). + +button(Digit) -> + gen_statem:call(?NAME, {button,Digit}). +code_length() -> + gen_statem:call(?NAME, code_length). +set_lock_button(LockButton) -> + gen_statem:call(?NAME, {set_lock_button,LockButton}). + +init({Code,LockButton}) -> + process_flag(trap_exit, true), + Data = #{code => Code}, + enter(handle_event_function, {locked,LockButton}, Data, []). + +%% State: locked +handle_event(internal, enter, {locked,_}, #{code := Code} = Data) -> + do_lock(), + {keep_state,Data#{remaining => Code}}; +handle_event( + {call,From}, {button,Digit}, {locked,LockButton}, + #{code := Code, remaining := Remaining} = Data) -> + case Remaining of + [Digit] -> % Complete + enter(next_state, {open,LockButton}, Data, [{reply,From,ok}]); + [Digit|Rest] -> % Incomplete + {keep_state,Data#{remaining := Rest},[{reply,From,ok}]}; + [_|_] -> % Wrong + {keep_state,Data#{remaining := Code},[{reply,From,ok}]} + end; +%% +%% State: open +handle_event(internal, enter, {open,_}, Data) -> + Tref = erlang:start_timer(10000, self(), lock), + do_unlock(), + {keep_state,Data#{timer => Tref}}; +handle_event( + info, {timeout,Tref,lock}, {open,LockButton}, + #{timer := Tref} = Data) -> + enter(next_state, {locked,LockButton}, Data, []); +handle_event( + {call,From}, {button,LockButton}, {open,LockButton}, + #{timer := Tref} = Data) -> + erlang:cancel_timer(Tref), + enter(next_state, {locked,LockButton}, Data, [{reply,From,locked}]); +handle_event({call,_}, {button,_}, {open,_}, _) -> + {keep_state_and_data,[postpone]}; +%% +%% Any state +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, _State, #{code := Code}) -> + {keep_state_and_data,[{reply,From,length(Code)}]}. + +enter(Tag, State, Data, Actions) -> + {Tag,State,Data,[{next_event,internal,enter}|Actions]}. + +do_lock() -> + io:format("Locked~n", []). +do_unlock() -> + io:format("Open~n", []). + +terminate(_Reason, State, _Data) -> + State =/= locked andalso do_lock(), + ok. +code_change(_Vsn, State, Data, _Extra) -> + {ok,State,Data}. + ]]> +

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

+
+ -- cgit v1.2.3 From 2977fbc6b658b0d664f7d3b36ecf8ca9e897aaa3 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 19 Apr 2016 09:28:38 +0200 Subject: Use ?NAME macro in examples --- system/doc/design_principles/statem.xml | 36 ++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 16 deletions(-) (limited to 'system') diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 26e4840640..72aaafd780 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -216,6 +216,7 @@ handle_event(EventType, EventContent, State, Data) -> -export([locked/3,open/3]). start_link(Code) -> - gen_statem:start_link({local,code_lock}, ?MODULE, Code, []). + gen_statem:start_link({local,?NAME}, ?MODULE, Code, []). button(Digit) -> - gen_statem:cast(code_lock, {button,Digit}). + gen_statem:cast(?NAME, {button,Digit}). init(Code) -> @@ -278,7 +279,7 @@ code_change(_Vsn, State, Data, _Extra) ->

- gen_statem:start_link({local,code_lock}, ?MODULE, Code, []). + gen_statem:start_link({local,?NAME}, ?MODULE, Code, []). ]]>

start_link calls the function @@ -290,9 +291,9 @@ start_link(Code) ->

- The first argument, {local,code_lock}, specifies + The first argument, {local,?NAME}, specifies the name. In this case, the gen_statem is locally - registered as code_lock. + registered as code_lock through the macro ?NAME.

If the name is omitted, the gen_statem is not registered. @@ -394,11 +395,12 @@ init(Code) ->

- gen_statem:cast(code_lock, {button,Digit}). + gen_statem:cast(?NAME, {button,Digit}). ]]>

- code_lock is the name of the gen_statem and must - agree with the name used to start it. + The first argument is the name of the gen_statem and must + agree with the name used to start it so therefore we use the + same macro ?NAME as when starting. {button,Digit} is the actual event content.

@@ -495,7 +497,7 @@ open(timeout, _, Data) -> ... code_length() -> - gen_statem:call(code_lock, code_length). + gen_statem:call(?NAME, code_length). ... locked(...) -> ... ; @@ -627,7 +629,7 @@ terminate(_Reason, State, _Data) -> ... stop() -> - gen_statem:stop(code_lock). + gen_statem:stop(?NAME). ]]>

This makes the gen_statem call the terminate/3 @@ -882,18 +884,19 @@ open(cast, {button,_}, Data) ->

spawn( fun () -> - true = register(code_lock, self()), + true = register(?NAME, self()), do_lock(), locked(Code, Code) end). button(Digit) -> - code_lock ! {button,Digit}. + ?NAME ! {button,Digit}. locked(Code, [Digit|Remaining]) -> receive @@ -1049,6 +1052,7 @@ enter(Tag, State, Data) -> -export([locked/3,open/3]). start_link(Code) -> - gen_statem:start_link({local,code_lock}, ?MODULE, Code, []). + gen_statem:start_link({local,?NAME}, ?MODULE, Code, []). stop() -> - gen_statem:stop(code_lock). + gen_statem:stop(?NAME). button(Digit) -> - gen_statem:cast(code_lock, {button,Digit}). + gen_statem:cast(?NAME, {button,Digit}). code_length() -> - gen_statem:call(code_lock, code_length). + gen_statem:call(?NAME, code_length). init(Code) -> Data = #{code => Code}, -- cgit v1.2.3 From 26b3c7d60d52d8a7be006b06d856bb0f7276e77a Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 21 Apr 2016 15:58:52 +0200 Subject: Modify code_change/4 to return CallbackMode Also move check of non-atom states in callback mode state_functions to where the state function is called. This gives homogenous diagnostics for state functions, code_change/4 and system_replace_state StateFun. Irregularities pointed out by James Fish. --- system/doc/design_principles/statem.xml | 87 +++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 38 deletions(-) (limited to 'system') diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 72aaafd780..27b9b7c761 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -90,11 +90,11 @@ State(S) x Event(E) -> Actions(A), State(S') Callback Modes

- The gen_statem behaviour supports two different - - callback modes. + The gen_statem behaviour supports two different callback modes. + In the mode + + state_functions, - In the mode state_functions, the state transition rules are written as a number of Erlang functions, which conform to the following convention:

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

- In the mode handle_event_function there is only one + In the mode + + handle_event_function + + there is only one Erlang function that implements all state transition rules:

@@ -125,9 +129,7 @@ handle_event(EventType, EventContent, State, Data) ->
       Choosing Callback Mode
       

The two - - callback modes - + callback modes gives different possibilities and restrictions, but one goal remains: you want to handle all possible combinations of @@ -186,7 +188,7 @@ handle_event(EventType, EventContent, State, Data) -> Example

This is an example starting off as equivalent to the the example in the - gen_fsm behaviour + gen_fsm behaviour description. In later chapters additions and tweaks are made using features in gen_statem that gen_fsm does not have. At the end of this section you can find the example again @@ -217,6 +219,7 @@ handle_event(EventType, EventContent, State, Data) -> -module(code_lock). -behaviour(gen_statem). -define(NAME, code_lock). +-define(CALLBACK_MODE, state_functions). -export([start_link/1]). -export([button/1]). @@ -233,7 +236,7 @@ button(Digit) -> init(Code) -> do_lock(), Data = #{code => Code, remaining => Code}, - {state_functions,locked,Data}. + {?CALLBACK_MODE,locked,Data}. locked( cast, {button,Digit}, @@ -264,7 +267,7 @@ terminate(_Reason, State, _Data) -> State =/= locked andalso do_lock(), ok. code_change(_Vsn, State, Data, _Extra) -> - {ok,State,Data}. + {?CALLBACK_MODE,State,Data}. ]]>

The code is explained in the next sections.

@@ -340,16 +343,20 @@ start_link(Code) -> If name registration succeeds, the new gen_statem process calls the callback function code_lock:init(Code). This function is expected to return {CallbackMode,State,Data}, - where CallbackMode selects callback module state function - mode, in this case state_functions that is each state + where + + CallbackMode + + selects callback module state function mode, in this case + + state_functions + + through the macro ?CALLBACK_MODE that is; each state has got its own handler function. State is the initial state of the gen_statem, in this case locked; assuming the door is locked to begin with. Data is the internal server data of the gen_statem. - Here the server data is a - - map - + Here the server data is a map with the key code that stores the correct button sequence and the key remaining that stores the remaining correct button sequence @@ -359,7 +366,7 @@ start_link(Code) -> init(Code) -> do_lock(), Data = #{code => Code, remaining => Code}, - {state_functions,locked,Data}. + {?CALLBACK_MODE,locked,Data}. ]]>

@@ -536,13 +543,12 @@ handle_event({call,From}, code_length, #{code := Code} = Data) ->

- Data = #{code => Code, remaining => Code}, - {handle_event_function,locked,Data}. +... handle_event(cast, {button,Digit}, State, #{code := Code} = Data) -> case State of @@ -598,8 +604,8 @@ handle_event(timeout, _, open, Data) -> process_flag(trap_exit, true), + do_lock(), ... - {CallbackMode,State,Data}. ]]>

In this example we let the terminate/3 function @@ -994,11 +1000,18 @@ do_unlock() -> utilizing a helper function enter/3 for state entry:

+ process_flag(trap_exit, true), Data = #{code => Code}, - enter(state_functions, locked, Data). + enter(?CALLBACK_MODE, locked, Data). ... + locked(internal, enter, _Data) -> do_lock(), {keep_state,Data#{remaining => Code}}; @@ -1053,6 +1066,7 @@ enter(Tag, State, Data) -> -module(code_lock). -behaviour(gen_statem). -define(NAME, code_lock_2). +-define(CALLBACK_MODE, state_functions). -export([start_link/1,stop/0]). -export([button/1,code_length/0]). @@ -1070,8 +1084,9 @@ code_length() -> gen_statem:call(?NAME, code_length). init(Code) -> + process_flag(trap_exit, true), Data = #{code => Code}, - enter(state_functions, locked, Data). + enter(?CALLBACK_MODE, locked, Data). locked(internal, enter, #{code := Code} = Data) -> do_lock(), @@ -1115,7 +1130,7 @@ terminate(_Reason, State, _Data) -> State =/= locked andalso do_lock(), ok. code_change(_Vsn, State, Data, _Extra) -> - {ok,State,Data}. + {?CALLBACK_MODE,State,Data}. ]]> @@ -1129,14 +1144,10 @@ code_change(_Vsn, State, Data, _Extra) ->

- process_flag(trap_exit, true), - Data = #{code => Code}, - enter(handle_event_function, locked, Data). +-export([handle_event/4]). ... @@ -1185,11 +1196,10 @@ handle_event({call,From}, code_length, _State, #{code := Code}) ->
Complex State

- The - - callback mode + The callback mode + + handle_event_function - handle_event_function enables using a non-atom state as described in Callback Modes, @@ -1245,6 +1255,7 @@ handle_event({call,From}, code_length, _State, #{code := Code}) -> -module(code_lock). -behaviour(gen_statem). -define(NAME, code_lock_3). +-define(CALLBACK_MODE, handle_event_function). -export([start_link/2,stop/0]). -export([button/1,code_length/0,set_lock_button/1]). @@ -1267,7 +1278,7 @@ set_lock_button(LockButton) -> init({Code,LockButton}) -> process_flag(trap_exit, true), Data = #{code => Code}, - enter(handle_event_function, {locked,LockButton}, Data, []). + enter(?CALLBACK_MODE, {locked,LockButton}, Data, []). %% State: locked handle_event(internal, enter, {locked,_}, #{code := Code} = Data) -> @@ -1323,7 +1334,7 @@ terminate(_Reason, State, _Data) -> State =/= locked andalso do_lock(), ok. code_change(_Vsn, State, Data, _Extra) -> - {ok,State,Data}. + {?CALLBACK_MODE,State,Data}. ]]>

It may be an ill-fitting model for a physical code lock -- cgit v1.2.3 From b54d82fea10c24359d2a315668b6176fc47963b7 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 22 Apr 2016 15:18:07 +0200 Subject: Promote gen_statem over gen_fsm --- system/doc/design_principles/des_princ.xml | 6 ++++-- system/doc/design_principles/fsm.xml | 12 +++++++++++- system/doc/design_principles/release_handling.xml | 4 ++-- system/doc/design_principles/statem.xml | 9 +++++++++ system/doc/design_principles/sup_princ.xml | 2 +- 5 files changed, 27 insertions(+), 6 deletions(-) (limited to 'system') diff --git a/system/doc/design_principles/des_princ.xml b/system/doc/design_principles/des_princ.xml index 0e087cf843..8ab8661c2d 100644 --- a/system/doc/design_principles/des_princ.xml +++ b/system/doc/design_principles/des_princ.xml @@ -4,7 +4,7 @@

- 19972013 + 19972016 Ericsson AB. All Rights Reserved. @@ -226,7 +226,9 @@ free(Ch, {Alloc, Free} = Channels) ->

gen_server

For implementing the server of a client-server relation

gen_fsm

-

For implementing finite-state machines

+

For implementing finite-state machines (Old)

+

gen_statem

+

For implementing state machines (New)

gen_event

For implementing event handling functionality

supervisor

diff --git a/system/doc/design_principles/fsm.xml b/system/doc/design_principles/fsm.xml index f58b50cbff..3468f93ae0 100644 --- a/system/doc/design_principles/fsm.xml +++ b/system/doc/design_principles/fsm.xml @@ -4,7 +4,7 @@
- 19972013 + 19972016 Ericsson AB. All Rights Reserved. @@ -30,6 +30,16 @@ fsm.xml
+ +

+ There is a new behaviour + gen_statem + that is intended to replace gen_fsm for new code. + It has the same features and add some really useful. + This module will not be removed for the foreseeable future + to keep old state machine implementations running. +

+

This section is to be read with the gen_fsm(3) manual page in STDLIB, where all interface functions and callback functions are described in detail.

diff --git a/system/doc/design_principles/release_handling.xml b/system/doc/design_principles/release_handling.xml index 20ddc3dbf5..4f71ad4437 100644 --- a/system/doc/design_principles/release_handling.xml +++ b/system/doc/design_principles/release_handling.xml @@ -4,7 +4,7 @@
- 20032014 + 20032016 Ericsson AB. All Rights Reserved. @@ -249,7 +249,7 @@

If Modules=dynamic, which is the case for event managers, the event manager process informs the release handler about the list of currently installed event handlers - (gen_fsm), and it is checked if the module name is in + (gen_event), and it is checked if the module name is in this list instead.

The release handler suspends, asks for code change, and resumes processes by calling the functions diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index 27b9b7c761..a921a40243 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -36,6 +36,15 @@ manual page in STDLIB, where all interface functions and callback functions are described in detail.

+

+ This is a new behaviour in 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. + But depending on user feedback, we do not expect + but might find it necessary to make minor + not backwards compatible changes into OTP-20.0, + so its state can be designated as "not quite experimental"... +

diff --git a/system/doc/design_principles/sup_princ.xml b/system/doc/design_principles/sup_princ.xml index a6b7dbb68d..749c9f69f2 100644 --- a/system/doc/design_principles/sup_princ.xml +++ b/system/doc/design_principles/sup_princ.xml @@ -402,8 +402,8 @@ supervisor:delete_child(Sup, Id)
restarts.

-
+ Simplified one_for_one Supervisors

A supervisor with restart strategy simple_one_for_one is a simplified one_for_one supervisor, where all child -- cgit v1.2.3 From f06f69068807168cf4cc731711ed82489cc5b99c Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Mon, 25 Apr 2016 12:07:32 +0200 Subject: Add section on state filtering Misc documentation fixes. --- system/doc/design_principles/code_lock.dia | Bin 2968 -> 2955 bytes system/doc/design_principles/code_lock.png | Bin 58906 -> 58823 bytes system/doc/design_principles/statem.xml | 209 +++++++++++++++++++++-------- 3 files changed, 152 insertions(+), 57 deletions(-) (limited to 'system') diff --git a/system/doc/design_principles/code_lock.dia b/system/doc/design_principles/code_lock.dia index d8262138a3..bed6d8ee86 100644 Binary files a/system/doc/design_principles/code_lock.dia and b/system/doc/design_principles/code_lock.dia differ diff --git a/system/doc/design_principles/code_lock.png b/system/doc/design_principles/code_lock.png index 5e5744ecc2..e40f0320aa 100644 Binary files a/system/doc/design_principles/code_lock.png and b/system/doc/design_principles/code_lock.png differ diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml index a921a40243..a4b8fb06a0 100644 --- a/system/doc/design_principles/statem.xml +++ b/system/doc/design_principles/statem.xml @@ -135,7 +135,7 @@ handle_event(EventType, EventContent, State, Data) ->

- Choosing Callback Mode + Choosing the Callback Mode

The two callback modes @@ -157,7 +157,7 @@ handle_event(EventType, EventContent, State, Data) -> on state name for you. This encourages the callback module to gather the implementation of all event actions particular to one state in the same place in the code - hence focus on one state at the time. + hence to focus on one state at the time.

This mode fits well when you have a regular state diagram @@ -216,7 +216,7 @@ handle_event(EventType, EventContent, State, Data) -> it is is wrong, we start all over, waiting for a new button sequence.

- + Code lock state diagram

@@ -842,9 +842,9 @@ open(cast, {button,_}, Data) -> changed i.e OldState =/= NewState.

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

@@ -951,9 +951,9 @@ do_unlock() -> passing non-system messages to the callback module.

- The + The state transition - state transition action + action postpone is designed to be able to model selective receives. A selective receive implicitly postpones @@ -974,9 +974,10 @@ do_unlock() -> Self Generated Events

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

@@ -1054,7 +1055,7 @@ enter(Tag, State, Data) -> and some more utilizing the entry actions, which deserves a new state diagram:

- + Code lock state diagram revisited

@@ -1200,6 +1201,71 @@ handle_event({call,From}, code_length, _State, #{code := Code}) ->

+ + +
+ Filter the State +

+ The example servers so far in this chapter will for example + when killed by an exit signal or due to an internal error + print out the full internal state in the error log. + This state contains both the code lock code + and which digits that remains to unlock. +

+

+ This state data can be regarded as sensitive, + and maybe not what you want in the error log + because of something unpredictable happening. +

+

+ Another reason to filter the state can be + that the state is too big to print out since it fills + the error log with uninteresting details. +

+

+ To avoid this you can format the internal state + that gets in the error log and gets returned from + + sys:get_status/1,2 + + by implementing the + + Module:format_status/2 + + function, for example like this: +

+ + StateData = + {State, + maps:filter( + fun (code, _) -> false; + (remaining, _) -> false; + (_, _) -> true + end, + Data)}, + case Opt of + terminate -> + StateData; + normal -> + [{data,[{"State",StateData}]}] + end. + ]]> +

+ It is not mandatory to implement a + + Module:format_status/2 + + function. If you do not a default implementation is used that + does the same as this example function without filtering + the Data term that is: StateData = {State,Data}. +

+
+
@@ -1260,7 +1326,7 @@ handle_event({call,From}, code_length, _State, #{code := Code}) -> where StateName is as before and LockButton is the current lock button:

- -export([start_link/2,stop/0]). -export([button/1,code_length/0,set_lock_button/1]). --export([init/1,terminate/3,code_change/4]). +-export([init/1,terminate/3,code_change/4,format_status/2]). -export([handle_event/4]). start_link(Code, LockButton) -> @@ -1286,51 +1352,65 @@ set_lock_button(LockButton) -> init({Code,LockButton}) -> process_flag(trap_exit, true), - Data = #{code => Code}, + Data = #{code => Code, remaining => undefined, timer => undefined}, enter(?CALLBACK_MODE, {locked,LockButton}, Data, []). -%% State: locked -handle_event(internal, enter, {locked,_}, #{code := Code} = Data) -> - do_lock(), - {keep_state,Data#{remaining => Code}}; handle_event( - {call,From}, {button,Digit}, {locked,LockButton}, - #{code := Code, remaining := Remaining} = Data) -> - case Remaining of - [Digit] -> % Complete - enter(next_state, {open,LockButton}, Data, [{reply,From,ok}]); - [Digit|Rest] -> % Incomplete - {keep_state,Data#{remaining := Rest},[{reply,From,ok}]}; - [_|_] -> % Wrong - {keep_state,Data#{remaining := Code},[{reply,From,ok}]} - end; -%% -%% State: open -handle_event(internal, enter, {open,_}, Data) -> - Tref = erlang:start_timer(10000, self(), lock), - do_unlock(), - {keep_state,Data#{timer => Tref}}; + {call,From}, {set_lock_button,NewLockButton}, + {StateName,OldLockButton}, Data) -> + {next_state,{StateName,NewLockButton},Data, + [{reply,From,OldLockButton}]}; handle_event( - info, {timeout,Tref,lock}, {open,LockButton}, - #{timer := Tref} = Data) -> - enter(next_state, {locked,LockButton}, Data, []); + {call,From}, code_length, + {_StateName,_LockButton}, #{code := Code}) -> + {keep_state_and_data, + [{reply,From,length(Code)}]}; handle_event( - {call,From}, {button,LockButton}, {open,LockButton}, - #{timer := Tref} = Data) -> - erlang:cancel_timer(Tref), - enter(next_state, {locked,LockButton}, Data, [{reply,From,locked}]); -handle_event({call,_}, {button,_}, {open,_}, _) -> - {keep_state_and_data,[postpone]}; -%% -%% Any state + EventType, EventContent, + {locked,LockButton}, #{code := Code, remaining := Remaining} = Data) -> + case {EventType,EventContent} of + {internal,enter} -> + do_lock(), + {keep_state,Data#{remaining := Code}}; + {{call,From},{button,Digit}} -> + case Remaining of + [Digit] -> % Complete + next_state( + {open,LockButton}, Data, + [{reply,From,ok}]); + [Digit|Rest] -> % Incomplete + {keep_state,Data#{remaining := Rest}, + [{reply,From,ok}]}; + [_|_] -> % Wrong + {keep_state,Data#{remaining := Code}, + [{reply,From,ok}]} + end + end; 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, _State, #{code := Code}) -> - {keep_state_and_data,[{reply,From,length(Code)}]}. + EventType, EventContent, + {open,LockButton}, #{timer := Timer} = Data) -> + case {EventType,EventContent} of + {internal,enter} -> + Tref = erlang:start_timer(10000, self(), lock), + do_unlock(), + {keep_state,Data#{timer := Tref}}; + {info,{timeout,Timer,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}]); + 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]}. @@ -1344,13 +1424,28 @@ terminate(_Reason, State, _Data) -> ok. code_change(_Vsn, State, Data, _Extra) -> {?CALLBACK_MODE,State,Data}. - ]]> -

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

+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 may be an ill-fitting model for a physical code lock + that the button/1 call might hang until the lock + is locked. But for an API in general it is really not + that strange. +

-- cgit v1.2.3