From adcc726c36555434204dd0fccd13ed984741a7fb Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Mon, 26 Oct 2015 11:50:43 +0100 Subject: Factor out common code to gen.erl --- lib/stdlib/src/gen.erl | 121 ++++++++++++++++++++++++++++++++++-------- lib/stdlib/src/gen_event.erl | 11 ++-- lib/stdlib/src/gen_fsm.erl | 85 ++++------------------------- lib/stdlib/src/gen_server.erl | 112 ++++---------------------------------- 4 files changed, 119 insertions(+), 210 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/src/gen.erl b/lib/stdlib/src/gen.erl index a05c2ce6fd..7dd02b84e7 100644 --- a/lib/stdlib/src/gen.erl +++ b/lib/stdlib/src/gen.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1996-2014. All Rights Reserved. +%% Copyright Ericsson AB 1996-2015. 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. @@ -26,7 +26,8 @@ %%% %%% The standard behaviour should export init_it/6. %%%----------------------------------------------------------------- --export([start/5, start/6, debug_options/1, +-export([start/5, start/6, debug_options/2, + name/1, unregister_name/1, get_proc_name/1, get_parent/0, call/3, call/4, reply/2, stop/1, stop/3]). -export([init_it/6, init_it/7]). @@ -124,7 +125,7 @@ init_it(GenMod, Starter, Parent, Mod, Args, Options) -> init_it2(GenMod, Starter, Parent, self(), Mod, Args, Options). init_it(GenMod, Starter, Parent, Name, Mod, Args, Options) -> - case name_register(Name) of + case register_name(Name) of true -> init_it2(GenMod, Starter, Parent, Name, Mod, Args, Options); {false, Pid} -> @@ -297,19 +298,19 @@ where({global, Name}) -> global:whereis_name(Name); where({via, Module, Name}) -> Module:whereis_name(Name); where({local, Name}) -> whereis(Name). -name_register({local, Name} = LN) -> +register_name({local, Name} = LN) -> try register(Name, self()) of true -> true catch error:_ -> {false, where(LN)} end; -name_register({global, Name} = GN) -> +register_name({global, Name} = GN) -> case global:register_name(Name, self()) of yes -> true; no -> {false, where(GN)} end; -name_register({via, Module, Name} = GN) -> +register_name({via, Module, Name} = GN) -> case Module:register_name(Name, self()) of yes -> true; @@ -317,34 +318,108 @@ name_register({via, Module, Name} = GN) -> {false, where(GN)} end. +name({local,Name}) -> Name; +name({global,Name}) -> Name; +name({via,_, Name}) -> Name; +name(Pid) when is_pid(Pid) -> Pid. + +unregister_name({local,Name}) -> + try unregister(Name) of + _ -> ok + catch + _:_ -> ok + end; +unregister_name({global,Name}) -> + _ = global:unregister_name(Name), + ok; +unregister_name({via, Mod, Name}) -> + _ = Mod:unregister_name(Name), + ok; +unregister_name(Pid) when is_pid(Pid) -> + ok. + +get_proc_name(Pid) when is_pid(Pid) -> + Pid; +get_proc_name({local, Name}) -> + case process_info(self(), registered_name) of + {registered_name, Name} -> + Name; + {registered_name, _Name} -> + exit(process_not_registered); + [] -> + exit(process_not_registered) + end; +get_proc_name({global, Name}) -> + case global:whereis_name(Name) of + undefined -> + exit(process_not_registered_globally); + Pid when Pid =:= self() -> + Name; + _Pid -> + exit(process_not_registered_globally) + end; +get_proc_name({via, Mod, Name}) -> + case Mod:whereis_name(Name) of + undefined -> + exit({process_not_registered_via, Mod}); + Pid when Pid =:= self() -> + Name; + _Pid -> + exit({process_not_registered_via, Mod}) + end. + +get_parent() -> + case get('$ancestors') of + [Parent | _] when is_pid(Parent) -> + Parent; + [Parent | _] when is_atom(Parent) -> + name_to_pid(Parent); + _ -> + exit(process_was_not_started_by_proc_lib) + end. + +name_to_pid(Name) -> + case whereis(Name) of + undefined -> + case global:whereis_name(Name) of + undefined -> + exit(could_not_find_registered_name); + Pid -> + Pid + end; + Pid -> + Pid + end. + timeout(Options) -> - case opt(timeout, Options) of - {ok, Time} -> + case lists:keyfind(timeout, 1, Options) of + {_,Time} -> Time; - _ -> + false -> infinity end. spawn_opts(Options) -> - case opt(spawn_opt, Options) of - {ok, Opts} -> + case lists:keyfind(spawn_opt, 1, Options) of + {_,Opts} -> Opts; - _ -> + false -> [] end. -opt(Op, [{Op, Value}|_]) -> - {ok, Value}; -opt(Op, [_|Options]) -> - opt(Op, Options); -opt(_, []) -> - false. - -debug_options(Opts) -> - case opt(debug, Opts) of - {ok, Options} -> sys:debug_options(Options); - _ -> [] +debug_options(Name, Opts) -> + case lists:keyfind(debug, 1, Opts) of + {_,Options} -> + try sys:debug_options(Options) + catch _:_ -> + error_logger:format( + "~p: ignoring erroneous debug options - ~p~n", + [Name,Options]), + [] + end; + false -> + [] end. format_status_header(TagLine, Pid) when is_pid(Pid) -> diff --git a/lib/stdlib/src/gen_event.erl b/lib/stdlib/src/gen_event.erl index 3d63c19de7..aedc0e7c59 100644 --- a/lib/stdlib/src/gen_event.erl +++ b/lib/stdlib/src/gen_event.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1996-2014. All Rights Reserved. +%% Copyright Ericsson AB 1996-2015. 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. @@ -147,16 +147,11 @@ init_it(Starter, self, Name, Mod, Args, Options) -> init_it(Starter, self(), Name, Mod, Args, Options); init_it(Starter, Parent, Name0, _, _, Options) -> process_flag(trap_exit, true), - Debug = gen:debug_options(Options), + Name = gen:name(Name0), + Debug = gen:debug_options(Name, Options), proc_lib:init_ack(Starter, {ok, self()}), - Name = name(Name0), loop(Parent, Name, [], Debug, false). -name({local,Name}) -> Name; -name({global,Name}) -> Name; -name({via,_, Name}) -> Name; -name(Pid) when is_pid(Pid) -> Pid. - -spec add_handler(emgr_ref(), handler(), term()) -> term(). add_handler(M, Handler, Args) -> rpc(M, {add_handler, Handler, Args}). diff --git a/lib/stdlib/src/gen_fsm.erl b/lib/stdlib/src/gen_fsm.erl index 7eabb95548..77d41c0652 100644 --- a/lib/stdlib/src/gen_fsm.erl +++ b/lib/stdlib/src/gen_fsm.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1996-2014. All Rights Reserved. +%% Copyright Ericsson AB 1996-2015. 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. @@ -305,64 +305,11 @@ enter_loop(Mod, Options, StateName, StateData, Timeout) -> enter_loop(Mod, Options, StateName, StateData, self(), Timeout). enter_loop(Mod, Options, StateName, StateData, ServerName, Timeout) -> - Name = get_proc_name(ServerName), - Parent = get_parent(), - Debug = gen:debug_options(Options), + Name = gen:get_proc_name(ServerName), + Parent = gen:get_parent(), + Debug = gen:debug_options(Name, Options), loop(Parent, Name, StateName, StateData, Mod, Timeout, Debug). -get_proc_name(Pid) when is_pid(Pid) -> - Pid; -get_proc_name({local, Name}) -> - case process_info(self(), registered_name) of - {registered_name, Name} -> - Name; - {registered_name, _Name} -> - exit(process_not_registered); - [] -> - exit(process_not_registered) - end; -get_proc_name({global, Name}) -> - case global:whereis_name(Name) of - undefined -> - exit(process_not_registered_globally); - Pid when Pid =:= self() -> - Name; - _Pid -> - exit(process_not_registered_globally) - end; -get_proc_name({via, Mod, Name}) -> - case Mod:whereis_name(Name) of - undefined -> - exit({process_not_registered_via, Mod}); - Pid when Pid =:= self() -> - Name; - _Pid -> - exit({process_not_registered_via, Mod}) - end. - -get_parent() -> - case get('$ancestors') of - [Parent | _] when is_pid(Parent) -> - Parent; - [Parent | _] when is_atom(Parent) -> - name_to_pid(Parent); - _ -> - exit(process_was_not_started_by_proc_lib) - end. - -name_to_pid(Name) -> - case whereis(Name) of - undefined -> - case global:whereis_name(Name) of - undefined -> - exit(could_not_find_registered_name); - Pid -> - Pid - end; - Pid -> - Pid - end. - %%% --------------------------------------------------- %%% Initiate the new process. %%% Register the name using the Rfunc function @@ -373,8 +320,8 @@ name_to_pid(Name) -> init_it(Starter, self, Name, Mod, Args, Options) -> init_it(Starter, self(), Name, Mod, Args, Options); init_it(Starter, Parent, Name0, Mod, Args, Options) -> - Name = name(Name0), - Debug = gen:debug_options(Options), + Name = gen:name(Name0), + Debug = gen:debug_options(Name, Options), case catch Mod:init(Args) of {ok, StateName, StateData} -> proc_lib:init_ack(Starter, {ok, self()}), @@ -383,15 +330,15 @@ init_it(Starter, Parent, Name0, Mod, Args, Options) -> proc_lib:init_ack(Starter, {ok, self()}), loop(Parent, Name, StateName, StateData, Mod, Timeout, Debug); {stop, Reason} -> - unregister_name(Name0), + gen:unregister_name(Name0), proc_lib:init_ack(Starter, {error, Reason}), exit(Reason); ignore -> - unregister_name(Name0), + gen:unregister_name(Name0), proc_lib:init_ack(Starter, ignore), exit(normal); {'EXIT', Reason} -> - unregister_name(Name0), + gen:unregister_name(Name0), proc_lib:init_ack(Starter, {error, Reason}), exit(Reason); Else -> @@ -400,20 +347,6 @@ init_it(Starter, Parent, Name0, Mod, Args, Options) -> exit(Error) end. -name({local,Name}) -> Name; -name({global,Name}) -> Name; -name({via,_, Name}) -> Name; -name(Pid) when is_pid(Pid) -> Pid. - -unregister_name({local,Name}) -> - _ = (catch unregister(Name)); -unregister_name({global,Name}) -> - _ = global:unregister_name(Name); -unregister_name({via, Mod, Name}) -> - _ = Mod:unregister_name(Name); -unregister_name(Pid) when is_pid(Pid) -> - Pid. - %%----------------------------------------------------------------- %% The MAIN loop %%----------------------------------------------------------------- diff --git a/lib/stdlib/src/gen_server.erl b/lib/stdlib/src/gen_server.erl index c58b1de609..6aaefbbbfe 100644 --- a/lib/stdlib/src/gen_server.erl +++ b/lib/stdlib/src/gen_server.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1996-2014. All Rights Reserved. +%% Copyright Ericsson AB 1996-2015. 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. @@ -304,9 +304,9 @@ enter_loop(Mod, Options, State, Timeout) -> enter_loop(Mod, Options, State, self(), Timeout). enter_loop(Mod, Options, State, ServerName, Timeout) -> - Name = get_proc_name(ServerName), - Parent = get_parent(), - Debug = debug_options(Name, Options), + Name = gen:get_proc_name(ServerName), + Parent = gen:get_parent(), + Debug = gen:debug_options(Name, Options), loop(Parent, Name, State, Mod, Timeout, Debug). %%%======================================================================== @@ -323,8 +323,8 @@ enter_loop(Mod, Options, State, ServerName, Timeout) -> init_it(Starter, self, Name, Mod, Args, Options) -> init_it(Starter, self(), Name, Mod, Args, Options); init_it(Starter, Parent, Name0, Mod, Args, Options) -> - Name = name(Name0), - Debug = debug_options(Name, Options), + Name = gen:name(Name0), + Debug = gen:debug_options(Name, Options), case catch Mod:init(Args) of {ok, State} -> proc_lib:init_ack(Starter, {ok, self()}), @@ -339,15 +339,15 @@ init_it(Starter, Parent, Name0, Mod, Args, Options) -> %% (Otherwise, the parent process could get %% an 'already_started' error if it immediately %% tried starting the process again.) - unregister_name(Name0), + gen:unregister_name(Name0), proc_lib:init_ack(Starter, {error, Reason}), exit(Reason); ignore -> - unregister_name(Name0), + gen:unregister_name(Name0), proc_lib:init_ack(Starter, ignore), exit(normal); {'EXIT', Reason} -> - unregister_name(Name0), + gen:unregister_name(Name0), proc_lib:init_ack(Starter, {error, Reason}), exit(Reason); Else -> @@ -356,20 +356,6 @@ init_it(Starter, Parent, Name0, Mod, Args, Options) -> exit(Error) end. -name({local,Name}) -> Name; -name({global,Name}) -> Name; -name({via,_, Name}) -> Name; -name(Pid) when is_pid(Pid) -> Pid. - -unregister_name({local,Name}) -> - _ = (catch unregister(Name)); -unregister_name({global,Name}) -> - _ = global:unregister_name(Name); -unregister_name({via, Mod, Name}) -> - _ = Mod:unregister_name(Name); -unregister_name(Pid) when is_pid(Pid) -> - Pid. - %%%======================================================================== %%% Internal functions %%%======================================================================== @@ -858,86 +844,6 @@ error_info(Reason, Name, Msg, State, Debug) -> sys:print_log(Debug), ok. -%%% --------------------------------------------------- -%%% Misc. functions. -%%% --------------------------------------------------- - -opt(Op, [{Op, Value}|_]) -> - {ok, Value}; -opt(Op, [_|Options]) -> - opt(Op, Options); -opt(_, []) -> - false. - -debug_options(Name, Opts) -> - case opt(debug, Opts) of - {ok, Options} -> dbg_opts(Name, Options); - _ -> [] - end. - -dbg_opts(Name, Opts) -> - case catch sys:debug_options(Opts) of - {'EXIT',_} -> - format("~p: ignoring erroneous debug options - ~p~n", - [Name, Opts]), - []; - Dbg -> - Dbg - end. - -get_proc_name(Pid) when is_pid(Pid) -> - Pid; -get_proc_name({local, Name}) -> - case process_info(self(), registered_name) of - {registered_name, Name} -> - Name; - {registered_name, _Name} -> - exit(process_not_registered); - [] -> - exit(process_not_registered) - end; -get_proc_name({global, Name}) -> - case global:whereis_name(Name) of - undefined -> - exit(process_not_registered_globally); - Pid when Pid =:= self() -> - Name; - _Pid -> - exit(process_not_registered_globally) - end; -get_proc_name({via, Mod, Name}) -> - case Mod:whereis_name(Name) of - undefined -> - exit({process_not_registered_via, Mod}); - Pid when Pid =:= self() -> - Name; - _Pid -> - exit({process_not_registered_via, Mod}) - end. - -get_parent() -> - case get('$ancestors') of - [Parent | _] when is_pid(Parent)-> - Parent; - [Parent | _] when is_atom(Parent)-> - name_to_pid(Parent); - _ -> - exit(process_was_not_started_by_proc_lib) - end. - -name_to_pid(Name) -> - case whereis(Name) of - undefined -> - case global:whereis_name(Name) of - undefined -> - exit(could_not_find_registered_name); - Pid -> - Pid - end; - Pid -> - Pid - end. - %%----------------------------------------------------------------- %% Status information %%----------------------------------------------------------------- -- cgit v1.2.3 From 6ace96d3e5c9ac8ace3d8967bcafb3e6a081d9be Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Mon, 26 Oct 2015 11:52:17 +0100 Subject: New state machine --- lib/stdlib/doc/src/Makefile | 3 +- lib/stdlib/doc/src/gen_statem.xml | 1131 +++++++++++++++++++++++++ lib/stdlib/doc/src/ref_man.xml | 3 +- lib/stdlib/doc/src/specs.xml | 1 + lib/stdlib/src/Makefile | 3 +- lib/stdlib/src/gen_statem.erl | 1095 ++++++++++++++++++++++++ lib/stdlib/src/proc_lib.erl | 16 +- lib/stdlib/src/stdlib.app.src | 3 +- lib/stdlib/test/Makefile | 1 + lib/stdlib/test/error_logger_forwarder.erl | 8 +- lib/stdlib/test/gen_statem_SUITE.erl | 1250 ++++++++++++++++++++++++++++ 11 files changed, 3501 insertions(+), 13 deletions(-) create mode 100644 lib/stdlib/doc/src/gen_statem.xml create mode 100644 lib/stdlib/src/gen_statem.erl create mode 100644 lib/stdlib/test/gen_statem_SUITE.erl (limited to 'lib') diff --git a/lib/stdlib/doc/src/Makefile b/lib/stdlib/doc/src/Makefile index 196c86748f..26602764a6 100644 --- a/lib/stdlib/doc/src/Makefile +++ b/lib/stdlib/doc/src/Makefile @@ -1,7 +1,7 @@ # # %CopyrightBegin% # -# Copyright Ericsson AB 1997-2015. 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. @@ -68,6 +68,7 @@ XML_REF3_FILES = \ gen_event.xml \ gen_fsm.xml \ gen_server.xml \ + gen_statem.xml \ io.xml \ io_lib.xml \ lib.xml \ diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml new file mode 100644 index 0000000000..885021f61c --- /dev/null +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -0,0 +1,1131 @@ + + + + +
+ + 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 + + + + +
+ gen_statem + Generic State Machine Behaviour + +

A behaviour module for implementing a state machine, primarily + a finite state machine, but an infinite state space is possible. + A generic state machine process (gen_statem) implemented using + this module will have a standard set of interface functions + and include functionality for tracing and error reporting. + It will also fit into an OTP supervision tree. Refer to + + OTP Design Principles for more information. +

+

A gen_statem assumes all specific parts to be located in a + callback module exporting a pre-defined set of functions. + The relationship between the behaviour functions and the callback + functions can be illustrated as follows:

+
+gen_statem module            Callback module
+-----------------            ---------------
+gen_statem:start
+gen_statem:start_link -----> Module:init/1
+
+gen_statem:stop       -----> Module:terminate/2
+
+gen_statem:call
+gen_statem:cast
+erlang:send
+erlang:'!'            -----> Module:State/5
+                             Module:handle_event/5
+
+-                     -----> Module:terminate/3
+
+-                     -----> Module:code_change/3
+

Events are of different + types + so the callback functions can know the origin of an event + and how to respond. +

+

If a callback function fails or returns a bad value, + the gen_statem will terminate. An exception of class + throw, + however, is not regarded as an error but as a valid return. +

+ +

The "state function" for a specific + state + in a gen_statem is the callback function that is called + for all events in this state. + An event can can be postponed causing it to be retried + after the state has changed, or more precisely; + after a state change all postponed events are retried + in the new state. +

+

The state machine + State + is normally an atom in which case the + state function + that will be called is + Module:State/5. + For a + State + that is not an atom the + state function + + Module:handle_event/5 + will be called. + If you use handle_event as a + state and later + decides to use non-atom states you will then have to + rewrite your code to stop using that state. +

+

When the using an atom-only + State + it becomes fairly obvious in the implementation code + which events are handled in which state + since there are different callback functions for different states. +

+

+ When using a non-atom State + all events are handled in the callback function + + Module:handle_event/5 + + so it may require more coding discipline to ensure what events + are handled in which state. Therefore it might be a wee bit + easier to accidentally postpone an event in two or more states + and not handling it in any of them causing a tight infinite + loop when the event bounces to be retried between the states. +

+

A gen_statem handles system messages as documented in + sys. + The sys module + can be used for debugging a gen_statem. +

+

Note that a gen_statem does not trap exit signals automatically, + this must be explicitly initiated by the callback module. +

+

Unless otherwise stated, all functions in this module fail if + the specified gen_statem does not exist or if bad arguments are given. +

+

The gen_statem process can go into hibernation (see + + erlang:hibernate/3 + ) if a + state function or + Module:init/1 + specifies 'hibernate' in the returned + StateOps list. + This might be useful if the server is expected to be idle + for a long time. However use this feature with care + since hibernation implies at least two garbage collections + (when hibernating and shortly after waking up) and that is not + something you'd want to do between each event on a busy server. +

+
+ + + + + +

Name specification to use when starting a gen_statem server. + See + start_link/3 + and + + server_ref() + below. +

+
+
+ + + +

Server specification to use when addressing a gen_statem server. + See call/2 and + + server_name() + above. +

+

It can be:

+ + the pid(), + Name, + if the gen_statem is locally registered, + + {Name,Node}, + if the gen_statem is locally registered at another node, or + + {global,GlobalName}, + if the gen_statem is globally registered. + + {via,RegMod,ViaName}, + if the gen_statem is registered through + an alternative process registry. + The registry callback module RegMod + should export the functions + register_name/2, unregister_name/1, + whereis_name/1 and send/2, + which should behave like the corresponding functions + in global. + Thus, {via,global,GlobalName} is the same as + {global,GlobalName}. + + +
+
+ + + +

Debug option that can be used when starting + a gen_statem server through for example + enter_loop/4. +

+

For every entry in Dbgs + the corresponding function in + sys will be called. +

+
+
+ + + +

Options that can be used when starting + a gen_statem server through for example + start_link/3. +

+
+
+ + + +

Return value from the start functions for_example + start_link/3. +

+
+
+ + + + +

Client address to use when replying through for example the + state_op() + {reply,Client,Reply} to a client + that has called the gen_statem server using + call/2. +

+
+
+ + + +

If the gen_statem State is an atom(), the + state function is + Module:State/5. + If it is any other term() the + state function is + + Module:handle_event/5 + . After a state change (NewState =/= State) + all postponed events are retried. +

+
+
+ + + +

A term() in which the state machine implementation + should store any state data it needs. The difference between + this data and the + state() + itself is that a change in this data does not cause + postponed events to be retried. +

+
+
+ + + +

External events are of 3 different type: + {call,Client}, cast or info. + Calls (synchronous) and casts (asynchronous) + originate from the corresponding API functions. + For calls the event contain whom to reply to. + Type info originates from normal messages sent + to the gen_statem process. + It is also possible for the state machine + implementation to insert events to itself, + in particular of types + timeout and internal. +

+
+
+ + + +

A fun() of arity 2 that takes an event + and returns a boolean. + When used in {remove_event,RemoveEventPredicate} + from state_op(). + The event for which the predicate returns true will + be removed. +

+

+ The predicate may not use a throw exception + to return its result. +

+
+
+ + + +

Either a + + state_option() + of which the first occurence + in the containing list takes precedence, or a + + state_operation() + that are performed in order of + the containing list. +

+

These may be returned from the + state function + or from Module:init/1. +

+

The gen_statem enqueues postponed events and + not yet processed events in order of arrival, except for + an event that a callback function inserts with + {insert_event,EventType,EventContent} that is inserted + as the next event to process. +

+

When the state machine changes states all enqueued events + becomes not yet processed to be processed before the old + not yet processed events. In other words; the order of arrival + is retained. +

+

The processing order is:

+ + If the option retry is true + the current event is enqueued as postponed to be retried. + + If the state changes all postponed events + are transferred to not yet processed to be processed + before other not yet processed events. + + All operations are processed in order of appearance. + + The timeout option is processed if present. + So a state timer may be started or a timeout zero event + may be inserted as if just received. + + The (possibly new) + state function + is called with the next not yet processed event + if there is any, otherwise the gen_statem goes into receive + or hibernation (if the option hibernate is true) + to wait for the next message. In hibernation the next + non-system event awakens the gen_statem. + + +
+
+ + + + + retry + {retry,Retry} + If Retry =:= true + or plain retry postpone the current event + to be retried after a state change. + + hibernate + {hibernate,Hibernate} + If Hibernate =:= true + or plain hibernate hibernate the gen_statem by calling + + proc_lib:hibernate/3 + before receive to wait for a new event. + If there are not yet processed events the + hibernate operation is ignored as if an event + just arrived and awakened the gen_statem. + + + {timeout,Time,Msg} + + Generate an event of + type timeout + after Time milliseconds unless some other + event is received before that time. Note that a retried + event counts just like a new in this respect. + If Time =:= infinity or Time =:= 0 + no timer is started but for zero time the timeout + event is enqued as just received after all + other already received events. + Also note that it is not possible + to cancel this timeout using the + + state_operation() + cancel_timer. + This timeout is cancelled automatically by any event. + + + + + + + + + + {reply,Client,Reply} + + Reply to a client that called + call/2. + Client must be the term from the + + {call,Client} + argument to the + state function. + + {stop,Reason} + The gen_statem will call + + Module:terminate/3 + with Reason and terminate. + + + + {insert_event,EventType,EventContent} + + + Insert the given event as the next to process + before any other not yet processed events. + An event of type + + internal + should be used when you want to reliably distinguish + an event inserted this way from any external event. + + + + {remove_event,EventType,EventContent} + + + Remove the oldest queued event + that matches equal to the given event. + + + + {remove_event,EventPredicate} + + + Remove the oldest queued event for which + the EventPredicate returns true. + + {cancel_timer,TimerRef} + Uses TimerRef when calling + + erlang:cancel_timer/2 + to cancel a timer, cleans the gen_statem's + message queue from any late timeout message from + the timer, and removes any late timeout message + from the queued events using + {remove_event,EventPredicate} above. + This is a convenience function that saves quite some + lines of code and testing time over doing it from + the primitives mentioned above. + + {demonitor,MonitorRef} + Like {cancel_timer,_} above but for + + demonitor/2 + . + + {unlink,Id} + Like {cancel_timer,_} above but for + + unlink/1 + . + + + + +
+ + + + + + + Create a linked gen_statem process + +

Creates a gen_statem process according to OTP design principles + (using + proc_lib + primitives) + that is linked to the calling process. + This is essential when the gen_statem shall be part of + a supervision tree so it gets linked to its supervisor. +

+

The gen_statem process calls + Module:init/1 + to initialize the server. To ensure a synchronized start-up + procedure, start_link/3,4 does not return until + Module:init/1 + has returned. +

+

ServerName specifies the + + server_name() + to register for the gen_statem. + If the gen_statem is started with start_link/3 + no ServerName is provided and + the gen_statem is not registered. +

+

Module is the name of the callback module.

+

Args is an arbitrary term which is passed as + the argument to + Module:init/1 + . +

+

If the option {timeout,Time} is present in + Options, the gen_statem is allowed to spend + Time milliseconds initializing or it will be + terminated and the start function will return + + {error,timeout} + . +

+

If the option + {debug,Dbgs} + is present in Options, debugging through + sys is activated. +

+

If the option {spawn_opt,SOpts} is present in + Options, SOpts will be passed + as option list to the spawn_opt BIF + which is used to + spawn + the gen_statem. +

+ +

Using the spawn option monitor is currently not + allowed, but will cause this function to fail with reason + badarg.

+
+

If the gen_statem is successfully created and initialized + this function returns + + {ok,Pid}, + where Pid is the pid() of the gen_statem. + If there already exists a process with the specified + ServerName this function returns + + {error,{already_started,Pid}} + , where Pid is the pid() of that process. +

+

If Module:init/1 fails with Reason, + this function returns + + {error,Reason} + . If Module:init/1 returns + + {stop,Reason} + + or + + ignore + , the process is terminated and this function + returns + + {error,Reason} + or + + ignore + , respectively. +

+
+
+ + + + + + Create a stand-alone gen_statem process + +

Creates a stand-alone gen_statem process according to + OTP design principles (using + proc_lib + primitives). + Since it does not get linked to the calling process + this start function can not be used by a supervisor + to start a child. +

+

See start_link/3,4 + for a description of arguments and return values. +

+
+
+ + + + Synchronously stop a generic server + +

The same as + + stop(ServerRef, normal, infinity) + . +

+
+
+ + + Synchronously stop a generic server + +

Orders the gen_statem + + ServerRef + to exit with the given Reason + and waits for it to terminate. + The gen_statem will call + + Module:terminate/3 + before exiting. +

+

This function returns ok if the server terminates + with the expected reason. Any other reason than normal, + shutdown, or {shutdown,Term} will cause an + error report to be issued through + + error_logger:format/2 + . + The default Reason is normal. +

+

Timeout is an integer greater than zero + which specifies how many milliseconds to wait for the server to + terminate, or the atom infinity to wait indefinitely. + The default value is infinity. + If the server has not terminated within the specified time, + a timeout exception is raised. +

+

If the process does not exist, a noproc exception + is raised. +

+
+
+ + + + + Make a synchronous call to a gen_statem + +

Makes a synchronous call to the gen_statem + + ServerRef + by sending a request + and waiting until its reply arrives. + The gen_statem will call the + state function with + event_type() + {call,Client} and event content + Request. +

+

A Reply is generated when a + state function + returns with + {reply,Client,Reply} as one + state_op(), + and that Reply becomes the return value + of this function. +

+

Timeout is an integer greater than zero + which specifies how many milliseconds to wait for a reply, + or the atom infinity to wait indefinitely, + which is the default. If no reply is received within + the specified time, the function call fails. + +

To avoid getting a late reply in the caller's + inbox this function spawns a proxy process that + does the call. A late reply gets delivered to the + dead proxy process hence gets discarded. This is + less efficient than using + Timeout =:= infinity. +

+ +

+

The call may fail for example if the gen_statem dies + before or during this function call. +

+
+
+ + + + Send an asynchronous event to a gen_statem + +

Sends an asynchronous event to the gen_statem + + ServerRef + and returns ok immediately, + ignoring if the destination node or gen_statem does not exist. + The gen_statem will call the + state function with + event_type() + cast and event content + Msg. +

+
+
+ + + + Send a reply to a client + +

This function can be used by a gen_statem to explicitly send + a reply to a client that called + call/2 + when the reply cannot be defined in + the return value of the + state function. +

+

Client must be the term from the + + {call,Client} + argument to the + state function. +

+ +

A reply sent with this function will not be visible + in sys debug output. +

+
+
+
+ + + + Enter the gen_statem receive loop + +

The same as + enter_loop/6 + except that no + + server_name() + must have been registered. +

+
+
+ + + Enter the gen_statem receive loop + +

If Server_or_StateOps is a list() + the same as + enter_loop/6 + except that no + + server_name() + must have been registered and + StateOps = Server_or_StateOps. +

+

Otherwise the same as + enter_loop/6 + with + Server = Server_or_StateOps and + StateOps = []. +

+
+
+ + + Enter the gen_statem receive loop + +

Makes an the calling process become a gen_statem. Does not return, + instead the calling process will enter the gen_statem receive + loop and become a gen_statem server. The process + must have been started using one of the start + functions in + proc_lib. + The user is responsible for any initialization of the process, + including registering a name for it. +

+

This function is useful when a more complex initialization + procedure is needed than the gen_statem behaviour provides. +

+

Module, Options and + Server have the same meanings + as when calling + gen_statem:start[_link]/3,4. + However, the + + server_name() + name must have been registered accordingly + before this function is called.

+

State and StateData + have the same meanings as in the return value of + Module:init/1. + Also, the callback module Module + does not need to export an init/1 function. +

+

Failure: If the calling process was not started by a + proc_lib + start function, or if it is not registered + according to + + server_name() + . +

+
+
+ +
+ + + +
+ CALLBACK FUNCTIONS +

The following functions should be exported from a + gen_statem callback module. +

+
+ + + + Module:init(Args) -> Result + Initialize process and internal state + + Args = term() + Result = {ok,State,StateData} +  | {ok,State,StateData,StateOps} +  | {stop,Reason} | ignore + State = state() + StateData = state_data() + StateOps = [state_op()] + Reason = term() + + + +

Whenever a gen_statem is started using + gen_statem:start_link/3,4 + or + gen_statem:start/3,4, + this function is called by the new process to initialize + the implementation loop data. +

+

Args is the Args argument provided to the start + function.

+

If the initialization is successful, the function should + return {ok,State,StateData} or + {ok,State,StateData,StateOps}. + State is the state + of the gen_statem. +

+

The StateOps + are executed before entering the first + state just as for a + state function. +

+

If something goes wrong during the initialization + the function should return {stop,Reason} + or ignore. See + gen_statem:start_link/3,4. +

+
+
+ + + Module:handle_event(EventType, EventContent, + PrevState, State, StateData) -> Result + + Module:State(EventType, EventContent, + PrevState, State, StateData) -> Result + + Handle an event + + EventType = + event_type() + + EventContent = term() + Result = {NewState,NewStateData,StateOps} +   | {NewState,NewStateData} +   The same as {NewState,NewStateData,[]} +   | {NewStateData} +   The same as {State,NewStateData,[retry]} +   | {} +   The same as {State,StateData,[]} +   | StateOps +   The same as {State,StateData,StateOps} + + PrevState = State = NewState = + state() + + StateData = NewStateData = + state_data() + + StateOps = + [state_op()] + + + +

Whenever a gen_statem receives an event from + gen_statem:call/2, + gen_statem:cast/2 or + as a normal process message this function is called. + If the EventType is + {call,Client} + the client is waiting for a reply. The reply can be sent + from this or from any other + state function + by returning with {reply,Client,Reply} in + StateOps + or by calling + + gen_statem:reply(Client, Reply) + . +

+

State + is the internal state of the gen_statem which + when State is an atom() + is the same as this function's name, so it is seldom useful, + except for example when comparing with PrevState + that is the gen_statem's previous state, or in + + Module:handle_event/5 + since that function is common for all states + that are not an atom(). +

+

If this function returns with + NewState =/= State + all postponed events will be retried in the new state. +

+

See state_op() + for the operations that can be done by gen_statem + after returning from this function. +

+
+
+ + + Module:terminate(Reason, State, StateData) + Clean up before termination + + Reason = normal | shutdown | {shutdown,term()} | term() + State = state() + StateData = + + state_data() + + + + +

This function is called by a gen_statem when it is about to + terminate. It should be the opposite of + Module:init/1 + and do any necessary cleaning up. When it returns, + the gen_statem terminates with Reason. The return + value is ignored.

+

Reason is a term denoting the stop reason and + State + is the internal state of the gen_statem. +

+

Reason depends on why the gen_statem is terminating. + If it is because another callback function has returned a + stop tuple {stop,Reason} in + StateOps, + Reason will have the value specified in that tuple. + If it is due to a failure, Reason is the error reason. +

+

If the gen_statem is part of a supervision tree and is + ordered by its supervisor to terminate, this function will be + called with Reason = shutdown if the following + conditions apply:

+ + the gen_statem has been set to trap exit signals, and + the shutdown strategy as defined in the supervisor's + child specification is an integer timeout value, not + brutal_kill. + + +

Even if the gen_statem is not part of a supervision tree, + this function will be called if it receives an 'EXIT' + message from its parent. Reason will be the same as in + the 'EXIT' message. +

+

Otherwise, the gen_statem will be immediately terminated. +

+

Note that for any other reason than normal, + shutdown, or {shutdown,Term} the gen_statem is + assumed to terminate due to an error and + an error report is issued using + + error_logger:format/2 + . +

+
+
+ + + Module:code_change(OldVsn, OldState, OldStateData, Extra) -> + Result + + Update the internal state during upgrade/downgrade + + OldVsn = Vsn | {down,Vsn} +   Vsn = term() + OldState = NewState = term() + Extra = term() + Result = {ok,{NewState,NewStateData}} | Reason + OldState = NewState = + state() + + OldStateData = NewStateData = + state_data() + + Reason = term() + + +

This function is called by a gen_statem when it should + update its internal state during a release upgrade/downgrade, + i.e. when the instruction {update,Module,Change,...} + where Change={advanced,Extra} is given in + the appup file. See + + OTP Design Principles + + for more information. +

+

In the case of an upgrade, OldVsn is Vsn, and + in the case of a downgrade, OldVsn is + {down,Vsn}. Vsn is defined by the vsn + attribute(s) of the old version of the callback module + Module. If no such attribute is defined, the version + is the checksum of the BEAM file. +

+

OldState and OldStateData is the internal state + of the gen_statem. +

+

Extra is passed as-is from the {advanced,Extra} + part of the update instruction. +

+

If successful, the function shall return the updated + internal state in an + {ok,{NewState,NewStateData}} tuple. +

+

If the function returns Reason, the ongoing + upgrade will fail and roll back to the old release.

+
+
+ + + Module:format_status(Opt, [PDict,State,StateData]) -> + Status + + Optional function for providing a term describing the + current gen_statem status + + Opt = normal | terminate + PDict = [{Key, Value}] + State = + state() + + StateData = + state_data() + + Key = term() + Value = term() + Status = term() + + + +

This callback is optional, so callback modules need not + export it. The gen_statem module provides a default + implementation of this function that returns the callback + module state. +

+
+

This function is called by a gen_statem process when:

+ + One of + + sys:get_status/1,2 + + is invoked to get the gen_statem status. Opt is set + to the atom normal for this case. + + The gen_statem terminates abnormally and logs an error. + Opt is set to the atom terminate for this case. + + +

This function is useful for customising the form and + appearance of the gen_statem status for these cases. A + callback module wishing to customise the + + sys:get_status/1,2 + return value as well as how + its status appears in termination error logs exports an + instance of format_status/2 that returns a term + describing the current status of the gen_statem. +

+

PDict is the current value of the gen_statem's + process dictionary. +

+

State + is the internal state of the gen_statem. +

+

The function should return Status, a term that + customises the details of the current state and status of + the gen_statem. There are no restrictions on the + form Status can take, but for the + + sys:get_status/1,2 + case (when Opt + is normal), the recommended form for + the Status value is [{data, [{"State", + Term}]}] where Term provides relevant details of + the gen_statem state. Following this recommendation isn't + required, but doing so will make the callback module status + consistent with the rest of the + + sys:get_status/1,2 + return value. +

+

One use for this function is to return compact alternative + state representations to avoid having large state terms + printed in logfiles. +

+
+
+ +
+ +
+ SEE ALSO +

gen_event, + gen_fsm, + gen_server, + supervisor, + proc_lib, + sys

+
+
diff --git a/lib/stdlib/doc/src/ref_man.xml b/lib/stdlib/doc/src/ref_man.xml index 82ad78e675..404873ea32 100644 --- a/lib/stdlib/doc/src/ref_man.xml +++ b/lib/stdlib/doc/src/ref_man.xml @@ -4,7 +4,7 @@
- 19962015 + 19962016 Ericsson AB. All Rights Reserved. @@ -66,6 +66,7 @@ + diff --git a/lib/stdlib/doc/src/specs.xml b/lib/stdlib/doc/src/specs.xml index 0418bf7b22..45b207b13d 100644 --- a/lib/stdlib/doc/src/specs.xml +++ b/lib/stdlib/doc/src/specs.xml @@ -30,6 +30,7 @@ + diff --git a/lib/stdlib/src/Makefile b/lib/stdlib/src/Makefile index 9f4a446ea0..302834f9d0 100644 --- a/lib/stdlib/src/Makefile +++ b/lib/stdlib/src/Makefile @@ -1,7 +1,7 @@ # # %CopyrightBegin% # -# Copyright Ericsson AB 1996-2015. All Rights Reserved. +# Copyright Ericsson AB 1996-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. @@ -85,6 +85,7 @@ MODULES= \ gen_event \ gen_fsm \ gen_server \ + gen_statem \ io \ io_lib \ io_lib_format \ diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl new file mode 100644 index 0000000000..9bb5ed013b --- /dev/null +++ b/lib/stdlib/src/gen_statem.erl @@ -0,0 +1,1095 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 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. +%% 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. +%% +%% %CopyrightEnd% +%% +-module(gen_statem). + +%% API +-export( + [start/3,start/4,start_link/3,start_link/4, + stop/1,stop/3, + cast/2,call/2,call/3, + enter_loop/4,enter_loop/5,enter_loop/6, + reply/2]). + +%% gen callbacks +-export( + [init_it/6]). + +%% sys callbacks +-export( + [system_continue/3, + system_terminate/4, + system_code_change/4, + system_get_state/1, + system_replace_state/2, + format_status/2]). + +%% Internal callbacks +-export( + [wakeup_from_hibernate/3]). + +%%%========================================================================== +%%% Interface functions. +%%%========================================================================== + +-type client() :: + {To :: pid(), Tag :: term()}. % Reply-to specifier for call +-type state() :: + atom() | % Calls state callback function State/5 + term(). % Calls state callback function handle_event/5 +-type state_data() :: term(). +-type event_type() :: + {'call',Client :: client()} | 'cast' | + 'info' | 'timeout' | 'internal'. +-type event_predicate() :: % Return true for the event in question + fun((event_type(), term()) -> boolean()). +-type state_op() :: + %% First NewState and NewStateData are set, + %% then all state_operations() are executed in order of + %% apperance. Postponing the current event is performed + %% (iff state_option() 'retry' is 'true'). + %% Lastly pending events are processed or if there are + %% no pending events the server goes into receive + %% or hibernate (iff state_option() 'hibernate' is 'true') + state_option() | state_operation(). +-type state_option() :: + %% The first of each kind in the state_op() list takes precedence + 'retry' | % Postpone the current event to a different (=/=) state + {'retry', Retry :: boolean()} | + 'hibernate' | % Hibernate the server instead of going into receive + {'hibernate', Hibernate :: boolean()} | + {'timeout', % Generate a ('timeout', Msg, ...) event after Time + Time :: timeout(), Msg :: term()}. +-type state_operation() :: + %% These can occur multiple times and are executed in order + %% of appearence in the state_op() list + {'reply', % Reply to a client + Client :: client(), Reply :: term()} | + {'stop', Reason :: term()} | % Stop the server + {'insert_event', % Insert event as the next to handle + EventType :: event_type(), + EventContent :: term()} | + {'remove_event', % Remove the oldest matching (=:=) event + EventType :: event_type(), EventContent :: term()} | + {'remove_event', % Remove the oldest event satisfying predicate + EventPredicate :: event_predicate()} | + {'cancel_timer', % Cancel timer and clean up mess(ages) + TimerRef :: reference()} | + {'demonitor', % Demonitor and clean up mess(ages) + MonitorRef :: reference()} | + {'unlink', % Unlink and clean up mess(ages) + Id :: pid() | port()}. + +%% The state machine init function. It is called only once and +%% the server is not running until this function has returned +%% an {ok, ...} tuple. Thereafter the state callbacks are called +%% for all events to this server. +-callback init(Args :: term()) -> + {'ok', state(), state_data()} | + {'ok', state(), state_data(), [state_op()]} | + 'ignore' | + {'stop', Reason :: term()}. + +%% An example callback for a fictive state 'handle_event' +%% that you should avoid having. See below. +%% +%% Note that state callbacks and only state callbacks have arity 5 +%% and that is intended. +%% +%% You should not actually use 'handle_event' as a state name, +%% since it is the callback function that is used if you would use +%% a State that is not an atom(). This is because since there is +%% no obvious way to decide on a state function name from any term(). +-callback handle_event( + event_type(), + EventContent :: term(), + PrevState :: state(), + State :: state(), % Current state + StateData :: state_data()) -> + [state_op()] | % {State,StateData,[state_op()]} + {} | % {State,StateData,[]} + {NewStateData :: state_data()} | % {State,NewStateData,[retry]} + {NewState :: state(), + NewStateData :: state_data()} | % {NewState,NewStateData,[]} + {NewState :: state(), NewStateData :: state_data(), [state_op()]}. + +%% Clean up before the server terminates. +-callback terminate( + Reason :: 'normal' | 'shutdown' | {'shutdown', term()} + | term(), + State :: state(), + StateData :: state_data()) -> + any(). + +%% Note that the new code can expect to get an OldState from +%% the old code version not only in code_change/4 but in the first +%% state callback function called thereafter +-callback code_change( + OldVsn :: term() | {'down', term()}, + OldState :: state(), + OldStateData :: state_data(), + Extra :: term()) -> + {ok, {NewState :: state(), NewStateData :: state_data()}}. + +%% Format the callback module state in some sensible that is +%% often condensed way. For StatusOption =:= 'normal' the perferred +%% return term is [{data,[{"State",FormattedState}]}], and for +%% StatusOption =:= 'terminate' it is just FormattedState. +-callback format_status( + StatusOption, + [ [{Key :: term(), Value :: term()}] | + state() | + state_data()]) -> + Status :: term() when + StatusOption :: 'normal' | 'terminate'. + +-optional_callbacks( + [format_status/2, % Has got a default implementation + handle_event/5]). % Only needed for State not an atom() +%% For every atom() State there has to be a State/5 callback function + +%% Type validation functions +client({Pid,Tag}) when is_pid(Pid), is_reference(Tag) -> + true; +client(_) -> + false. +%% +event_type({call,Client}) -> + client(Client); +event_type(Type) -> + case Type of + cast -> + true; + info -> + true; + timeout -> + true; + internal -> + true; + _ -> + false + end. + +%%%========================================================================== +%%% API + +-type server_name() :: + {'global', GlobalName :: term()} + | {'via', RegMod :: module(), Name :: term()} + | {'local', atom()}. +-type server_ref() :: + {'global', GlobalName :: term()} + | {'via', RegMod :: module(), ViaName :: term()} + | (LocalName :: atom()) + | {Name :: atom(), Node :: atom()} + | pid(). +-type debug_opt() :: + {'debug', + Dbgs :: + ['trace' | 'log' | 'statistics' | 'debug' + | {'logfile', string()}]}. +-type start_opt() :: + debug_opt() + | {'timeout', Time :: timeout()} + | {'spawn_opt', SOpts :: [proc_lib:spawn_option()]}. +-type start_ret() :: {'ok', pid()} | 'ignore' | {'error', term()}. + + + +%% Start a state machine +-spec start( + Module :: module(), Args :: term(), Options :: [start_opt()]) -> + start_ret(). +start(Module, Args, Options) -> + gen:start(?MODULE, nolink, Module, Args, Options). +%% +-spec start( + ServerName :: server_name(), + Module :: module(), Args :: term(), Options :: [start_opt()]) -> + start_ret(). +start(ServerName, Module, Args, Options) -> + gen:start(?MODULE, nolink, ServerName, Module, Args, Options). + +%% Start and link to a state machine +-spec start_link( + Module :: module(), Args :: term(), Options :: [start_opt()]) -> + start_ret(). +start_link(Module, Args, Options) -> + gen:start(?MODULE, link, Module, Args, Options). +%% +-spec start_link( + ServerName :: server_name(), + Module :: module(), Args :: term(), Options :: [start_opt()]) -> + start_ret(). +start_link(ServerName, Module, Args, Options) -> + gen:start(?MODULE, link, ServerName, Module, Args, Options). + +%% Stop a state machine +-spec stop(ServerRef :: server_ref()) -> ok. +stop(ServerRef) -> + gen:stop(ServerRef). +%% +-spec stop( + ServerRef :: server_ref(), + Reason :: term(), + Timeout :: timeout()) -> ok. +stop(ServerRef, Reason, Timeout) -> + gen:stop(ServerRef, Reason, Timeout). + +%% Send an event to a state machine that arrives with type 'event' +-spec cast(ServerRef :: server_ref(), Msg :: term()) -> ok. +cast({global,Name}, Msg) -> + try global:send(Name, cast(Msg)) of + _ -> ok + catch + _:_ -> ok + end; +cast({via,RegMod,Name}, Msg) -> + try RegMod:send(Name, cast(Msg)) of + _ -> ok + catch + _:_ -> ok + end; +cast({Name,Node} = ServerRef, Msg) when is_atom(Name), is_atom(Node) -> + do_send(ServerRef, cast(Msg)); +cast(ServerRef, Msg) when is_atom(ServerRef) -> + do_send(ServerRef, cast(Msg)); +cast(ServerRef, Msg) when is_pid(ServerRef) -> + do_send(ServerRef, cast(Msg)). + +%% Call a state machine (synchronous; a reply is expected) that +%% arrives with type {call,Client} +-spec call(ServerRef :: server_ref(), Request :: term()) -> Reply :: term(). +call(ServerRef, Request) -> + call(ServerRef, Request, infinity). +%% +-spec call( + ServerRef :: server_ref(), + Request :: term(), + Timeout :: timeout()) -> + Reply :: term(). +call(ServerRef, Request, infinity) -> + try gen:call(ServerRef, '$gen_call', Request, infinity) of + {ok,Reply} -> + Reply + catch + Class:Reason -> + erlang:raise( + Class, + {Reason,{?MODULE,call,[ServerRef,Request,infinity]}}, + erlang:get_stacktrace()) + end; +call(ServerRef, Request, Timeout) -> + %% Call server through proxy process to dodge any late reply + Ref = make_ref(), + Self = self(), + Pid = spawn( + fun () -> + Self ! + try gen:call( + ServerRef, '$gen_call', Request, Timeout) of + Result -> + {Ref,Result} + catch Class:Reason -> + {Ref,Class,Reason,erlang:get_stacktrace()} + end + end), + Mref = monitor(process, Pid), + receive + {Ref,Result} -> + demonitor(Mref, [flush]), + case Result of + {ok,Reply} -> + Reply + end; + {Ref,Class,Reason,Stacktrace} -> + demonitor(Mref, [flush]), + erlang:raise( + Class, + {Reason,{?MODULE,call,[ServerRef,Request,Timeout]}}, + Stacktrace); + {'DOWN',Mref,_,_,Reason} -> + %% There is just a theoretical possibility that the + %% proxy process gets killed between try--of and ! + %% so this clause is in case of that + exit(Reason) + end. + +%% Reply from a state machine callback to whom awaits in call/2 +-spec reply(Client :: client(), Reply :: term()) -> ok. +reply({To,Tag}, Reply) -> + Msg = {Tag,Reply}, + try To ! Msg of + _ -> + ok + catch + _:_ -> ok + end. + +%% Instead of starting the state machine through start/3,4 +%% or start_link/3,4 turn the current process presumably +%% started by proc_lib into a state machine using +%% the same arguments as you would have returned from init/1 +-spec enter_loop( + Module :: module(), Options :: [debug_opt()], + State :: state(), StateData :: state_data()) -> + no_return(). +enter_loop(Module, Options, State, StateData) -> + enter_loop(Module, Options, State, StateData, self()). +%% +-spec enter_loop( + Module :: module(), Options :: [debug_opt()], + State :: state(), StateData :: state_data(), + Server_or_StateOps :: server_name() | pid() | [state_op()]) -> + no_return(). +enter_loop(Module, Options, State, StateData, Server_or_StateOps) -> + if + is_list(Server_or_StateOps) -> + enter_loop( + Module, Options, State, StateData, + self(), Server_or_StateOps); + true -> + enter_loop( + Module, Options, State, StateData, + Server_or_StateOps, []) + end. +%% +-spec enter_loop( + Module :: module(), Options :: [debug_opt()], + State :: state(), StateData :: state_data(), + Server :: server_name() | pid(), + StateOps :: [state_op()]) -> + no_return(). +enter_loop(Module, Options, State, StateData, Server, StateOps) -> + Parent = gen:get_parent(), + enter(Module, Options, State, StateData, Server, StateOps, Parent). + +%%--------------------------------------------------------------------------- +%% API helpers + +cast(Event) -> + {'$gen_cast',Event}. + +%% Might actually not send the message in case of caught exception +do_send(Proc, Msg) -> + try erlang:send(Proc, Msg, [noconnect]) of + noconnect -> + _ = spawn(erlang, send, [Proc,Msg]), + ok; + ok -> + ok + catch + _:_ -> + ok + end. + +%% Here init_it and all enter_loop functions converge +enter(Module, Options, State, StateData, Server, StateOps, Parent) -> + Name = gen:get_proc_name(Server), + Debug = gen:debug_options(Name, Options), + PrevState = make_ref(), + S = #{ + module => Module, + name => Name, + prev_state => PrevState, + state => PrevState, + state_data => StateData, + timer => undefined, + postponed => [], + hibernate => false}, + loop_event_state_ops( + Parent, Debug, S, [], {event,undefined}, + State, StateData, [{retry,false}|StateOps]). + +%%%========================================================================== +%%% gen callbacks + +init_it(Starter, Parent, ServerRef, Module, Args, Options) -> + try Module:init(Args) of + Result -> + init_result(Starter, Parent, ServerRef, Module, Result, Options) + catch + Result -> + init_result(Starter, Parent, ServerRef, Module, Result, Options); + Class:Reason -> + gen:unregister_name(ServerRef), + proc_lib:init_ack(Starter, {error,Reason}), + erlang:raise(Class, Reason, erlang:get_stacktrace()) + end. + +%%--------------------------------------------------------------------------- +%% gen callbacks helpers + +init_result(Starter, Parent, ServerRef, Module, Result, Options) -> + case Result of + {ok,State,StateData} -> + proc_lib:init_ack(Starter, {ok,self()}), + enter( + Module, Options, State, StateData, ServerRef, + [], Parent); + {ok,State,StateData,StateOps} -> + proc_lib:init_ack(Starter, {ok,self()}), + enter( + Module, Options, State, StateData, ServerRef, + StateOps, Parent); + {stop,Reason} -> + gen:unregister_name(ServerRef), + proc_lib:init_ack(Starter, {error,Reason}), + exit(Reason); + ignore -> + gen:unregister_name(ServerRef), + proc_lib:init_ack(Starter, ignore), + exit(normal); + Other -> + Error = {bad_return_value,Other}, + proc_lib:init_ack(Starter, {error,Error}), + exit(Error) + end. + +%%%========================================================================== +%%% sys callbacks + +system_continue(Parent, Debug, S) -> + loop(Parent, Debug, S). + +system_terminate(Reason, _Parent, Debug, S) -> + terminate(Reason, Debug, S, []). + +system_code_change( + #{module := Module, + state := State, + state_data := StateData} = S, + _Mod, OldVsn, Extra) -> + case + try Module:code_change(OldVsn, State, StateData, Extra) + catch + Result -> Result + end + of + {ok,{NewState,NewStateData}} -> + {ok, + S#{ + state := NewState, + state_data := NewStateData}}; + Error -> + Error + end. + +system_get_state(#{state := State, state_data := StateData}) -> + {ok,{State,StateData}}. + +system_replace_state( + StateFun, + #{state := State, + state_data := StateData} = S) -> + {NewState,NewStateData} = Result = StateFun({State,StateData}), + {ok,Result,S#{state := NewState, state_data := NewStateData}}. + +format_status( + Opt, + [PDict,SysState,Parent,Debug, + #{name := Name, postponed := P} = S]) -> + Header = gen:format_status_header("Status for state machine", Name), + Log = sys:get_debug(log, Debug, []), + [{header,Header}, + {data, + [{"Status",SysState}, + {"Parent",Parent}, + {"Logged Events",Log}, + {"Postponed",P}]} | + case format_status(Opt, PDict, S) of + L when is_list(L) -> L; + T -> [T] + end]. + +%%--------------------------------------------------------------------------- +%% Format debug messages. Print them as the call-back module sees +%% them, not as the real erlang messages. Use trace for that. +%%--------------------------------------------------------------------------- + +print_event(Dev, {in,Event}, #{name := Name}) -> + io:format( + Dev, "*DBG* ~p received ~s~n", + [Name,event_string(Event)]); +print_event(Dev, {out,Reply,{To,_Tag}}, #{name := Name}) -> + io:format( + Dev, "*DBG* ~p sent ~p to ~p~n", + [Name,Reply,To]); +print_event(Dev, {Tag,Event,NewState}, #{name := Name, state := State}) -> + StateString = + case NewState of + State -> + io_lib:format("~p", [State]); + _ -> + io_lib:format("~p => ~p", [State,NewState]) + end, + io:format( + Dev, "*DBG* ~p ~w ~s in state ~s~n", + [Name,Tag,event_string(Event),StateString]). + +event_string(Event) -> + case Event of + {{call,{Pid,_Tag}},Request} -> + io_lib:format("call ~p from ~w", [Request,Pid]); + {Tag,Content} -> + io_lib:format("~w ~p", [Tag,Content]) + end. + +sys_debug(Debug, S, Entry) -> + case Debug of + [] -> + Debug; + _ -> + sys:handle_debug(Debug, fun print_event/3, S, Entry) + end. + +%%%========================================================================== +%%% Internal callbacks + +wakeup_from_hibernate(Parent, Debug, S) -> + %% It is a new message that woke us up so we have to receive it now + loop_receive(Parent, Debug, S). + +%%%========================================================================== +%%% STate Machine engine implementation of proc_lib/gen server + +%% Server loop, consists of all loop* functions +%% and some detours through sys and proc_lib + +%% Entry point for system_continue/3 +loop(Parent, Debug, #{hibernate := Hib} = S) -> + case Hib of + true -> + loop_hibernate(Parent, Debug, S); + false -> + loop_receive(Parent, Debug, S) + end. + +loop_hibernate(Parent, Debug, S) -> + %% Does not return but restarts process at + %% wakeup_from_hibernate/3 that jumps to loop_receive/3 + proc_lib:hibernate( + ?MODULE, wakeup_from_hibernate, [Parent,Debug,S]), + error( + {should_not_have_arrived_here_but_instead_in, + {wakeup_from_hibernate,3}}). + +%% Entry point for wakeup_from_hibernate/3 +loop_receive(Parent, Debug, #{timer := Timer} = S) -> + receive + Msg -> + case Msg of + {system,Pid,Req} -> + %% Does not return but tail recursively calls + %% system_continue/3 that jumps to loop/3 + sys:handle_system_msg( + Req, Pid, Parent, ?MODULE, Debug, S, + maps:get(hibernate, S)); + {'EXIT',Parent,Reason} = EXIT -> + %% EXIT is not a 2-tuple and therefore + %% not an event and has no event_type(), + %% but this will stand out in the crash report... + terminate(Reason, Debug, S, [EXIT]); + {timeout,Timer,Content} when Timer =/= undefined -> + loop_receive( + Parent, Debug, S, {timeout,Content}, undefined); + _ -> + Event = + case Msg of + {'$gen_call',Client,Request} -> + {{call,Client},Request}; + {'$gen_cast',E} -> + {cast,E}; + _ -> + {info,Msg} + end, + loop_receive(Parent, Debug, S, Event, Timer) + end + end. + +loop_receive(Parent, Debug, S, Event, Timer) -> + NewDebug = sys_debug(Debug, S, {in,Event}), + %% Here the queue of not yet processed events is created + loop_events(Parent, NewDebug, S, [Event], Timer). + +%% Process first event in queue, or if there is none receive a new +%% +%% The loop_event* functions optimize S map handling by dismantling it, +%% passing the parts in arguments to avoid map lookups and construct the +%% new S map in one go on exit. Premature optimization, I know, but +%% the code was way to readable and there were quite some map lookups +%% repeated in different functions. +loop_events(Parent, Debug, S, [], _Timer) -> + loop(Parent, Debug, S); +loop_events( + Parent, Debug, + #{module := Module, + prev_state := PrevState, + state := State, + state_data := StateData} = S, + [{Type,Content} = Event|Events] = Q, Timer) -> + _ = (Timer =/= undefined) andalso + cancel_timer(Timer), + Func = + if + is_atom(State) -> + State; + true -> + handle_event + end, + try Module:Func(Type, Content, PrevState, State, StateData) of + Result -> + loop_event_result( + Parent, Debug, S, Events, Event, Result) + catch + Result -> + loop_event_result( + Parent, Debug, S, Events, Event, Result); + error:undef -> + %% Process an undef to check for the simple mistake + %% of calling a nonexistent state function + case erlang:get_stacktrace() of + [{Module,State,[Event,StateData]=Args,_}|Stacktrace] -> + terminate( + error, + {undef_state_function,{Module,State,Args}}, + Stacktrace, + Debug, S, Q); + Stacktrace -> + terminate(error, undef, Stacktrace, Debug, S, Q) + end; + Class:Reason -> + Stacktrace = erlang:get_stacktrace(), + terminate(Class, Reason, Stacktrace, Debug, S, Q) + end. + +%% Interprete all callback return value variants +loop_event_result( + Parent, Debug, + #{state := State, state_data := StateData} = S, + Events, Event, Result) -> + case Result of + {} -> % Ignore + loop_event_state_ops( + Parent, Debug, S, Events, Event, + State, StateData, []); + {NewStateData} -> % Retry + loop_event_state_ops( + Parent, Debug, S, Events, Event, + State, NewStateData, [retry]); + {NewState,NewStateData} -> % Consume + loop_event_state_ops( + Parent, Debug, S, Events, Event, + NewState, NewStateData, []); + {NewState,NewStateData,StateOps} when is_list(StateOps) -> + loop_event_state_ops( + Parent, Debug, S, Events, Event, + NewState, NewStateData, StateOps); + StateOps when is_list(StateOps) -> % Stay in state + loop_event_state_ops( + Parent, Debug, S, Events, Event, + State, StateData, StateOps); + BadReturn -> + terminate( + {bad_return_value,BadReturn}, Debug, S, [Event|Events]) + end. + +loop_event_state_ops( + Parent, Debug0, #{state := State, postponed := P0} = S, Events, Event, + NewState, NewStateData, StateOps) -> + case collect_state_options(StateOps) of + {Retry,Hibernate,Timeout,Operations} -> + P1 = % Move current event to postponed if Retry + case Retry of + true -> + [Event|P0]; + false -> + P0 + end, + {Q2,P2} = % Move all postponed events to queue if state change + if + NewState =:= State -> + {Events,P1}; + true -> + {lists:reverse(P1, Events),[]} + end, + %% + case process_state_operations( + Operations, Debug0, S, Q2, P2) of + {Debug,Q3,P} -> + NewDebug = + sys_debug( + Debug, S, + case Retry of + true -> + {retry,Event,NewState}; + false -> + {consume,Event,NewState} + end), + {Timer,Q} = + case Timeout of + undefined -> + {undefined,Q3}; + {timeout,0,Msg} -> + %% Pretend the timeout has just been received + {undefined,Q3 ++ [{timeout,Msg}]}; + {timeout,Time,Msg} -> + {erlang:start_timer(Time, self(), Msg),Q3} + end, + loop_events( + Parent, NewDebug, + S#{ + prev_state := State, + state := NewState, + state_data := NewStateData, + timer := Timer, + hibernate := Hibernate, + postponed := P}, + Q, Timer); + [Reason,Debug] -> + terminate(Reason, Debug, S, [Event|Events]); + [Class,Reason,Stacktrace,Debug] -> + terminate( + Class, Reason, Stacktrace, Debug, S, [Event|Events]) + end; + %% + [Reason] -> + terminate(Reason, Debug0, S, [Event|Events]) + end. + +%%--------------------------------------------------------------------------- +%% Server helpers + +collect_state_options(StateOps) -> + collect_state_options( + lists:reverse(StateOps), false, false, undefined, []). +%% Keep the last of each kind +collect_state_options( + [], Retry, Hibernate, Timeout, Operations) -> + {Retry,Hibernate,Timeout,Operations}; +collect_state_options( + [StateOp|StateOps] = SOSOs, Retry, Hibernate, Timeout, Operations) -> + case StateOp of + retry -> + collect_state_options( + StateOps, true, Hibernate, Timeout, Operations); + {retry,NewRetry} when is_boolean(NewRetry) -> + collect_state_options( + StateOps, NewRetry, Hibernate, Timeout, Operations); + {retry,_} -> + [{bad_state_ops,SOSOs}]; + hibernate -> + collect_state_options( + StateOps, Retry, true, Timeout, Operations); + {hibernate,NewHibernate} when is_boolean(NewHibernate) -> + collect_state_options( + StateOps, Retry, NewHibernate, Timeout, Operations); + {hibernate,_} -> + [{bad_state_ops,SOSOs}]; + {timeout,infinity,_} -> % Ignore since it will never time out + collect_state_options( + StateOps, Retry, Hibernate, undefined, Operations); + {timeout,Time,_} = NewTimeout when is_integer(Time), Time >= 0 -> + collect_state_options( + StateOps, Retry, Hibernate, NewTimeout, Operations); + {timeout,_,_} -> + [{bad_state_ops,SOSOs}]; + _ -> % Collect others as operations + collect_state_options( + StateOps, Retry, Hibernate, Timeout, [StateOp|Operations]) + end. + +process_state_operations([], Debug, _S, Q, P) -> + {Debug,Q,P}; +process_state_operations([Operation|Operations] = OOs, Debug, S, Q, P) -> + case Operation of + {reply,{_To,_Tag}=Client,Reply} -> + reply(Client, Reply), + NewDebug = sys_debug(Debug, S, {out,Reply,Client}), + process_state_operations(Operations, NewDebug, S, Q, P); + {stop,Reason} -> + [Reason,Debug]; + {insert_event,Type,Content} -> + case event_type(Type) of + true -> + process_state_operations( + Operations, Debug, S, [{Type,Content}|Q], P); + false -> + [{bad_state_ops,OOs},Debug] + end; + _ -> + %% All others are remove operations + case remove_fun(Operation) of + false -> + process_state_operations( + Operations, Debug, S, Q, P); + undefined -> + [{bad_state_ops,OOs},Debug]; + RemoveFun when is_function(RemoveFun, 2) -> + case remove_event(RemoveFun, Q, P) of + {NewQ,NewP} -> + process_state_operations( + Operations, Debug, S, NewQ, NewP); + Error -> + Error ++ [Debug] + end; + Error -> + Error ++ [Debug] + end + end. + +%% Remove oldest matching event from the queue(s) +remove_event(RemoveFun, Q, P) -> + try + case remove_tail_event(RemoveFun, P) of + false -> + case remove_head_event(RemoveFun, Q) of + false -> + {P,Q}; + NewQ -> + {P,NewQ} + end; + NewP -> + {NewP,Q} + end + catch + Class:Reason -> + [Class,Reason,erlang:get_stacktrace()] + end. + +%% Do the given state operation and create an event removal predicate fun() +remove_fun({remove_event,Type,Content}) -> + fun (T, C) when T =:= Type, C =:= Content -> true; + (_, _) -> false + end; +remove_fun({remove_event,RemoveFun}) when is_function(RemoveFun, 2) -> + RemoveFun; +remove_fun({cancel_timer,TimerRef}) -> + try cancel_timer(TimerRef) of + false -> + false; + true -> + fun + (info, {timeout,TRef,_}) + when TRef =:= TimerRef -> + true; + (_, _) -> + false + end + catch + Class:Reason -> + [Class,Reason,erlang:get_stacktrace()] + end; +remove_fun({demonitor,MonitorRef}) -> + try erlang:demonitor(MonitorRef, [flush,info]) of + false -> + false; + true -> + fun (info, {'DOWN',MRef,_,_,_}) + when MRef =:= MonitorRef-> + true; + (_, _) -> + false + end + catch + Class:Reason -> + [Class,Reason,erlang:get_stacktrace()] + end; +remove_fun({unlink,Id}) -> + try unlink(Id) of + true -> + receive + {'EXIT',Id,_} -> + ok + after 0 -> + ok + end, + fun (info, {'EXIT',I,_}) + when I =:= Id -> + true; + (_, _) -> + false + end + catch + Class:Reason -> + {Class,Reason,erlang:get_stacktrace()} + end; +remove_fun(_) -> + undefined. + + +%% Cancel a timer and clense the process mailbox returning +%% false if no such timer message can arrive after this or +%% true otherwise +cancel_timer(TimerRef) -> + case erlang:cancel_timer(TimerRef) of + TimeLeft when is_integer(TimeLeft) -> + false; + false -> + receive + {timeout,TimerRef,_} -> + false + after 0 -> + true + end + end. + + +terminate(Reason, Debug, S, Q) -> + terminate(exit, Reason, [], Debug, S, Q). +%% +terminate( + Class, Reason, Stacktrace, Debug, + #{name := Name, module := Module, + state := State, state_data := StateData} = S, + Q) -> + try Module:terminate(Reason, State, StateData) of + _ -> ok + catch + _ -> ok; + C:R -> + ST = erlang:get_stacktrace(), + error_info( + C, R, ST, Debug, Name, Q, + format_status(terminate, get(), S)), + erlang:raise(C, R, ST) + end, + case Reason of + normal -> ok; + shutdown -> ok; + {shutdown,_} -> ok; + _ -> + error_info( + Class, Reason, Stacktrace, Debug, Name, Q, + format_status(terminate, get(), S)) + end, + case Stacktrace of + [] -> + erlang:Class(Reason); + _ -> + erlang:raise(Class, Reason, Stacktrace) + end. + +error_info( + Class, Reason, Stacktrace, Debug, Name, Q, FmtStateData) -> + {FixedReason,FixedStacktrace} = + case Stacktrace of + [{M,F,Args,_}|ST] + when Class =:= error, Reason =:= undef -> + case code:is_loaded(M) of + false -> + {{'module could not be loaded',M},ST}; + _ -> + Arity = length(Args), + case erlang:function_exported(M, F, Arity) of + true -> + {Reason,Stacktrace}; + false -> + {{'function not exported',{M,F,Arity}}, + ST} + end + end; + _ -> {Reason,Stacktrace} + end, + error_logger:format( + "** State machine ~p terminating~n" ++ + case Q of + [] -> + ""; + _ -> + "** Last event = ~p~n" + end ++ + "** When Server state = ~p~n" ++ + "** Reason for termination = ~w:~p~n" ++ + case FixedStacktrace of + [] -> + ""; + _ -> + "** Stacktrace =~n" + "** ~p~n" + end, + [Name | + case Q of + [] -> + [FmtStateData,Class,FixedReason]; + [Event|_] -> + [Event,FmtStateData,Class,FixedReason] + end] ++ + case FixedStacktrace of + [] -> + []; + _ -> + [FixedStacktrace] + end), + sys:print_log(Debug), + ok. + + +%% Call Module:format_status/2 or return a default value +format_status( + Opt, PDict, + #{module := Module, state := State, state_data := StateData}) -> + case erlang:function_exported(Module, format_status, 2) of + true -> + try Module:format_status(Opt, [PDict,State,StateData]) + catch + Result -> Result; + _:_ -> + format_status_default(Opt, State, StateData) + end; + false -> + format_status_default(Opt, State, StateData) + end. + +%% The default Module:format_status/2 +format_status_default(Opt, State, StateData) -> + SSD = {State,StateData}, + case Opt of + terminate -> + SSD; + _ -> + [{data,[{"State",SSD}]}] + end. + +%%--------------------------------------------------------------------------- +%% Farily general helpers + +%% Return the modified list where the first element that satisfies +%% the RemoveFun predicate is removed, or false if no such element exists. +remove_head_event(_RemoveFun, []) -> + false; +remove_head_event(RemoveFun, [{Tag,Content}|Events]) -> + case RemoveFun(Tag, Content) of + false -> + remove_head_event(RemoveFun, Events); + true -> + Events + end. + +%% Return the modified list where the last element that satisfies +%% the RemoveFun predicate is removed, or false if no such element exists. +remove_tail_event(_RemoveFun, []) -> + false; +remove_tail_event(RemoveFun, [{Tag,Content} = Event|Events]) -> + case remove_tail_event(RemoveFun, Events) of + false -> + RemoveFun(Tag, Content) andalso Events; + NewEvents -> + [Event|NewEvents] + end. diff --git a/lib/stdlib/src/proc_lib.erl b/lib/stdlib/src/proc_lib.erl index 10c476a6f5..3f79ed0f87 100644 --- a/lib/stdlib/src/proc_lib.erl +++ b/lib/stdlib/src/proc_lib.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1996-2014. All Rights Reserved. +%% Copyright Ericsson AB 1996-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. @@ -472,13 +472,15 @@ trans_init(gen,init_it,[gen_server,_,_,supervisor_bridge,[Module|_],_]) -> {supervisor_bridge,Module,1}; trans_init(gen,init_it,[gen_server,_,_,_,supervisor_bridge,[Module|_],_]) -> {supervisor_bridge,Module,1}; -trans_init(gen,init_it,[gen_server,_,_,Module,_,_]) -> +trans_init(gen,init_it,[GenMod,_,_,Module,_,_]) + when GenMod =:= gen_server; + GenMod =:= gen_statem; + GenMod =:= gen_fsm -> {Module,init,1}; -trans_init(gen,init_it,[gen_server,_,_,_,Module|_]) -> - {Module,init,1}; -trans_init(gen,init_it,[gen_fsm,_,_,Module,_,_]) -> - {Module,init,1}; -trans_init(gen,init_it,[gen_fsm,_,_,_,Module|_]) -> +trans_init(gen,init_it,[GenMod,_,_,_,Module|_]) + when GenMod =:= gen_server; + GenMod =:= gen_statem; + GenMod =:= gen_fsm -> {Module,init,1}; trans_init(gen,init_it,[gen_event|_]) -> {gen_event,init_it,6}; diff --git a/lib/stdlib/src/stdlib.app.src b/lib/stdlib/src/stdlib.app.src index 7f9bbbf649..dc17a44150 100644 --- a/lib/stdlib/src/stdlib.app.src +++ b/lib/stdlib/src/stdlib.app.src @@ -2,7 +2,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 1996-2015. All Rights Reserved. +%% Copyright Ericsson AB 1996-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. @@ -65,6 +65,7 @@ gen_event, gen_fsm, gen_server, + gen_statem, io, io_lib, io_lib_format, diff --git a/lib/stdlib/test/Makefile b/lib/stdlib/test/Makefile index e366c2b755..427d7fcaea 100644 --- a/lib/stdlib/test/Makefile +++ b/lib/stdlib/test/Makefile @@ -46,6 +46,7 @@ MODULES= \ gen_event_SUITE \ gen_fsm_SUITE \ gen_server_SUITE \ + gen_statem_SUITE \ id_transform_SUITE \ io_SUITE \ io_proto_SUITE \ diff --git a/lib/stdlib/test/error_logger_forwarder.erl b/lib/stdlib/test/error_logger_forwarder.erl index 7ac2cfce82..d34dde9def 100644 --- a/lib/stdlib/test/error_logger_forwarder.erl +++ b/lib/stdlib/test/error_logger_forwarder.erl @@ -1,7 +1,7 @@ %% %% %CopyrightBegin% %% -%% Copyright Ericsson AB 2010. All Rights Reserved. +%% Copyright Ericsson AB 2010-2015. 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. @@ -20,7 +20,7 @@ -module(error_logger_forwarder). %% API. --export([register/0]). +-export([register/0, unregister/0]). %% Internal export for error_logger. -export([init/1, @@ -33,6 +33,10 @@ register() -> error_logger:add_report_handler(?MODULE, self()). +unregister() -> + Self = self(), + Self = error_logger:delete_report_handler(?MODULE). + init(Tester) -> {ok,Tester}. diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl new file mode 100644 index 0000000000..342be32acb --- /dev/null +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -0,0 +1,1250 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 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. +%% 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. +%% +%% %CopyrightEnd% +%% +-module(gen_statem_SUITE). + +-include_lib("test_server/include/test_server.hrl"). + +-compile(export_all). +-behaviour(gen_statem). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +suite() -> [{ct_hooks,[ts_install_cth]}]. + +all() -> + [{group, start}, + {group, abnormal}, + shutdown, + {group, sys}, hibernate, enter_loop]. + +groups() -> + [{start, [], + [start1, start2, start3, start4, start5, start6, start7, + start8, start9, start10, start11, start12]}, + {stop, [], + [stop1, stop2, stop3, stop4, stop5, stop6, stop7, stop8, stop9, stop10]}, + {abnormal, [], [abnormal1, abnormal2]}, + {sys, [], + [sys1, + call_format_status, + error_format_status, terminate_crash_format, + get_state, replace_state]}]. + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + ok. + +init_per_group(_GroupName, Config) -> + Config. + +end_per_group(_GroupName, Config) -> + Config. + +init_per_testcase(_CaseName, Config) -> + ?t:messages_get(), + Dog = ?t:timetrap(?t:minutes(1)), +%%% dbg:tracer(), +%%% dbg:p(all, c), +%%% dbg:tpl(gen_statem, cx), + [{watchdog, Dog} | Config]. + +end_per_testcase(_CaseName, Config) -> +%%% dbg:stop(), + Dog = ?config(watchdog, Config), + test_server:timetrap_cancel(Dog), + Config. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-define(EXPECT_FAILURE(Code, Reason), + try begin Code end of + _ -> + ?t:fail() + catch + error:Reason -> Reason; + exit:Reason -> Reason + end). +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% anonymous +start1(Config) when is_list(Config) -> + %%OldFl = process_flag(trap_exit, true), + + {ok,Pid0} = gen_statem:start_link(?MODULE, [], []), + ok = do_func_test(Pid0), + ok = do_sync_func_test(Pid0), + stop_it(Pid0), +%% stopped = gen_statem:call(Pid0, stop), +%% timeout = +%% ?EXPECT_FAILURE(gen_statem:call(Pid0, hej), Reason), + + %%process_flag(trap_exit, OldFl), + ok = verify_empty_msgq(). + +%% anonymous w. shutdown +start2(Config) when is_list(Config) -> + %% Dont link when shutdown + {ok,Pid0} = gen_statem:start(?MODULE, [], []), + ok = do_func_test(Pid0), + ok = do_sync_func_test(Pid0), + stopped = gen_statem:call(Pid0, {stop,shutdown}), + check_stopped(Pid0), + ok = verify_empty_msgq(). + +%% anonymous with timeout +start3(Config) when is_list(Config) -> + %%OldFl = process_flag(trap_exit, true), + + {ok,Pid0} = gen_statem:start(?MODULE, [], [{timeout,5}]), + ok = do_func_test(Pid0), + ok = do_sync_func_test(Pid0), + stop_it(Pid0), + + {error,timeout} = gen_statem:start(?MODULE, sleep, + [{timeout,5}]), + + %%process_flag(trap_exit, OldFl), + ok = verify_empty_msgq(). + +%% anonymous with ignore +start4(Config) when is_list(Config) -> + OldFl = process_flag(trap_exit, true), + + ignore = gen_statem:start(?MODULE, ignore, []), + + process_flag(trap_exit, OldFl), + ok = verify_empty_msgq(). + +%% anonymous with stop +start5(suite) -> []; +start5(Config) when is_list(Config) -> + OldFl = process_flag(trap_exit, true), + + {error,stopped} = gen_statem:start(?MODULE, stop, []), + + process_flag(trap_exit, OldFl), + ok = verify_empty_msgq(). + +%% anonymous linked +start6(Config) when is_list(Config) -> + {ok,Pid} = gen_statem:start_link(?MODULE, [], []), + ok = do_func_test(Pid), + ok = do_sync_func_test(Pid), + stop_it(Pid), + + ok = verify_empty_msgq(). + +%% global register linked +start7(Config) when is_list(Config) -> + STM = {global,my_stm}, + + {ok,Pid} = + gen_statem:start_link(STM, ?MODULE, [], []), + {error,{already_started,Pid}} = + gen_statem:start_link(STM, ?MODULE, [], []), + {error,{already_started,Pid}} = + gen_statem:start(STM, ?MODULE, [], []), + + ok = do_func_test(Pid), + ok = do_sync_func_test(Pid), + ok = do_func_test(STM), + ok = do_sync_func_test(STM), + stop_it(STM), + + ok = verify_empty_msgq(). + + +%% local register +start8(Config) when is_list(Config) -> + %%OldFl = process_flag(trap_exit, true), + Name = my_stm, + STM = {local,Name}, + + {ok,Pid} = + gen_statem:start(STM, ?MODULE, [], []), + {error,{already_started,Pid}} = + gen_statem:start(STM, ?MODULE, [], []), + + ok = do_func_test(Pid), + ok = do_sync_func_test(Pid), + ok = do_func_test(Name), + ok = do_sync_func_test(Name), + stop_it(Pid), + + %%process_flag(trap_exit, OldFl), + ok = verify_empty_msgq(). + +%% local register linked +start9(Config) when is_list(Config) -> + %%OldFl = process_flag(trap_exit, true), + Name = my_stm, + STM = {local,Name}, + + {ok,Pid} = + gen_statem:start_link(STM, ?MODULE, [], []), + {error,{already_started,Pid}} = + gen_statem:start(STM, ?MODULE, [], []), + + ok = do_func_test(Pid), + ok = do_sync_func_test(Pid), + ok = do_func_test(Name), + ok = do_sync_func_test(Name), + stop_it(Pid), + + %%process_flag(trap_exit, OldFl), + ok = verify_empty_msgq(). + +%% global register +start10(Config) when is_list(Config) -> + STM = {global,my_stm}, + + {ok,Pid} = + gen_statem:start(STM, ?MODULE, [], []), + {error,{already_started,Pid}} = + gen_statem:start(STM, ?MODULE, [], []), + {error,{already_started,Pid}} = + gen_statem:start_link(STM, ?MODULE, [], []), + + ok = do_func_test(Pid), + ok = do_sync_func_test(Pid), + ok = do_func_test(STM), + ok = do_sync_func_test(STM), + stop_it(STM), + + ok = verify_empty_msgq(). + +%% Stop registered processes +start11(Config) when is_list(Config) -> + Name = my_stm, + LocalSTM = {local,Name}, + GlobalSTM = {global,Name}, + + {ok,Pid} = + gen_statem:start_link(LocalSTM, ?MODULE, [], []), + stop_it(Pid), + + {ok,_Pid1} = + gen_statem:start_link(LocalSTM, ?MODULE, [], []), + stop_it(Name), + + {ok,Pid2} = + gen_statem:start(GlobalSTM, ?MODULE, [], []), + stop_it(Pid2), + receive after 1 -> true end, + Result = + gen_statem:start(GlobalSTM, ?MODULE, [], []), + io:format("Result = ~p~n",[Result]), + {ok,_Pid3} = Result, + stop_it(GlobalSTM), + + ok = verify_empty_msgq(). + +%% Via register linked +start12(Config) when is_list(Config) -> + dummy_via:reset(), + VIA = {via,dummy_via,my_stm}, + + {ok,Pid} = + gen_statem:start_link(VIA, ?MODULE, [], []), + {error,{already_started,Pid}} = + gen_statem:start_link(VIA, ?MODULE, [], []), + {error,{already_started,Pid}} = + gen_statem:start(VIA, ?MODULE, [], []), + + ok = do_func_test(Pid), + ok = do_sync_func_test(Pid), + ok = do_func_test(VIA), + ok = do_sync_func_test(VIA), + stop_it(VIA), + + ok = verify_empty_msgq(). + + +%% Anonymous, reason 'normal' +stop1(_Config) -> + {ok,Pid} = gen_statem:start(?MODULE, [], []), + ok = gen_statem:stop(Pid), + false = erlang:is_process_alive(Pid), + noproc = + ?EXPECT_FAILURE(gen_statem:stop(Pid), Reason). + +%% Anonymous, other reason +stop2(_Config) -> + {ok,Pid} = gen_statem:start(?MODULE, [], []), + ok = gen_statem:stop(Pid, other_reason, infinity), + false = erlang:is_process_alive(Pid), + ok. + +%% Anonymous, invalid timeout +stop3(_Config) -> + {ok,Pid} = gen_statem:start(?MODULE, [], []), + _ = + ?EXPECT_FAILURE( + gen_statem:stop(Pid, other_reason, invalid_timeout), + Reason), + true = erlang:is_process_alive(Pid), + ok = gen_statem:stop(Pid), + false = erlang:is_process_alive(Pid), + ok. + +%% Registered name +stop4(_Config) -> + {ok,Pid} = gen_statem:start({local,to_stop},?MODULE, [], []), + ok = gen_statem:stop(to_stop), + false = erlang:is_process_alive(Pid), + noproc = + ?EXPECT_FAILURE(gen_statem:stop(to_stop), Reason), + ok. + +%% Registered name and local node +stop5(_Config) -> + Name = to_stop, + {ok,Pid} = gen_statem:start({local,Name},?MODULE, [], []), + ok = gen_statem:stop({Name,node()}), + false = erlang:is_process_alive(Pid), + noproc = + ?EXPECT_FAILURE(gen_statem:stop({Name,node()}), Reason), + ok. + +%% Globally registered name +stop6(_Config) -> + STM = {global,to_stop}, + {ok,Pid} = gen_statem:start(STM, ?MODULE, [], []), + ok = gen_statem:stop(STM), + false = erlang:is_process_alive(Pid), + noproc = + ?EXPECT_FAILURE(gen_statem:stop(STM), Reason), + ok. + +%% 'via' registered name +stop7(_Config) -> + VIA = {via,dummy_via,to_stop}, + dummy_via:reset(), + {ok,Pid} = gen_statem:start(VIA, + ?MODULE, [], []), + ok = gen_statem:stop(VIA), + false = erlang:is_process_alive(Pid), + noproc = + ?EXPECT_FAILURE(gen_statem:stop(VIA), Reason), + ok. + +%% Anonymous on remote node +stop8(_Config) -> + {ok,Node} = ?t:start_node(gen_statem_stop8, slave, []), + Dir = filename:dirname(code:which(?MODULE)), + rpc:call(Node, code, add_path, [Dir]), + {ok,Pid} = rpc:call(Node, gen_statem,start, [?MODULE,[],[]]), + ok = gen_statem:stop(Pid), + false = rpc:call(Node, erlang, is_process_alive, [Pid]), + noproc = + ?EXPECT_FAILURE(gen_statem:stop(Pid), Reason1), + true = ?t:stop_node(Node), + {nodedown,Node} = + ?EXPECT_FAILURE(gen_statem:stop(Pid), Reason2), + ok. + +%% Registered name on remote node +stop9(_Config) -> + Name = to_stop, + LocalSTM = {local,Name}, + {ok,Node} = ?t:start_node(gen_statem__stop9, slave, []), + STM = {Name,Node}, + Dir = filename:dirname(code:which(?MODULE)), + rpc:call(Node, code, add_path, [Dir]), + {ok,Pid} = rpc:call(Node, gen_statem, start, [LocalSTM,?MODULE,[],[]]), + ok = gen_statem:stop(STM), + undefined = rpc:call(Node,erlang,whereis,[Name]), + false = rpc:call(Node,erlang,is_process_alive,[Pid]), + noproc = + ?EXPECT_FAILURE(gen_statem:stop(STM), Reason1), + true = ?t:stop_node(Node), + {nodedown,Node} = + ?EXPECT_FAILURE(gen_statem:stop(STM), Reason2), + ok. + +%% Globally registered name on remote node +stop10(_Config) -> + STM = {global,to_stop}, + {ok,Node} = ?t:start_node(gen_statem_stop10, slave, []), + Dir = filename:dirname(code:which(?MODULE)), + rpc:call(Node,code,add_path,[Dir]), + {ok,Pid} = rpc:call(Node, gen_statem, start, [STM,?MODULE,[],[]]), + global:sync(), + ok = gen_statem:stop(STM), + false = rpc:call(Node, erlang, is_process_alive, [Pid]), + noproc = + ?EXPECT_FAILURE(gen_statem:stop(STM), Reason1), + true = ?t:stop_node(Node), + noproc = + ?EXPECT_FAILURE(gen_statem:stop(STM), Reason2), + ok. + +%% Check that time outs in calls work +abnormal1(Config) when is_list(Config) -> + Name = abnormal1, + LocalSTM = {local,Name}, + + {ok, _Pid} = gen_statem:start(LocalSTM, ?MODULE, [], []), + + %% timeout call. + delayed = gen_statem:call(Name, {delayed_answer,1}, 100), + {timeout,_} = + ?EXPECT_FAILURE( + gen_statem:call(Name, {delayed_answer,1000}, 10), + Reason), + ok = verify_empty_msgq(). + +%% Check that bad return values makes the stm crash. Note that we must +%% trap exit since we must link to get the real bad_return_ error +abnormal2(Config) when is_list(Config) -> + OldFl = process_flag(trap_exit, true), + {ok,Pid} = gen_statem:start_link(?MODULE, [], []), + + %% bad return value in the gen_statem loop + {{bad_return_value,badreturn},_} = + ?EXPECT_FAILURE(gen_statem:call(Pid, badreturn), Reason), + receive + {'EXIT',Pid,{bad_return_value,badreturn}} -> ok + after 5000 -> + ?t:fail(gen_statem_did_not_die) + end, + + process_flag(trap_exit, OldFl), + ok = verify_empty_msgq(). + +shutdown(Config) when is_list(Config) -> + process_flag(trap_exit, true), + + {ok,Pid0} = gen_statem:start_link(?MODULE, [], []), + ok = do_func_test(Pid0), + ok = do_sync_func_test(Pid0), + stopped = gen_statem:call(Pid0, {stop,{shutdown,reason}}), + receive {'EXIT',Pid0,{shutdown,reason}} -> ok end, + process_flag(trap_exit, false), + + {noproc,_} = + ?EXPECT_FAILURE(gen_statem:call(Pid0, hej), Reason), + + receive + Any -> + io:format("Unexpected: ~p", [Any]), + ?t:fail() + after 500 -> + ok + end. + + + +sys1(Config) when is_list(Config) -> + {ok,Pid} = gen_statem:start(?MODULE, [], []), + {status, Pid, {module,gen_statem}, _} = sys:get_status(Pid), + sys:suspend(Pid), + Parent = self(), + Tag = make_ref(), + Caller = + spawn( + fun () -> + Parent ! {Tag,gen_statem:call(Pid, hej)} + end), + receive + {Tag,_} -> + ?t:fail() + after 3000 -> + exit(Caller, ok) + end, + + %% {timeout,_} = + %% ?EXPECT_FAILURE(gen_statem:call(Pid, hej), Reason), + sys:resume(Pid), + stop_it(Pid). + +call_format_status(Config) when is_list(Config) -> + {ok,Pid} = gen_statem:start(?MODULE, [], []), + Status = sys:get_status(Pid), + {status,Pid,_Mod,[_PDict,running,_,_, Data]} = Status, + [format_status_called|_] = lists:reverse(Data), + stop_it(Pid), + + %% check that format_status can handle a name being an atom (pid is + %% already checked by the previous test) + {ok, Pid2} = gen_statem:start({local, gstm}, ?MODULE, [], []), + Status2 = sys:get_status(gstm), + {status,Pid2,_Mod,[_PDict2,running,_,_,Data2]} = Status2, + [format_status_called|_] = lists:reverse(Data2), + stop_it(Pid2), + + %% check that format_status can handle a name being a term other than a + %% pid or atom + GlobalName1 = {global,"CallFormatStatus"}, + {ok,Pid3} = gen_statem:start(GlobalName1, ?MODULE, [], []), + Status3 = sys:get_status(GlobalName1), + {status,Pid3,_Mod,[_PDict3,running,_,_,Data3]} = Status3, + [format_status_called|_] = lists:reverse(Data3), + stop_it(Pid3), + GlobalName2 = {global,{name, "term"}}, + {ok,Pid4} = gen_statem:start(GlobalName2, ?MODULE, [], []), + Status4 = sys:get_status(GlobalName2), + {status,Pid4,_Mod,[_PDict4,running,_,_, Data4]} = Status4, + [format_status_called|_] = lists:reverse(Data4), + stop_it(Pid4), + + %% check that format_status can handle a name being a term other than a + %% pid or atom + dummy_via:reset(), + ViaName1 = {via,dummy_via,"CallFormatStatus"}, + {ok,Pid5} = gen_statem:start(ViaName1, ?MODULE, [], []), + Status5 = sys:get_status(ViaName1), + {status,Pid5,_Mod, [_PDict5,running,_,_, Data5]} = Status5, + [format_status_called|_] = lists:reverse(Data5), + stop_it(Pid5), + ViaName2 = {via,dummy_via,{name,"term"}}, + {ok, Pid6} = gen_statem:start(ViaName2, ?MODULE, [], []), + Status6 = sys:get_status(ViaName2), + {status,Pid6,_Mod,[_PDict6,running,_,_,Data6]} = Status6, + [format_status_called|_] = lists:reverse(Data6), + stop_it(Pid6). + + + +error_format_status(Config) when is_list(Config) -> + error_logger_forwarder:register(), + OldFl = process_flag(trap_exit, true), + StateData = "called format_status", + {ok,Pid} = gen_statem:start(?MODULE, {state_data,StateData}, []), + %% bad return value in the gen_statem loop + {{bad_return_value,badreturn},_} = + ?EXPECT_FAILURE(gen_statem:call(Pid, badreturn), Reason), + receive + {error,_, + {Pid, + "** State machine"++_, + [Pid,{{call,_},badreturn}, + {formatted,idle,StateData}, + exit,{bad_return_value,badreturn}|_]}} -> + ok; + Other when is_tuple(Other), element(1, Other) =:= error -> + error_logger_forwarder:unregister(), + ?t:fail({unexpected,Other}) + after 1000 -> + error_logger_forwarder:unregister(), + ?t:fail() + end, + process_flag(trap_exit, OldFl), + error_logger_forwarder:unregister(), + receive + %% Comes with SASL + {error_report,_,{Pid,crash_report,_}} -> + ok + after 500 -> + ok + end, + ok = verify_empty_msgq(). + +terminate_crash_format(Config) when is_list(Config) -> + error_logger_forwarder:register(), + OldFl = process_flag(trap_exit, true), + StateData = crash_terminate, + {ok,Pid} = gen_statem:start(?MODULE, {state_data,StateData}, []), + stop_it(Pid), + Self = self(), + receive + {error,_GroupLeader, + {Pid, + "** State machine"++_, + [Pid, + {{call,{Self,_}},stop}, + {formatted,idle,StateData}, + exit,{crash,terminate}|_]}} -> + ok; + Other when is_tuple(Other), element(1, Other) =:= error -> + error_logger_forwarder:unregister(), + ?t:fail({unexpected,Other}) + after 1000 -> + error_logger_forwarder:unregister(), + ?t:fail() + end, + process_flag(trap_exit, OldFl), + error_logger_forwarder:unregister(), + receive + %% Comes with SASL + {error_report,_,{Pid,crash_report,_}} -> + ok + after 500 -> + ok + end, + ok = verify_empty_msgq(). + + +get_state(Config) when is_list(Config) -> + State = self(), + {ok,Pid} = gen_statem:start(?MODULE, {state_data,State}, []), + {idle,State} = sys:get_state(Pid), + {idle,State} = sys:get_state(Pid, 5000), + stop_it(Pid), + + %% check that get_state can handle a name being an atom (pid is + %% already checked by the previous test) + {ok,Pid2} = + gen_statem:start({local,gstm}, ?MODULE, {state_data,State}, []), + {idle,State} = sys:get_state(gstm), + {idle,State} = sys:get_state(gstm, 5000), + stop_it(Pid2), + + %% check that get_state works when pid is sys suspended + {ok,Pid3} = gen_statem:start(?MODULE, {state_data,State}, []), + {idle,State} = sys:get_state(Pid3), + ok = sys:suspend(Pid3), + {idle,State} = sys:get_state(Pid3, 5000), + ok = sys:resume(Pid3), + stop_it(Pid3), + ok = verify_empty_msgq(). + +replace_state(Config) when is_list(Config) -> + State = self(), + {ok, Pid} = gen_statem:start(?MODULE, {state_data,State}, []), + {idle,State} = sys:get_state(Pid), + NState1 = "replaced", + Replace1 = fun({StateName, _}) -> {StateName,NState1} end, + {idle,NState1} = sys:replace_state(Pid, Replace1), + {idle,NState1} = sys:get_state(Pid), + NState2 = "replaced again", + Replace2 = fun({idle, _}) -> {state0,NState2} end, + {state0,NState2} = sys:replace_state(Pid, Replace2, 5000), + {state0,NState2} = sys:get_state(Pid), + %% verify no change in state if replace function crashes + Replace3 = fun(_) -> error(fail) end, + {callback_failed, + {gen_statem,system_replace_state},{error,fail}} = + ?EXPECT_FAILURE(sys:replace_state(Pid, Replace3), Reason), + {state0, NState2} = sys:get_state(Pid), + %% verify state replaced if process sys suspended + ok = sys:suspend(Pid), + Suffix2 = " and again", + NState3 = NState2 ++ Suffix2, + Replace4 = fun({StateName, _}) -> {StateName, NState3} end, + {state0,NState3} = sys:replace_state(Pid, Replace4), + ok = sys:resume(Pid), + {state0,NState3} = sys:get_state(Pid, 5000), + stop_it(Pid), + ok = verify_empty_msgq(). + +%% Hibernation +hibernate(Config) when is_list(Config) -> + OldFl = process_flag(trap_exit, true), + + {ok,Pid0} = gen_statem:start_link(?MODULE, hiber_now, []), + is_in_erlang_hibernate(Pid0), + stop_it(Pid0), + receive + {'EXIT',Pid0,normal} -> ok + after 5000 -> + ?t:fail(gen_statem_did_not_die) + end, + + {ok,Pid} = gen_statem:start_link(?MODULE, hiber, []), + true = ({current_function,{erlang,hibernate,3}} =/= + erlang:process_info(Pid,current_function)), + hibernating = gen_statem:call(Pid, hibernate_sync), + is_in_erlang_hibernate(Pid), + good_morning = gen_statem:call(Pid, wakeup_sync), + is_not_in_erlang_hibernate(Pid), + hibernating = gen_statem:call(Pid, hibernate_sync), + is_in_erlang_hibernate(Pid), + please_just_five_more = gen_statem:call(Pid, snooze_sync), + is_in_erlang_hibernate(Pid), + good_morning = gen_statem:call(Pid, wakeup_sync), + is_not_in_erlang_hibernate(Pid), + ok = gen_statem:cast(Pid, hibernate_async), + is_in_erlang_hibernate(Pid), + ok = gen_statem:cast(Pid, wakeup_async), + is_not_in_erlang_hibernate(Pid), + ok = gen_statem:cast(Pid, hibernate_async), + is_in_erlang_hibernate(Pid), + ok = gen_statem:cast(Pid, snooze_async), + is_in_erlang_hibernate(Pid), + ok = gen_statem:cast(Pid, wakeup_async), + is_not_in_erlang_hibernate(Pid), + + Pid ! hibernate_later, + true = + ({current_function,{erlang,hibernate,3}} =/= + erlang:process_info(Pid, current_function)), + is_in_erlang_hibernate(Pid), + + 'alive!' = gen_statem:call(Pid, 'alive?'), + true = + ({current_function,{erlang,hibernate,3}} =/= + erlang:process_info(Pid, current_function)), + Pid ! hibernate_now, + is_in_erlang_hibernate(Pid), + + 'alive!' = gen_statem:call(Pid, 'alive?'), + true = + ({current_function,{erlang,hibernate,3}} =/= + erlang:process_info(Pid, current_function)), + + hibernating = gen_statem:call(Pid, hibernate_sync), + is_in_erlang_hibernate(Pid), + good_morning = gen_statem:call(Pid, wakeup_sync), + is_not_in_erlang_hibernate(Pid), + hibernating = gen_statem:call(Pid, hibernate_sync), + is_in_erlang_hibernate(Pid), + please_just_five_more = gen_statem:call(Pid, snooze_sync), + is_in_erlang_hibernate(Pid), + good_morning = gen_statem:call(Pid, wakeup_sync), + is_not_in_erlang_hibernate(Pid), + ok = gen_statem:cast(Pid, hibernate_async), + is_in_erlang_hibernate(Pid), + ok = gen_statem:cast(Pid, wakeup_async), + is_not_in_erlang_hibernate(Pid), + ok = gen_statem:cast(Pid, hibernate_async), + is_in_erlang_hibernate(Pid), + ok = gen_statem:cast(Pid, snooze_async), + is_in_erlang_hibernate(Pid), + ok = gen_statem:cast(Pid, wakeup_async), + is_not_in_erlang_hibernate(Pid), + + hibernating = gen_statem:call(Pid, hibernate_sync), + is_in_erlang_hibernate(Pid), + sys:suspend(Pid), + is_in_erlang_hibernate(Pid), + sys:resume(Pid), + is_in_erlang_hibernate(Pid), + receive after 1000 -> ok end, + is_in_erlang_hibernate(Pid), + + good_morning = gen_statem:call(Pid, wakeup_sync), + is_not_in_erlang_hibernate(Pid), + stop_it(Pid), + process_flag(trap_exit, OldFl), + receive + {'EXIT',Pid,normal} -> ok + after 5000 -> + ?t:fail(gen_statem_did_not_die) + end, + ok = verify_empty_msgq(). + +is_in_erlang_hibernate(Pid) -> + receive after 1 -> ok end, + is_in_erlang_hibernate_1(200, Pid). + +is_in_erlang_hibernate_1(0, Pid) -> + io:format("~p\n", [erlang:process_info(Pid, current_function)]), + ?t:fail(not_in_erlang_hibernate_3); +is_in_erlang_hibernate_1(N, Pid) -> + {current_function,MFA} = erlang:process_info(Pid, current_function), + case MFA of + {erlang,hibernate,3} -> + ok; + _ -> + receive after 10 -> ok end, + is_in_erlang_hibernate_1(N-1, Pid) + end. + +is_not_in_erlang_hibernate(Pid) -> + receive after 1 -> ok end, + is_not_in_erlang_hibernate_1(200, Pid). + +is_not_in_erlang_hibernate_1(0, Pid) -> + io:format("~p\n", [erlang:process_info(Pid, current_function)]), + ?t:fail(not_in_erlang_hibernate_3); +is_not_in_erlang_hibernate_1(N, Pid) -> + {current_function,MFA} = erlang:process_info(Pid, current_function), + case MFA of + {erlang,hibernate,3} -> + receive after 10 -> ok end, + is_not_in_erlang_hibernate_1(N-1, Pid); + _ -> + ok + end. + +%%sys1(suite) -> []; +%%sys1(_) -> + +enter_loop(Config) when is_list(Config) -> + OldFlag = process_flag(trap_exit, true), + + dummy_via:reset(), + + %% Locally registered process + {local,Name} + {ok,Pid1a} = + proc_lib:start_link(?MODULE, enter_loop, [local,local]), + yes = gen_statem:call(Pid1a, 'alive?'), + stopped = gen_statem:call(Pid1a, stop), + receive + {'EXIT',Pid1a,normal} -> + ok + after 5000 -> + ?t:fail(gen_statem_did_not_die) + end, + + %% Unregistered process + {local,Name} + {ok,Pid1b} = + proc_lib:start_link(?MODULE, enter_loop, [anon,local]), + receive + {'EXIT',Pid1b,process_not_registered} -> + ok + after 5000 -> + ?t:fail(gen_statem_did_not_die) + end, + + %% Globally registered process + {global,Name} + {ok,Pid2a} = + proc_lib:start_link(?MODULE, enter_loop, [global,global]), + yes = gen_statem:call(Pid2a, 'alive?'), + stopped = gen_statem:call(Pid2a, stop), + receive + {'EXIT',Pid2a,normal} -> + ok + after 5000 -> + ?t:fail(gen_statem_did_not_die) + end, + + %% Unregistered process + {global,Name} + {ok,Pid2b} = + proc_lib:start_link(?MODULE, enter_loop, [anon,global]), + receive + {'EXIT',Pid2b,process_not_registered_globally} -> + ok + after 5000 -> + ?t:fail(gen_statem_did_not_die) + end, + + %% Unregistered process + no name + {ok,Pid3} = + proc_lib:start_link(?MODULE, enter_loop, [anon,anon]), + yes = gen_statem:call(Pid3, 'alive?'), + stopped = gen_statem:call(Pid3, stop), + receive + {'EXIT',Pid3,normal} -> + ok + after 5000 -> + ?t:fail(gen_statem_did_not_die) + end, + + %% Process not started using proc_lib + Pid4 = + spawn_link(gen_statem, enter_loop, [?MODULE,[],state0,[]]), + receive + {'EXIT',Pid4,process_was_not_started_by_proc_lib} -> + ok + after 5000 -> + ?t:fail(gen_statem_did_not_die) + end, + + %% Make sure I am the parent, ie that ordering a shutdown will + %% result in the process terminating with Reason==shutdown + {ok,Pid5} = + proc_lib:start_link(?MODULE, enter_loop, [anon,anon]), + yes = gen_statem:call(Pid5, 'alive?'), + exit(Pid5, shutdown), + receive + {'EXIT',Pid5,shutdown} -> + ok + after 5000 -> + ?t:fail(gen_statem_did_not_die) + end, + + %% Make sure gen_statem:enter_loop does not accept {local,Name} + %% when it's another process than the calling one which is + %% registered under that name + register(armitage, self()), + {ok,Pid6a} = + proc_lib:start_link(?MODULE, enter_loop, [anon,local]), + receive + {'EXIT',Pid6a,process_not_registered} -> + ok + after 1000 -> + ?t:fail(gen_statem_started) + end, + unregister(armitage), + + %% Make sure gen_statem:enter_loop does not accept {global,Name} + %% when it's another process than the calling one which is + %% registered under that name + global:register_name(armitage, self()), + {ok,Pid6b} = + proc_lib:start_link(?MODULE, enter_loop, [anon,global]), + receive + {'EXIT',Pid6b,process_not_registered_globally} -> + ok + after 1000 -> + ?t:fail(gen_statem_started) + end, + global:unregister_name(armitage), + + dummy_via:register_name(armitage, self()), + {ok,Pid6c} = + proc_lib:start_link(?MODULE, enter_loop, [anon,via]), + receive + {'EXIT',Pid6c,{process_not_registered_via,dummy_via}} -> + ok + after 1000 -> + ?t:fail( + {gen_statem_started, + process_info(self(), messages)}) + end, + dummy_via:unregister_name(armitage), + + process_flag(trap_exit, OldFlag), + ok = verify_empty_msgq(). + +enter_loop(Reg1, Reg2) -> + process_flag(trap_exit, true), + case Reg1 of + local -> register(armitage, self()); + global -> global:register_name(armitage, self()); + via -> dummy_via:register_name(armitage, self()); + anon -> ignore + end, + proc_lib:init_ack({ok, self()}), + case Reg2 of + local -> + gen_statem:enter_loop(?MODULE, [], state0, [], {local,armitage}); + global -> + gen_statem:enter_loop(?MODULE, [], state0, [], {global,armitage}); + via -> + gen_statem:enter_loop(?MODULE, [], state0, [], + {via, dummy_via, armitage}); + anon -> + gen_statem:enter_loop(?MODULE, [], state0, []) + end. + +%% +%% Functionality check +%% + +wfor(Msg) -> + receive + Msg -> ok + after 5000 -> + error(timeout) + end. + + +stop_it(STM) -> + stopped = gen_statem:call(STM, stop), + check_stopped(STM). + + +check_stopped(STM) -> + Call = there_you_are, + {_,{gen_statem,call,[_,Call,infinity]}} = + ?EXPECT_FAILURE(gen_statem:call(STM, Call), Reason), + ok. + + +do_func_test(STM) -> + ok = gen_statem:cast(STM, {'alive?',self()}), + wfor(yes), + ok = do_connect(STM), + ok = gen_statem:cast(STM, {'alive?',self()}), + wfor(yes), + ?t:do_times(3, ?MODULE, do_msg, [STM]), + ok = gen_statem:cast(STM, {'alive?',self()}), + wfor(yes), + ok = do_disconnect(STM), + ok = gen_statem:cast(STM, {'alive?',self()}), + wfor(yes), + ok. + + +do_connect(STM) -> + check_state(STM, idle), + gen_statem:cast(STM, {connect,self()}), + wfor(accept), + check_state(STM, wfor_conf), + gen_statem:cast(STM, confirm), + check_state(STM, connected), + ok. + +do_msg(STM) -> + check_state(STM, connected), + R = make_ref(), + ok = gen_statem:cast(STM, {msg,self(),R}), + wfor({ack,R}). + + +do_disconnect(STM) -> + ok = gen_statem:cast(STM, disconnect), + check_state(STM, idle). + +check_state(STM, State) -> + case gen_statem:call(STM, get) of + {state, State, _} -> ok + end. + +do_sync_func_test(STM) -> + yes = gen_statem:call(STM, 'alive?'), + ok = do_sync_connect(STM), + yes = gen_statem:call(STM, 'alive?'), + ?t:do_times(3, ?MODULE, do_sync_msg, [STM]), + yes = gen_statem:call(STM, 'alive?'), + ok = do_sync_disconnect(STM), + yes = gen_statem:call(STM, 'alive?'), + check_state(STM, idle), + ok = gen_statem:call(STM, {timeout,200}), + yes = gen_statem:call(STM, 'alive?'), + check_state(STM, idle), + ok. + + +do_sync_connect(STM) -> + check_state(STM, idle), + accept = gen_statem:call(STM, connect), + check_state(STM, wfor_conf), + yes = gen_statem:call(STM, confirm), + check_state(STM, connected), + ok. + +do_sync_msg(STM) -> + check_state(STM, connected), + R = make_ref(), + {ack,R} = gen_statem:call(STM, {msg,R}), + ok. + +do_sync_disconnect(STM) -> + yes = gen_statem:call(STM, disconnect), + check_state(STM, idle). + + +verify_empty_msgq() -> + receive after 500 -> ok end, + [] = ?t:messages_get(), + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% The State Machine +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +init(ignore) -> + ignore; +init(stop) -> + {stop,stopped}; +init(stop_shutdown) -> + {stop,shutdown}; +init(sleep) -> + ?t:sleep(1000), + {ok,idle,data}; +init(hiber) -> + {ok,hiber_idle,[]}; +init(hiber_now) -> + {ok,hiber_idle,[],[hibernate]}; +init({state_data, StateData}) -> + {ok,idle,StateData}; +init(_) -> + {ok,idle,state_data}. + +terminate(_, _State, crash_terminate) -> + exit({crash,terminate}); +terminate({From,stopped}, State, _Data) -> + From ! {self(),{stopped,State}}, + ok; +terminate(_Reason, _State, _Data) -> + ok. + + +%% State functions + +idle(cast, {connect,Pid}, _, _, Data) -> + Pid ! accept, + {wfor_conf,Data}; +idle({call,From}, connect, _, _, Data) -> + {wfor_conf,Data,[{reply,From,accept}]}; +idle(cast, badreturn, _, _, _Data) -> + badreturn; +idle({call,_From}, badreturn, _, _, _Data) -> + badreturn; +idle({call,From}, {delayed_answer,T}, _, _, _Data) -> + receive + after T -> + [{reply,From,delayed}] + end; +idle({call,From}, {timeout,Time}, _, State, _Data) -> + {timeout, {From,Time}, [{timeout,Time,State}]}; +idle(Type, Content, PrevState, State, Data) -> + case handle_common_events(Type, Content, PrevState, State, Data) of + [] -> + case Type of + {call,From} -> + {State,Data,[{reply,From,'eh?'}]}; + _ -> + {State,Data, + [{stop,{unexpected,State,PrevState,Type,Content}}]} + end; + Result -> + Result + end. + +timeout(timeout, idle, idle, timeout, {From,Time}) -> + TRef2 = erlang:start_timer(Time, self(), ok), + TRefC1 = erlang:start_timer(Time, self(), cancel1), + TRefC2 = erlang:start_timer(Time, self(), cancel2), + {timeout2, + {From, Time, TRef2}, + [{cancel_timer, TRefC1}, + {insert_event,internal,{cancel_timer,TRefC2}}]}; +timeout(_, _, _, _, Data) -> + {Data}. + +timeout2( + internal, {cancel_timer,TRefC2}, timeout, _, {From,Time,TRef2}) -> + Time4 = Time * 4, + receive after Time4 -> ok end, + {timeout3,{From,TRef2},[{cancel_timer,TRefC2}]}; +timeout2(_, _, _, _, Data) -> + {Data}. + +timeout3(info, {timeout,TRef2,Result}, _, _, {From,TRef2}) -> + {idle,state,[{reply,From,Result}]}; +timeout3(_, _, _, _, Data) -> + {Data}. + +wfor_conf({call,From}, confirm, _, _, Data) -> + {connected,Data,[{reply,From,yes}]}; +wfor_conf(cast, confirm, _, _, Data) -> + {connected,Data}; +wfor_conf(Type, Content, PrevState, State, Data) -> + case handle_common_events(Type, Content, PrevState, State, Data) of + [] -> + case Type of + {call,From} -> + {idle,Data,[{reply,From,'eh?'}]}; + _ -> + {Data} + end; + Result -> + Result + end. + +connected({call,From}, {msg,Ref}, _, State, Data) -> + {State,Data,[{reply,From,{ack,Ref}}]}; +connected(cast, {msg,From,Ref}, _, _, _Data) -> + From ! {ack,Ref}, + {}; +connected({call,From}, disconnect, _, _, Data) -> + {idle,Data,[{reply,From,yes}]}; +connected(cast, disconnect, _, _, Data) -> + {idle,Data}; +connected(Type, Content, PrevState, State, Data) -> + case handle_common_events(Type, Content, PrevState, State, Data) of + [] -> + case Type of + {call,From} -> + [{reply,From,'eh?'}]; + _ -> + {Data} + end; + Result -> + Result + end. + +state0({call,From}, stop, _, State, Data) -> + {State,Data, + [{reply,From,stopped}, + {stop,normal}]}; +state0(Type, Content, PrevState, State, Data) -> + case handle_common_events(Type, Content, PrevState, State, Data) of + [] -> + {Data}; + Result -> + Result + end. + +hiber_idle({call,From}, 'alive?', _, _, _) -> + [{reply,From,'alive!'}]; +hiber_idle({call,From}, hibernate_sync, _, _, Data) -> + {hiber_wakeup,Data, + [{reply,From,hibernating}, + hibernate]}; +hiber_idle(info, hibernate_later, _, State, _) -> + Tref = erlang:start_timer(1000, self(), hibernate), + {State,Tref}; +hiber_idle(info, hibernate_now, _, State, Data) -> + {State,Data,[hibernate]}; +hiber_idle(info, {timeout,Tref,hibernate}, _, State, Tref) -> + {State,[], + [hibernate]}; +hiber_idle(cast, hibernate_async, _, _, Data) -> + {hiber_wakeup,Data, + [hibernate]}; +hiber_idle(Type, Content, PrevState, State, Data) -> + case handle_common_events(Type, Content, PrevState, State, Data) of + [] -> + {Data}; + Result -> + Result + end. + +hiber_wakeup({call,From}, wakeup_sync, _, _, Data) -> + {hiber_idle,Data,[{reply,From,good_morning}]}; +hiber_wakeup({call,From}, snooze_sync, _, State, Data) -> + {State,Data, + [{reply,From,please_just_five_more}, + hibernate]}; +hiber_wakeup(cast, wakeup_async, _, _, Data) -> + {hiber_idle,Data}; +hiber_wakeup(cast, snooze_async, _, _, _Data) -> + [hibernate]; +hiber_wakeup(Type, Content, PrevState, State, Data) -> + case handle_common_events(Type, Content, PrevState, State, Data) of + [] -> + {Data}; + Result -> + Result + end. + + +handle_common_events({call,From}, get, _, State, Data) -> + [{reply,From,{state,State,Data}}]; +handle_common_events(cast, {get,Pid}, _, State, Data) -> + Pid ! {state,State,Data}, + {}; +handle_common_events({call,From}, stop, _, _, _) -> + [{reply,From,stopped}, + {stop,normal}]; +handle_common_events(cast, stop, _, State, Data) -> + {State,Data, + [{stop,normal}]}; +handle_common_events({call,From}, {stop,Reason}, _, State, Data) -> + {State,Data, + [{reply,From,stopped}, + {stop,Reason}]}; +handle_common_events(cast, {stop,Reason}, _, _, _) -> + [{stop,Reason}]; +handle_common_events({call,From}, 'alive?', _, State, Data) -> + {State,Data, + [{reply,From,yes}]}; +handle_common_events(cast, {'alive?',Pid}, _, State, Data) -> + Pid ! yes, + {State,Data}; +handle_common_events(_, _, _, _, _) -> + []. + +code_change(_OldVsn, State, StateData, _Extra) -> + {ok,State,StateData}. + +format_status(terminate, [_Pdict,State,StateData]) -> + {formatted,State,StateData}; +format_status(normal, [_Pdict,_State,_StateData]) -> + [format_status_called]. -- cgit v1.2.3 From a07f6e35c3adb6e1536a63cc8137f48f64e2f6f7 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 9 Feb 2016 11:45:55 +0100 Subject: Correct grammar errors found by Magnus Henoch --- lib/stdlib/doc/src/gen_statem.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 885021f61c..83d1f1e9e6 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -96,10 +96,10 @@ erlang:'!' -----> Module:State/5 will be called. If you use handle_event as a state and later - decides to use non-atom states you will then have to + decide to use non-atom states you will then have to rewrite your code to stop using that state.

-

When the using an atom-only +

When using an atom-only State it becomes fairly obvious in the implementation code which events are handled in which state -- cgit v1.2.3 From 3815de0c7337058991066454c246587c0dbaa664 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 16 Feb 2016 10:51:39 +0100 Subject: Change to {next_state,...} and {stop,...} return format --- lib/stdlib/doc/src/gen_statem.xml | 75 +++++++++++------- lib/stdlib/src/gen_statem.erl | 103 +++++++++++++++--------- lib/stdlib/test/gen_statem_SUITE.erl | 148 ++++++++++++++++++----------------- 3 files changed, 191 insertions(+), 135 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 83d1f1e9e6..f7ce0f61ae 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -397,23 +397,6 @@ erlang:'!' -----> Module:State/5 - - {reply,Client,Reply} - - Reply to a client that called - call/2. - Client must be the term from the - - {call,Client} - argument to the - state function. - - {stop,Reason} - The gen_statem will call - - Module:terminate/3 - with Reason and terminate. - {insert_event,EventType,EventContent} @@ -471,6 +454,24 @@ erlang:'!' -----> Module:State/5 + + + + + + {reply,Client,Reply} + + Reply to a client that called + call/2. + Client must be the term from the + + {call,Client} + argument to the + state function. + + + + @@ -867,22 +868,39 @@ erlang:'!' -----> Module:State/5 event_type() EventContent = term() - Result = {NewState,NewStateData,StateOps} -   | {NewState,NewStateData} -   The same as {NewState,NewStateData,[]} -   | {NewStateData} -   The same as {State,NewStateData,[retry]} -   | {} -   The same as {State,StateData,[]} -   | StateOps -   The same as {State,StateData,StateOps} - + Result = {stop,Reason,NewStateData} +   The same as {stop,Reason,[],NewStateData} +   | {stop,Reason,Reply,NewStateData} +   The same as + {stop,Reason,[Reply],NewStateData} + +   | {stop,Reason,Replies,NewStateData} +   The gen_statem will first send all + Replies and then call + + Module:terminate/3 + with Reason and terminate. + +   | {next_state,NewState,NewStateData} +   The same as + {next_state,NewState,NewStateData,NewStateData,[]} + +   | {next_state,NewState,NewStateData,StateOps} +   The gen_statem will do a state transition to + NewState (which may be the same as State) + and execute all StateOps + + Reason = term() PrevState = State = NewState = state() StateData = NewStateData = state_data() + Reply = + reply_operation() + + Replies = [Reply] StateOps = [state_op()] @@ -898,7 +916,8 @@ erlang:'!' -----> Module:State/5 from this or from any other state function by returning with {reply,Client,Reply} in - StateOps + StateOps, in + Replies or by calling gen_statem:reply(Client, Reply) diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 9bb5ed013b..7c3cd8c2f3 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -74,14 +74,13 @@ {'retry', Retry :: boolean()} | 'hibernate' | % Hibernate the server instead of going into receive {'hibernate', Hibernate :: boolean()} | + (Timeout :: timeout()) | % {timeout,Timeout} {'timeout', % Generate a ('timeout', Msg, ...) event after Time Time :: timeout(), Msg :: term()}. -type state_operation() :: %% These can occur multiple times and are executed in order %% of appearence in the state_op() list - {'reply', % Reply to a client - Client :: client(), Reply :: term()} | - {'stop', Reason :: term()} | % Stop the server + reply_operation() | {'insert_event', % Insert event as the next to handle EventType :: event_type(), EventContent :: term()} | @@ -95,6 +94,9 @@ MonitorRef :: reference()} | {'unlink', % Unlink and clean up mess(ages) Id :: pid() | port()}. +-type reply_operation() :: + {'reply', % Reply to a client + Client :: client(), Reply :: term()}. %% The state machine init function. It is called only once and %% the server is not running until this function has returned @@ -122,12 +124,20 @@ PrevState :: state(), State :: state(), % Current state StateData :: state_data()) -> - [state_op()] | % {State,StateData,[state_op()]} - {} | % {State,StateData,[]} - {NewStateData :: state_data()} | % {State,NewStateData,[retry]} - {NewState :: state(), - NewStateData :: state_data()} | % {NewState,NewStateData,[]} - {NewState :: state(), NewStateData :: state_data(), [state_op()]}. + {stop, % Stop the server + Reason :: term(), + NewStateData :: state_data()} | + {stop, % Stop the server + Reason :: term(), + [reply_operation()] | reply_operation(), + NewStateData :: state_data()} | + {next_state, % {next_state,NewState,NewStateData,[]} + NewState :: state(), + NewStateData :: state_data()} | + {next_state, % State transition, maybe to the same state + NewState :: state(), + NewStateData :: state_data(), + [state_op()] | state_op()}. %% Clean up before the server terminates. -callback terminate( @@ -679,35 +689,42 @@ loop_events( terminate(Class, Reason, Stacktrace, Debug, S, Q) end. -%% Interprete all callback return value variants -loop_event_result( - Parent, Debug, - #{state := State, state_data := StateData} = S, - Events, Event, Result) -> +%% Interpret all callback return value variants +loop_event_result(Parent, Debug, S, Events, Event, Result) -> case Result of - {} -> % Ignore - loop_event_state_ops( - Parent, Debug, S, Events, Event, - State, StateData, []); - {NewStateData} -> % Retry - loop_event_state_ops( - Parent, Debug, S, Events, Event, - State, NewStateData, [retry]); - {NewState,NewStateData} -> % Consume + {stop,Reason,NewStateData} -> + terminate( + Reason, Debug, + S#{state_data := NewStateData}, + [Event|Events]); + {stop,Reason,Reply,NewStateData} -> + NewS = S#{state_data := NewStateData}, + Q = [Event|Events], + Replies = + if + is_list(Reply) -> + Reply; + true -> + [Reply] + end, + BadReplies = + reply_then_terminate(Reason, Debug, NewS, Q, Replies), + %% Since it returned Replies was bad + terminate( + {bad_return_value,{stop,Reason,BadReplies,NewStateData}}, + Debug, NewS, Q); + {next_state,NewState,NewStateData} -> loop_event_state_ops( Parent, Debug, S, Events, Event, NewState, NewStateData, []); - {NewState,NewStateData,StateOps} when is_list(StateOps) -> + {next_state,NewState,NewStateData,StateOps} + when is_list(StateOps) -> loop_event_state_ops( Parent, Debug, S, Events, Event, NewState, NewStateData, StateOps); - StateOps when is_list(StateOps) -> % Stay in state - loop_event_state_ops( - Parent, Debug, S, Events, Event, - State, StateData, StateOps); - BadReturn -> + _ -> terminate( - {bad_return_value,BadReturn}, Debug, S, [Event|Events]) + {bad_return_value,Result}, Debug, S, [Event|Events]) end. loop_event_state_ops( @@ -750,7 +767,8 @@ loop_event_state_ops( %% Pretend the timeout has just been received {undefined,Q3 ++ [{timeout,Msg}]}; {timeout,Time,Msg} -> - {erlang:start_timer(Time, self(), Msg),Q3} + {erlang:start_timer(Time, self(), Msg), + Q3} end, loop_events( Parent, NewDebug, @@ -820,11 +838,8 @@ process_state_operations([], Debug, _S, Q, P) -> process_state_operations([Operation|Operations] = OOs, Debug, S, Q, P) -> case Operation of {reply,{_To,_Tag}=Client,Reply} -> - reply(Client, Reply), - NewDebug = sys_debug(Debug, S, {out,Reply,Client}), + NewDebug = do_reply(Debug, S, Client, Reply), process_state_operations(Operations, NewDebug, S, Q, P); - {stop,Reason} -> - [Reason,Debug]; {insert_event,Type,Content} -> case event_type(Type) of true -> @@ -854,6 +869,22 @@ process_state_operations([Operation|Operations] = OOs, Debug, S, Q, P) -> end end. +reply_then_terminate(Reason, Debug, S, Q, []) -> + terminate(Reason, Debug, S, Q); +reply_then_terminate(Reason, Debug, S, Q, [R|Rs] = RRs) -> + case R of + {reply,{_To,_Tag}=Client,Reply} -> + NewDebug = do_reply(Debug, S, Client, Reply), + reply_then_terminate(Reason, NewDebug, S, Q, Rs); + _ -> + RRs % bad_return_value + end. + +do_reply(Debug, S, Client, Reply) -> + reply(Client, Reply), + sys_debug(Debug, S, {out,Reply,Client}). + + %% Remove oldest matching event from the queue(s) remove_event(RemoveFun, Q, P) -> try @@ -928,7 +959,7 @@ remove_fun({unlink,Id}) -> end catch Class:Reason -> - {Class,Reason,erlang:get_stacktrace()} + [Class,Reason,erlang:get_stacktrace()] end; remove_fun(_) -> undefined. diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 342be32acb..51e08f5ec1 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -1068,29 +1068,30 @@ terminate(_Reason, _State, _Data) -> idle(cast, {connect,Pid}, _, _, Data) -> Pid ! accept, - {wfor_conf,Data}; + {next_state,wfor_conf,Data}; idle({call,From}, connect, _, _, Data) -> - {wfor_conf,Data,[{reply,From,accept}]}; + {next_state,wfor_conf,Data,[{reply,From,accept}]}; idle(cast, badreturn, _, _, _Data) -> badreturn; idle({call,_From}, badreturn, _, _, _Data) -> badreturn; -idle({call,From}, {delayed_answer,T}, _, _, _Data) -> +idle({call,From}, {delayed_answer,T}, _, State, Data) -> receive after T -> - [{reply,From,delayed}] + {next_state,State,Data, + [{reply,From,delayed}]} end; idle({call,From}, {timeout,Time}, _, State, _Data) -> - {timeout, {From,Time}, [{timeout,Time,State}]}; + {next_state,timeout,{From,Time}, + [{timeout,Time,State}]}; idle(Type, Content, PrevState, State, Data) -> case handle_common_events(Type, Content, PrevState, State, Data) of - [] -> + undefined -> case Type of {call,From} -> - {State,Data,[{reply,From,'eh?'}]}; + {next_state,State,Data,[{reply,From,'eh?'}]}; _ -> - {State,Data, - [{stop,{unexpected,State,PrevState,Type,Content}}]} + {stop,{unexpected,State,PrevState,Type,Content},Data} end; Result -> Result @@ -1100,146 +1101,151 @@ timeout(timeout, idle, idle, timeout, {From,Time}) -> TRef2 = erlang:start_timer(Time, self(), ok), TRefC1 = erlang:start_timer(Time, self(), cancel1), TRefC2 = erlang:start_timer(Time, self(), cancel2), - {timeout2, - {From, Time, TRef2}, + {next_state,timeout2,{From,Time,TRef2}, [{cancel_timer, TRefC1}, {insert_event,internal,{cancel_timer,TRefC2}}]}; -timeout(_, _, _, _, Data) -> - {Data}. +timeout(_, _, _, State, Data) -> + {next_state,State,Data}. timeout2( internal, {cancel_timer,TRefC2}, timeout, _, {From,Time,TRef2}) -> Time4 = Time * 4, receive after Time4 -> ok end, - {timeout3,{From,TRef2},[{cancel_timer,TRefC2}]}; -timeout2(_, _, _, _, Data) -> - {Data}. + {next_state,timeout3,{From,TRef2}, + [{cancel_timer,TRefC2}]}; +timeout2(_, _, _, State, Data) -> + {next_state,State,Data}. timeout3(info, {timeout,TRef2,Result}, _, _, {From,TRef2}) -> - {idle,state,[{reply,From,Result}]}; -timeout3(_, _, _, _, Data) -> - {Data}. + {next_state,idle,state, + [{reply,From,Result}]}; +timeout3(_, _, _, State, Data) -> + {next_state,State,Data}. wfor_conf({call,From}, confirm, _, _, Data) -> - {connected,Data,[{reply,From,yes}]}; + {next_state,connected,Data, + [{reply,From,yes}]}; wfor_conf(cast, confirm, _, _, Data) -> - {connected,Data}; + {next_state,connected,Data}; wfor_conf(Type, Content, PrevState, State, Data) -> case handle_common_events(Type, Content, PrevState, State, Data) of - [] -> + undefined -> case Type of {call,From} -> - {idle,Data,[{reply,From,'eh?'}]}; + {next_state,idle,Data, + [{reply,From,'eh?'}]}; _ -> - {Data} + {next_state,State,Data} end; Result -> Result end. connected({call,From}, {msg,Ref}, _, State, Data) -> - {State,Data,[{reply,From,{ack,Ref}}]}; -connected(cast, {msg,From,Ref}, _, _, _Data) -> + {next_state,State,Data, + [{reply,From,{ack,Ref}}]}; +connected(cast, {msg,From,Ref}, _, State, Data) -> From ! {ack,Ref}, - {}; + {next_state,State,Data}; connected({call,From}, disconnect, _, _, Data) -> - {idle,Data,[{reply,From,yes}]}; + {next_state,idle,Data, + [{reply,From,yes}]}; connected(cast, disconnect, _, _, Data) -> - {idle,Data}; + {next_state,idle,Data}; connected(Type, Content, PrevState, State, Data) -> case handle_common_events(Type, Content, PrevState, State, Data) of - [] -> + undefined -> case Type of {call,From} -> - [{reply,From,'eh?'}]; + {next_state,State,Data, + [{reply,From,'eh?'}]}; _ -> - {Data} + {next_state,State,Data} end; Result -> Result end. -state0({call,From}, stop, _, State, Data) -> - {State,Data, - [{reply,From,stopped}, - {stop,normal}]}; +state0({call,From}, stop, _, _, Data) -> + {stop,normal,[{reply,From,stopped}],Data}; state0(Type, Content, PrevState, State, Data) -> case handle_common_events(Type, Content, PrevState, State, Data) of - [] -> - {Data}; + undefined -> + {next_state,State,Data}; Result -> Result end. -hiber_idle({call,From}, 'alive?', _, _, _) -> - [{reply,From,'alive!'}]; +hiber_idle({call,From}, 'alive?', _, State, Data) -> + {next_state,State,Data, + [{reply,From,'alive!'}]}; hiber_idle({call,From}, hibernate_sync, _, _, Data) -> - {hiber_wakeup,Data, + {next_state,hiber_wakeup,Data, [{reply,From,hibernating}, hibernate]}; hiber_idle(info, hibernate_later, _, State, _) -> Tref = erlang:start_timer(1000, self(), hibernate), - {State,Tref}; + {next_state,State,Tref}; hiber_idle(info, hibernate_now, _, State, Data) -> - {State,Data,[hibernate]}; + {next_state,State,Data, + [hibernate]}; hiber_idle(info, {timeout,Tref,hibernate}, _, State, Tref) -> - {State,[], + {next_state,State,[], [hibernate]}; hiber_idle(cast, hibernate_async, _, _, Data) -> - {hiber_wakeup,Data, + {next_state,hiber_wakeup,Data, [hibernate]}; hiber_idle(Type, Content, PrevState, State, Data) -> case handle_common_events(Type, Content, PrevState, State, Data) of - [] -> - {Data}; + undefined -> + {next_state,State,Data}; Result -> Result end. hiber_wakeup({call,From}, wakeup_sync, _, _, Data) -> - {hiber_idle,Data,[{reply,From,good_morning}]}; + {next_state,hiber_idle,Data, + [{reply,From,good_morning}]}; hiber_wakeup({call,From}, snooze_sync, _, State, Data) -> - {State,Data, + {next_state,State,Data, [{reply,From,please_just_five_more}, hibernate]}; hiber_wakeup(cast, wakeup_async, _, _, Data) -> - {hiber_idle,Data}; -hiber_wakeup(cast, snooze_async, _, _, _Data) -> - [hibernate]; + {next_state,hiber_idle,Data}; +hiber_wakeup(cast, snooze_async, _, State, Data) -> + {next_state,State,Data, + [hibernate]}; hiber_wakeup(Type, Content, PrevState, State, Data) -> case handle_common_events(Type, Content, PrevState, State, Data) of - [] -> - {Data}; + undefined -> + {next_state,State,Data}; Result -> Result end. handle_common_events({call,From}, get, _, State, Data) -> - [{reply,From,{state,State,Data}}]; + {next_state,State,Data, + [{reply,From,{state,State,Data}}]}; handle_common_events(cast, {get,Pid}, _, State, Data) -> Pid ! {state,State,Data}, - {}; -handle_common_events({call,From}, stop, _, _, _) -> - [{reply,From,stopped}, - {stop,normal}]; -handle_common_events(cast, stop, _, State, Data) -> - {State,Data, - [{stop,normal}]}; -handle_common_events({call,From}, {stop,Reason}, _, State, Data) -> - {State,Data, - [{reply,From,stopped}, - {stop,Reason}]}; -handle_common_events(cast, {stop,Reason}, _, _, _) -> - [{stop,Reason}]; + {next_state,State,Data}; +handle_common_events({call,From}, stop, _, _, Data) -> + {stop,normal,[{reply,From,stopped}],Data}; +handle_common_events(cast, stop, _, _, Data) -> + {stop,normal,Data}; +handle_common_events({call,From}, {stop,Reason}, _, _, Data) -> + {stop,Reason,{reply,From,stopped},Data}; +handle_common_events(cast, {stop,Reason}, _, _, Data) -> + {stop,Reason,Data}; handle_common_events({call,From}, 'alive?', _, State, Data) -> - {State,Data, + {next_state,State,Data, [{reply,From,yes}]}; handle_common_events(cast, {'alive?',Pid}, _, State, Data) -> Pid ! yes, - {State,Data}; + {next_state,State,Data}; handle_common_events(_, _, _, _, _) -> - []. + undefined. code_change(_OldVsn, State, StateData, _Extra) -> {ok,State,StateData}. -- cgit v1.2.3 From 99d48f5921f95073c8e46280494746b0e1a2c375 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 16 Feb 2016 17:54:15 +0100 Subject: Implement option callback_mode --- lib/stdlib/doc/src/gen_statem.xml | 272 ++++++++++++++++++++++------------- lib/stdlib/src/gen_statem.erl | 134 +++++++++++------ lib/stdlib/test/gen_statem_SUITE.erl | 242 ++++++++++++++++++++++--------- 3 files changed, 439 insertions(+), 209 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index f7ce0f61ae..f608e10469 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -31,8 +31,15 @@ gen_statem Generic State Machine Behaviour -

A behaviour module for implementing a state machine, primarily - a finite state machine, but an infinite state space is possible. +

A behaviour module for implementing a state machine. + Two callback modes are supported. One for a finite state + machine like gen_fsm + that require the state to be an atom and use that state as + the name of the callback function for a particular state, + and one without restriction on the state that use the same + callback function for all states. +

+

A generic state machine process (gen_statem) implemented using this module will have a standard set of interface functions and include functionality for tracing and error reporting. @@ -55,7 +62,7 @@ gen_statem:stop -----> Module:terminate/2 gen_statem:call gen_statem:cast erlang:send -erlang:'!' -----> Module:State/5 +erlang:'!' -----> Module:StateName/5 Module:handle_event/5 - -----> Module:terminate/3 @@ -75,47 +82,39 @@ erlang:'!' -----> Module:State/5

The "state function" for a specific state in a gen_statem is the callback function that is called - for all events in this state. - An event can can be postponed causing it to be retried - after the state has changed, or more precisely; - after a state change all postponed events are retried - in the new state. + for all events in this state, and is selected depending on + callback_mode + that the implementation selects during gen_statem init.

-

The state machine - State - is normally an atom in which case the - state function - that will be called is - Module:State/5. - For a - State - that is not an atom the - state function - - Module:handle_event/5 - will be called. - If you use handle_event as a - state and later - decide to use non-atom states you will then have to - rewrite your code to stop using that state. +

When + callback_mode + is state_functions the state has to be an atom and + is used as the state function name. + See + + Module:StateName/5 + . + This naturally collects all code for a specific state + in one function and hence dispatches on state first.

-

When using an atom-only - State - it becomes fairly obvious in the implementation code - which events are handled in which state - since there are different callback functions for different states. +

When + callback_mode + is handle_event_function the state can be any term + and the state function name is + + Module:handle_event/5 + . + This makes it easy to dispatch on state or on event as + you like but you will have to implement it. + Also be careful about which events you handle in which + states so you do not accidentally postpone one event + forever creating an infinite busy loop.

- When using a non-atom State - all events are handled in the callback function - - Module:handle_event/5 - - so it may require more coding discipline to ensure what events - are handled in which state. Therefore it might be a wee bit - easier to accidentally postpone an event in two or more states - and not handling it in any of them causing a tight infinite - loop when the event bounces to be retried between the states. + An event can can be postponed causing it to be retried + after the state has changed, or more precisely; + after a state change all postponed events are retried + in the new state.

A gen_statem handles system messages as documented in sys. @@ -239,18 +238,22 @@ erlang:'!' -----> Module:State/5 -

If the gen_statem State is an atom(), the - state function is - Module:State/5. - If it is any other term() the - state function is - - Module:handle_event/5 - . After a state change (NewState =/= State) +

After a state change (NewState =/= State) all postponed events are retried.

+ + + +

If + + callback_mode + is state_functions, which is the default, + the state has to be of this type i.e an atom(). +

+
+
@@ -296,6 +299,33 @@ erlang:'!' -----> Module:State/5

+ + + +

Option that only is valid when initializing the gen_statem

+
+
+ + + + + state_functions + The state has to be of type + state_name() + and one callback function per state that is + + Module:StateName/5 + is used. This is the default. + + handle_event_function + The state can be any term and the callback function + + Module:handle_event/5 + is used for all states. + + + + @@ -472,6 +502,53 @@ erlang:'!' -----> Module:State/5 + + + + + + {stop,Reason,NewStateData} + + The same as + {stop,Reason,[],NewStateData} + + {stop, + Reason, + Replies, + NewStateData} + + The gen_statem will first send all + Replies and then terminate by calling + + Module:terminate/3 + with Reason. + + + + {next_state,NewState,NewStateData} + + + The same as + + {next_state,NewState,NewStateData,[]} + + + + + {next_state, + NewState, + NewStateData, + StateOps} + + + The gen_statem will do a state transition to + NewState + (which may be the same as State) + and execute all StateOps + + + + @@ -777,13 +854,16 @@ erlang:'!' -----> Module:State/5

Module, Options and Server have the same meanings as when calling - gen_statem:start[_link]/3,4. + + gen_statem:start[_link]/3,4 + . However, the server_name() name must have been registered accordingly before this function is called.

-

State and StateData +

State, StateData + and StateOps have the same meanings as in the return value of Module:init/1. Also, the callback module Module @@ -821,8 +901,13 @@ erlang:'!' -----> Module:State/5  | {ok,State,StateData,StateOps}  | {stop,Reason} | ignore State = state() - StateData = state_data() - StateOps = [state_op()] + StateData = + state_data() + + StateOps = + [state_op() + | init_option()] + Reason = term() @@ -843,10 +928,15 @@ erlang:'!' -----> Module:State/5 of the gen_statem.

The StateOps - are executed before entering the first + are executed when entering the first state just as for a state function.

+

This function allows an option to select the callback mode + of the gen_statem. See + init_option. + This option is not allowed from the state function(s). +

If something goes wrong during the initialization the function should return {stop,Reason} or ignore. See @@ -856,10 +946,10 @@ erlang:'!' -----> Module:State/5 - Module:handle_event(EventType, EventContent, - PrevState, State, StateData) -> Result + Module:StateName(EventType, EventContent, + PrevStateName, StateName, StateData) -> Result - Module:State(EventType, EventContent, + Module:handle_event(EventType, EventContent, PrevState, State, StateData) -> Result Handle an event @@ -868,41 +958,23 @@ erlang:'!' -----> Module:State/5 event_type() EventContent = term() - Result = {stop,Reason,NewStateData} -   The same as {stop,Reason,[],NewStateData} -   | {stop,Reason,Reply,NewStateData} -   The same as - {stop,Reason,[Reply],NewStateData} - -   | {stop,Reason,Replies,NewStateData} -   The gen_statem will first send all - Replies and then call - - Module:terminate/3 - with Reason and terminate. - -   | {next_state,NewState,NewStateData} -   The same as - {next_state,NewState,NewStateData,NewStateData,[]} - -   | {next_state,NewState,NewStateData,StateOps} -   The gen_statem will do a state transition to - NewState (which may be the same as State) - and execute all StateOps - - Reason = term() - PrevState = State = NewState = + PrevStateName = + state_name() + | reference() + + StateName = + state_name() + + PrevState = State = state() StateData = NewStateData = state_data() - Reply = - reply_operation() - - Replies = [Reply] - StateOps = - [state_op()] + Result = + + state_callback_result() + @@ -923,19 +995,18 @@ erlang:'!' -----> Module:State/5 gen_statem:reply(Client, Reply) .

-

State - is the internal state of the gen_statem which - when State is an atom() - is the same as this function's name, so it is seldom useful, - except for example when comparing with PrevState - that is the gen_statem's previous state, or in - - Module:handle_event/5 - since that function is common for all states - that are not an atom(). -

-

If this function returns with - NewState =/= State +

StateName is rarely useful. One exception is if + you call a common event handling function from your state + function then you might want to pass StateName. +

+

PrevStateName and PrevState are rarely useful. + They can be used when you want to do something only at the + first event in a state. Note that when gen_statem enters + its first state this is set to a reference() since + that can not match the state. +

+

If this function returns with a new state that + does not match equal (=/=) to the current state all postponed events will be retried in the new state.

See state_op() @@ -946,7 +1017,7 @@ erlang:'!' -----> Module:State/5 - Module:terminate(Reason, State, StateData) + Module:terminate(Reason, State, StateData) -> Ignored Clean up before termination Reason = normal | shutdown | {shutdown,term()} | term() @@ -956,6 +1027,7 @@ erlang:'!' -----> Module:State/5 state_data() + Ignored = term()

This function is called by a gen_statem when it is about to diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 7c3cd8c2f3..e6bb38fc2c 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -44,6 +44,9 @@ -export( [wakeup_from_hibernate/3]). +%% Fix problem for doc build +-export_type([state_callback_result/0]). + %%%========================================================================== %%% Interface functions. %%%========================================================================== @@ -51,14 +54,18 @@ -type client() :: {To :: pid(), Tag :: term()}. % Reply-to specifier for call -type state() :: - atom() | % Calls state callback function State/5 - term(). % Calls state callback function handle_event/5 + state_name() | % For state callback function StateName/5 + term(). % For state callback function handle_event/5 +-type state_name() :: atom(). -type state_data() :: term(). -type event_type() :: {'call',Client :: client()} | 'cast' | 'info' | 'timeout' | 'internal'. -type event_predicate() :: % Return true for the event in question fun((event_type(), term()) -> boolean()). +-type init_option() :: + {'callback_mode', callback_mode()}. +-type callback_mode() :: 'state_functions' | 'handle_event_function'. -type state_op() :: %% First NewState and NewStateData are set, %% then all state_operations() are executed in order of @@ -97,6 +104,21 @@ -type reply_operation() :: {'reply', % Reply to a client Client :: client(), Reply :: term()}. +-type state_callback_result() :: + {stop, % Stop the server + Reason :: term(), + NewStateData :: state_data()} | + {stop, % Stop the server + Reason :: term(), + Replies :: [reply_operation()] | reply_operation(), + NewStateData :: state_data()} | + {next_state, % {next_state,NewState,NewStateData,[]} + NewState :: state(), + NewStateData :: state_data()} | + {next_state, % State transition, maybe to the same state + NewState :: state(), + NewStateData :: state_data(), + StateOps :: [state_op()] | state_op()}. %% The state machine init function. It is called only once and %% the server is not running until this function has returned @@ -104,40 +126,36 @@ %% for all events to this server. -callback init(Args :: term()) -> {'ok', state(), state_data()} | - {'ok', state(), state_data(), [state_op()]} | + {'ok', state(), state_data(), [state_op()|init_option()]} | 'ignore' | {'stop', Reason :: term()}. -%% An example callback for a fictive state 'handle_event' -%% that you should avoid having. See below. +%% Example callback for callback_mode =:= state_functions +%% state name 'state_name'. +%% +%% In this mode all states has to be type state_name() i.e atom(). %% %% Note that state callbacks and only state callbacks have arity 5 %% and that is intended. +-callback state_name( + event_type(), + EventContent :: term(), + PrevStateName :: state_name() | reference(), + StateName :: state_name(), % Current state + StateData :: state_data()) -> + state_callback_result(). +%% +%% Callback for callback_mode =:= handle_event_function. %% -%% You should not actually use 'handle_event' as a state name, -%% since it is the callback function that is used if you would use -%% a State that is not an atom(). This is because since there is -%% no obvious way to decide on a state function name from any term(). +%% Note that state callbacks and only state callbacks have arity 5 +%% and that is intended. -callback handle_event( event_type(), EventContent :: term(), PrevState :: state(), State :: state(), % Current state StateData :: state_data()) -> - {stop, % Stop the server - Reason :: term(), - NewStateData :: state_data()} | - {stop, % Stop the server - Reason :: term(), - [reply_operation()] | reply_operation(), - NewStateData :: state_data()} | - {next_state, % {next_state,NewState,NewStateData,[]} - NewState :: state(), - NewStateData :: state_data()} | - {next_state, % State transition, maybe to the same state - NewState :: state(), - NewStateData :: state_data(), - [state_op()] | state_op()}. + state_callback_result(). %% Clean up before the server terminates. -callback terminate( @@ -171,8 +189,11 @@ -optional_callbacks( [format_status/2, % Has got a default implementation - handle_event/5]). % Only needed for State not an atom() -%% For every atom() State there has to be a State/5 callback function + %% + state_name/5, % Example for callback_mode =:= state_functions: + %% there has to be a StateName/5 callback function for every StateName. + %% + handle_event/5]). % For callback_mode =:= handle_event_function %% Type validation functions client({Pid,Tag}) when is_pid(Pid), is_reference(Tag) -> @@ -366,7 +387,8 @@ enter_loop(Module, Options, State, StateData) -> -spec enter_loop( Module :: module(), Options :: [debug_opt()], State :: state(), StateData :: state_data(), - Server_or_StateOps :: server_name() | pid() | [state_op()]) -> + Server_or_StateOps :: + server_name() | pid() | [state_op()|init_option()]) -> no_return(). enter_loop(Module, Options, State, StateData, Server_or_StateOps) -> if @@ -384,7 +406,7 @@ enter_loop(Module, Options, State, StateData, Server_or_StateOps) -> Module :: module(), Options :: [debug_opt()], State :: state(), StateData :: state_data(), Server :: server_name() | pid(), - StateOps :: [state_op()]) -> + StateOps :: [state_op()|init_option()]) -> no_return(). enter_loop(Module, Options, State, StateData, Server, StateOps) -> Parent = gen:get_parent(), @@ -410,11 +432,12 @@ do_send(Proc, Msg) -> end. %% Here init_it and all enter_loop functions converge -enter(Module, Options, State, StateData, Server, StateOps, Parent) -> +enter(Module, Options, State, StateData, Server, InitOps, Parent) -> Name = gen:get_proc_name(Server), Debug = gen:debug_options(Name, Options), PrevState = make_ref(), S = #{ + callback_mode => state_functions, module => Module, name => Name, prev_state => PrevState, @@ -423,9 +446,17 @@ enter(Module, Options, State, StateData, Server, StateOps, Parent) -> timer => undefined, postponed => [], hibernate => false}, - loop_event_state_ops( - Parent, Debug, S, [], {event,undefined}, - State, StateData, [{retry,false}|StateOps]). + case collect_init_options(InitOps) of + {CallbackMode,StateOps} -> + loop_event_state_ops( + Parent, Debug, + S#{callback_mode := CallbackMode}, + [], {event,undefined}, + State, StateData, + [{retry,false}|StateOps]); + [Reason] -> + terminate(Reason, Debug, S, []) + end. %%%========================================================================== %%% gen callbacks @@ -643,13 +674,13 @@ loop_receive(Parent, Debug, S, Event, Timer) -> %% The loop_event* functions optimize S map handling by dismantling it, %% passing the parts in arguments to avoid map lookups and construct the %% new S map in one go on exit. Premature optimization, I know, but -%% the code was way to readable and there were quite some map lookups -%% repeated in different functions. +%% there were quite some map lookups repeated in different functions. loop_events(Parent, Debug, S, [], _Timer) -> loop(Parent, Debug, S); loop_events( Parent, Debug, - #{module := Module, + #{callback_mode := CallbackMode, + module := Module, prev_state := PrevState, state := State, state_data := StateData} = S, @@ -657,11 +688,11 @@ loop_events( _ = (Timer =/= undefined) andalso cancel_timer(Timer), Func = - if - is_atom(State) -> - State; - true -> - handle_event + case CallbackMode of + handle_event_function -> + handle_event; + state_functions -> + State end, try Module:Func(Type, Content, PrevState, State, StateData) of Result -> @@ -675,7 +706,10 @@ loop_events( %% Process an undef to check for the simple mistake %% of calling a nonexistent state function case erlang:get_stacktrace() of - [{Module,State,[Event,StateData]=Args,_}|Stacktrace] -> + [{Module,Func, + [Type,Content,PrevState,State,StateData]=Args, + _} + |Stacktrace] -> terminate( error, {undef_state_function,{Module,State,Args}}, @@ -709,7 +743,7 @@ loop_event_result(Parent, Debug, S, Events, Event, Result) -> end, BadReplies = reply_then_terminate(Reason, Debug, NewS, Q, Replies), - %% Since it returned Replies was bad + %% Since we got back here Replies was bad terminate( {bad_return_value,{stop,Reason,BadReplies,NewStateData}}, Debug, NewS, Q); @@ -794,6 +828,24 @@ loop_event_state_ops( %%--------------------------------------------------------------------------- %% Server helpers +collect_init_options(InitOps) -> + collect_init_options(lists:reverse(InitOps), state_functions, []). +%% Keep the last of each kind +collect_init_options([], CallbackMode, StateOps) -> + {CallbackMode,StateOps}; +collect_init_options([InitOp|InitOps] = IOIOs, CallbackMode, StateOps) -> + case InitOp of + {callback_mode,Mode} + when Mode =:= state_functions; + Mode =:= handle_event_function -> + collect_init_options(InitOps, Mode, StateOps); + {callback_mode,_} -> + [{bad_init_ops,IOIOs}]; + _ -> % Collect others as StateOps + collect_init_options( + InitOps, CallbackMode, [InitOp|StateOps]) + end. + collect_state_options(StateOps) -> collect_state_options( lists:reverse(StateOps), false, false, undefined, []). diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 51e08f5ec1..8a96f0e8e2 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -19,7 +19,7 @@ %% -module(gen_statem_SUITE). --include_lib("test_server/include/test_server.hrl"). +-include_lib("common_test/include/ct.hrl"). -compile(export_all). -behaviour(gen_statem). @@ -30,7 +30,11 @@ suite() -> [{ct_hooks,[ts_install_cth]}]. all() -> [{group, start}, + {group, start_handle_event}, + {group, stop}, + {group, stop_handle_event}, {group, abnormal}, + {group, abnormal_handle_event}, shutdown, {group, sys}, hibernate, enter_loop]. @@ -38,10 +42,21 @@ groups() -> [{start, [], [start1, start2, start3, start4, start5, start6, start7, start8, start9, start10, start11, start12]}, + {start_handle_event, [], + [start1, start2, start3, start4, start5, start6, start7, + start8, start9, start10, start11, start12]}, {stop, [], [stop1, stop2, stop3, stop4, stop5, stop6, stop7, stop8, stop9, stop10]}, + {stop_handle_event, [], + [stop1, stop2, stop3, stop4, stop5, stop6, stop7, stop8, stop9, stop10]}, {abnormal, [], [abnormal1, abnormal2]}, + {abnormal_handle_event, [], [abnormal1, abnormal2]}, {sys, [], + [sys1, + call_format_status, + error_format_status, terminate_crash_format, + get_state, replace_state]}, + {sys_handle_event, [], [sys1, call_format_status, error_format_status, terminate_crash_format, @@ -53,6 +68,12 @@ init_per_suite(Config) -> end_per_suite(_Config) -> ok. +init_per_group(GroupName, Config) + when GroupName =:= start_handle_event; + GroupName =:= stop_handle_event; + GroupName =:= abnormal_handle_event; + GroupName =:= sys_handle_event -> + [{init_ops,[{callback_mode,handle_event_function}]}|Config]; init_per_group(_GroupName, Config) -> Config. @@ -65,6 +86,7 @@ init_per_testcase(_CaseName, Config) -> %%% dbg:tracer(), %%% dbg:p(all, c), %%% dbg:tpl(gen_statem, cx), +%%% dbg:tpl(sys, cx), [{watchdog, Dog} | Config]. end_per_testcase(_CaseName, Config) -> @@ -88,7 +110,7 @@ end_per_testcase(_CaseName, Config) -> start1(Config) when is_list(Config) -> %%OldFl = process_flag(trap_exit, true), - {ok,Pid0} = gen_statem:start_link(?MODULE, [], []), + {ok,Pid0} = gen_statem:start_link(?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid0), ok = do_sync_func_test(Pid0), stop_it(Pid0), @@ -102,7 +124,7 @@ start1(Config) when is_list(Config) -> %% anonymous w. shutdown start2(Config) when is_list(Config) -> %% Dont link when shutdown - {ok,Pid0} = gen_statem:start(?MODULE, [], []), + {ok,Pid0} = gen_statem:start(?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid0), ok = do_sync_func_test(Pid0), stopped = gen_statem:call(Pid0, {stop,shutdown}), @@ -113,13 +135,15 @@ start2(Config) when is_list(Config) -> start3(Config) when is_list(Config) -> %%OldFl = process_flag(trap_exit, true), - {ok,Pid0} = gen_statem:start(?MODULE, [], [{timeout,5}]), + {ok,Pid0} = + gen_statem:start(?MODULE, start_arg(Config, []), [{timeout,5}]), ok = do_func_test(Pid0), ok = do_sync_func_test(Pid0), stop_it(Pid0), - {error,timeout} = gen_statem:start(?MODULE, sleep, - [{timeout,5}]), + {error,timeout} = + gen_statem:start( + ?MODULE, start_arg(Config, sleep), [{timeout,5}]), %%process_flag(trap_exit, OldFl), ok = verify_empty_msgq(). @@ -128,7 +152,7 @@ start3(Config) when is_list(Config) -> start4(Config) when is_list(Config) -> OldFl = process_flag(trap_exit, true), - ignore = gen_statem:start(?MODULE, ignore, []), + ignore = gen_statem:start(?MODULE, start_arg(Config, ignore), []), process_flag(trap_exit, OldFl), ok = verify_empty_msgq(). @@ -138,14 +162,14 @@ start5(suite) -> []; start5(Config) when is_list(Config) -> OldFl = process_flag(trap_exit, true), - {error,stopped} = gen_statem:start(?MODULE, stop, []), + {error,stopped} = gen_statem:start(?MODULE, start_arg(Config, stop), []), process_flag(trap_exit, OldFl), ok = verify_empty_msgq(). %% anonymous linked start6(Config) when is_list(Config) -> - {ok,Pid} = gen_statem:start_link(?MODULE, [], []), + {ok,Pid} = gen_statem:start_link(?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid), ok = do_sync_func_test(Pid), stop_it(Pid), @@ -157,11 +181,11 @@ start7(Config) when is_list(Config) -> STM = {global,my_stm}, {ok,Pid} = - gen_statem:start_link(STM, ?MODULE, [], []), + gen_statem:start_link(STM, ?MODULE, start_arg(Config, []), []), {error,{already_started,Pid}} = - gen_statem:start_link(STM, ?MODULE, [], []), + gen_statem:start_link(STM, ?MODULE, start_arg(Config, []), []), {error,{already_started,Pid}} = - gen_statem:start(STM, ?MODULE, [], []), + gen_statem:start(STM, ?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid), ok = do_sync_func_test(Pid), @@ -179,9 +203,9 @@ start8(Config) when is_list(Config) -> STM = {local,Name}, {ok,Pid} = - gen_statem:start(STM, ?MODULE, [], []), + gen_statem:start(STM, ?MODULE, start_arg(Config, []), []), {error,{already_started,Pid}} = - gen_statem:start(STM, ?MODULE, [], []), + gen_statem:start(STM, ?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid), ok = do_sync_func_test(Pid), @@ -199,9 +223,9 @@ start9(Config) when is_list(Config) -> STM = {local,Name}, {ok,Pid} = - gen_statem:start_link(STM, ?MODULE, [], []), + gen_statem:start_link(STM, ?MODULE, start_arg(Config, []), []), {error,{already_started,Pid}} = - gen_statem:start(STM, ?MODULE, [], []), + gen_statem:start(STM, ?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid), ok = do_sync_func_test(Pid), @@ -217,11 +241,11 @@ start10(Config) when is_list(Config) -> STM = {global,my_stm}, {ok,Pid} = - gen_statem:start(STM, ?MODULE, [], []), + gen_statem:start(STM, ?MODULE, start_arg(Config, []), []), {error,{already_started,Pid}} = - gen_statem:start(STM, ?MODULE, [], []), + gen_statem:start(STM, ?MODULE, start_arg(Config, []), []), {error,{already_started,Pid}} = - gen_statem:start_link(STM, ?MODULE, [], []), + gen_statem:start_link(STM, ?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid), ok = do_sync_func_test(Pid), @@ -238,19 +262,19 @@ start11(Config) when is_list(Config) -> GlobalSTM = {global,Name}, {ok,Pid} = - gen_statem:start_link(LocalSTM, ?MODULE, [], []), + gen_statem:start_link(LocalSTM, ?MODULE, start_arg(Config, []), []), stop_it(Pid), {ok,_Pid1} = - gen_statem:start_link(LocalSTM, ?MODULE, [], []), + gen_statem:start_link(LocalSTM, ?MODULE, start_arg(Config, []), []), stop_it(Name), {ok,Pid2} = - gen_statem:start(GlobalSTM, ?MODULE, [], []), + gen_statem:start(GlobalSTM, ?MODULE, start_arg(Config, []), []), stop_it(Pid2), receive after 1 -> true end, Result = - gen_statem:start(GlobalSTM, ?MODULE, [], []), + gen_statem:start(GlobalSTM, ?MODULE, start_arg(Config, []), []), io:format("Result = ~p~n",[Result]), {ok,_Pid3} = Result, stop_it(GlobalSTM), @@ -263,11 +287,11 @@ start12(Config) when is_list(Config) -> VIA = {via,dummy_via,my_stm}, {ok,Pid} = - gen_statem:start_link(VIA, ?MODULE, [], []), + gen_statem:start_link(VIA, ?MODULE, start_arg(Config, []), []), {error,{already_started,Pid}} = - gen_statem:start_link(VIA, ?MODULE, [], []), + gen_statem:start_link(VIA, ?MODULE, start_arg(Config, []), []), {error,{already_started,Pid}} = - gen_statem:start(VIA, ?MODULE, [], []), + gen_statem:start(VIA, ?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid), ok = do_sync_func_test(Pid), @@ -279,23 +303,23 @@ start12(Config) when is_list(Config) -> %% Anonymous, reason 'normal' -stop1(_Config) -> - {ok,Pid} = gen_statem:start(?MODULE, [], []), +stop1(Config) -> + {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), ok = gen_statem:stop(Pid), false = erlang:is_process_alive(Pid), noproc = ?EXPECT_FAILURE(gen_statem:stop(Pid), Reason). %% Anonymous, other reason -stop2(_Config) -> - {ok,Pid} = gen_statem:start(?MODULE, [], []), +stop2(Config) -> + {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), ok = gen_statem:stop(Pid, other_reason, infinity), false = erlang:is_process_alive(Pid), ok. %% Anonymous, invalid timeout -stop3(_Config) -> - {ok,Pid} = gen_statem:start(?MODULE, [], []), +stop3(Config) -> + {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), _ = ?EXPECT_FAILURE( gen_statem:stop(Pid, other_reason, invalid_timeout), @@ -306,8 +330,10 @@ stop3(_Config) -> ok. %% Registered name -stop4(_Config) -> - {ok,Pid} = gen_statem:start({local,to_stop},?MODULE, [], []), +stop4(Config) -> + {ok,Pid} = + gen_statem:start( + {local,to_stop},?MODULE, start_arg(Config, []), []), ok = gen_statem:stop(to_stop), false = erlang:is_process_alive(Pid), noproc = @@ -315,9 +341,11 @@ stop4(_Config) -> ok. %% Registered name and local node -stop5(_Config) -> +stop5(Config) -> Name = to_stop, - {ok,Pid} = gen_statem:start({local,Name},?MODULE, [], []), + {ok,Pid} = + gen_statem:start( + {local,Name},?MODULE, start_arg(Config, []), []), ok = gen_statem:stop({Name,node()}), false = erlang:is_process_alive(Pid), noproc = @@ -325,9 +353,9 @@ stop5(_Config) -> ok. %% Globally registered name -stop6(_Config) -> +stop6(Config) -> STM = {global,to_stop}, - {ok,Pid} = gen_statem:start(STM, ?MODULE, [], []), + {ok,Pid} = gen_statem:start(STM, ?MODULE, start_arg(Config, []), []), ok = gen_statem:stop(STM), false = erlang:is_process_alive(Pid), noproc = @@ -335,11 +363,10 @@ stop6(_Config) -> ok. %% 'via' registered name -stop7(_Config) -> +stop7(Config) -> VIA = {via,dummy_via,to_stop}, dummy_via:reset(), - {ok,Pid} = gen_statem:start(VIA, - ?MODULE, [], []), + {ok,Pid} = gen_statem:start(VIA, ?MODULE, start_arg(Config, []), []), ok = gen_statem:stop(VIA), false = erlang:is_process_alive(Pid), noproc = @@ -347,46 +374,55 @@ stop7(_Config) -> ok. %% Anonymous on remote node -stop8(_Config) -> +stop8(Config) -> {ok,Node} = ?t:start_node(gen_statem_stop8, slave, []), Dir = filename:dirname(code:which(?MODULE)), rpc:call(Node, code, add_path, [Dir]), - {ok,Pid} = rpc:call(Node, gen_statem,start, [?MODULE,[],[]]), + {ok,Pid} = + rpc:call( + Node, gen_statem,start, + [?MODULE,start_arg(Config, []),[]]), ok = gen_statem:stop(Pid), false = rpc:call(Node, erlang, is_process_alive, [Pid]), noproc = ?EXPECT_FAILURE(gen_statem:stop(Pid), Reason1), true = ?t:stop_node(Node), - {nodedown,Node} = + {{nodedown,Node},{sys,terminate,_}} = ?EXPECT_FAILURE(gen_statem:stop(Pid), Reason2), ok. %% Registered name on remote node -stop9(_Config) -> +stop9(Config) -> Name = to_stop, LocalSTM = {local,Name}, {ok,Node} = ?t:start_node(gen_statem__stop9, slave, []), STM = {Name,Node}, Dir = filename:dirname(code:which(?MODULE)), rpc:call(Node, code, add_path, [Dir]), - {ok,Pid} = rpc:call(Node, gen_statem, start, [LocalSTM,?MODULE,[],[]]), + {ok,Pid} = + rpc:call( + Node, gen_statem, start, + [LocalSTM,?MODULE,start_arg(Config, []),[]]), ok = gen_statem:stop(STM), undefined = rpc:call(Node,erlang,whereis,[Name]), false = rpc:call(Node,erlang,is_process_alive,[Pid]), noproc = ?EXPECT_FAILURE(gen_statem:stop(STM), Reason1), true = ?t:stop_node(Node), - {nodedown,Node} = + {{nodedown,Node},{sys,terminate,_}} = ?EXPECT_FAILURE(gen_statem:stop(STM), Reason2), ok. %% Globally registered name on remote node -stop10(_Config) -> +stop10(Config) -> STM = {global,to_stop}, {ok,Node} = ?t:start_node(gen_statem_stop10, slave, []), Dir = filename:dirname(code:which(?MODULE)), rpc:call(Node,code,add_path,[Dir]), - {ok,Pid} = rpc:call(Node, gen_statem, start, [STM,?MODULE,[],[]]), + {ok,Pid} = + rpc:call( + Node, gen_statem, start, + [STM,?MODULE,start_arg(Config, []),[]]), global:sync(), ok = gen_statem:stop(STM), false = rpc:call(Node, erlang, is_process_alive, [Pid]), @@ -402,7 +438,8 @@ abnormal1(Config) when is_list(Config) -> Name = abnormal1, LocalSTM = {local,Name}, - {ok, _Pid} = gen_statem:start(LocalSTM, ?MODULE, [], []), + {ok, _Pid} = + gen_statem:start(LocalSTM, ?MODULE, start_arg(Config, []), []), %% timeout call. delayed = gen_statem:call(Name, {delayed_answer,1}, 100), @@ -410,13 +447,14 @@ abnormal1(Config) when is_list(Config) -> ?EXPECT_FAILURE( gen_statem:call(Name, {delayed_answer,1000}, 10), Reason), + ok = gen_statem:stop(Name), ok = verify_empty_msgq(). %% Check that bad return values makes the stm crash. Note that we must %% trap exit since we must link to get the real bad_return_ error abnormal2(Config) when is_list(Config) -> OldFl = process_flag(trap_exit, true), - {ok,Pid} = gen_statem:start_link(?MODULE, [], []), + {ok,Pid} = gen_statem:start_link(?MODULE, start_arg(Config, []), []), %% bad return value in the gen_statem loop {{bad_return_value,badreturn},_} = @@ -433,7 +471,7 @@ abnormal2(Config) when is_list(Config) -> shutdown(Config) when is_list(Config) -> process_flag(trap_exit, true), - {ok,Pid0} = gen_statem:start_link(?MODULE, [], []), + {ok,Pid0} = gen_statem:start_link(?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid0), ok = do_sync_func_test(Pid0), stopped = gen_statem:call(Pid0, {stop,{shutdown,reason}}), @@ -454,7 +492,7 @@ shutdown(Config) when is_list(Config) -> sys1(Config) when is_list(Config) -> - {ok,Pid} = gen_statem:start(?MODULE, [], []), + {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), {status, Pid, {module,gen_statem}, _} = sys:get_status(Pid), sys:suspend(Pid), Parent = self(), @@ -477,7 +515,7 @@ sys1(Config) when is_list(Config) -> stop_it(Pid). call_format_status(Config) when is_list(Config) -> - {ok,Pid} = gen_statem:start(?MODULE, [], []), + {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), Status = sys:get_status(Pid), {status,Pid,_Mod,[_PDict,running,_,_, Data]} = Status, [format_status_called|_] = lists:reverse(Data), @@ -485,7 +523,9 @@ call_format_status(Config) when is_list(Config) -> %% check that format_status can handle a name being an atom (pid is %% already checked by the previous test) - {ok, Pid2} = gen_statem:start({local, gstm}, ?MODULE, [], []), + {ok, Pid2} = + gen_statem:start( + {local, gstm}, ?MODULE, start_arg(Config, []), []), Status2 = sys:get_status(gstm), {status,Pid2,_Mod,[_PDict2,running,_,_,Data2]} = Status2, [format_status_called|_] = lists:reverse(Data2), @@ -494,13 +534,17 @@ call_format_status(Config) when is_list(Config) -> %% check that format_status can handle a name being a term other than a %% pid or atom GlobalName1 = {global,"CallFormatStatus"}, - {ok,Pid3} = gen_statem:start(GlobalName1, ?MODULE, [], []), + {ok,Pid3} = + gen_statem:start( + GlobalName1, ?MODULE, start_arg(Config, []), []), Status3 = sys:get_status(GlobalName1), {status,Pid3,_Mod,[_PDict3,running,_,_,Data3]} = Status3, [format_status_called|_] = lists:reverse(Data3), stop_it(Pid3), GlobalName2 = {global,{name, "term"}}, - {ok,Pid4} = gen_statem:start(GlobalName2, ?MODULE, [], []), + {ok,Pid4} = + gen_statem:start( + GlobalName2, ?MODULE, start_arg(Config, []), []), Status4 = sys:get_status(GlobalName2), {status,Pid4,_Mod,[_PDict4,running,_,_, Data4]} = Status4, [format_status_called|_] = lists:reverse(Data4), @@ -510,13 +554,15 @@ call_format_status(Config) when is_list(Config) -> %% pid or atom dummy_via:reset(), ViaName1 = {via,dummy_via,"CallFormatStatus"}, - {ok,Pid5} = gen_statem:start(ViaName1, ?MODULE, [], []), + {ok,Pid5} = gen_statem:start(ViaName1, ?MODULE, start_arg(Config, []), []), Status5 = sys:get_status(ViaName1), {status,Pid5,_Mod, [_PDict5,running,_,_, Data5]} = Status5, [format_status_called|_] = lists:reverse(Data5), stop_it(Pid5), ViaName2 = {via,dummy_via,{name,"term"}}, - {ok, Pid6} = gen_statem:start(ViaName2, ?MODULE, [], []), + {ok, Pid6} = + gen_statem:start( + ViaName2, ?MODULE, start_arg(Config, []), []), Status6 = sys:get_status(ViaName2), {status,Pid6,_Mod,[_PDict6,running,_,_,Data6]} = Status6, [format_status_called|_] = lists:reverse(Data6), @@ -528,7 +574,9 @@ error_format_status(Config) when is_list(Config) -> error_logger_forwarder:register(), OldFl = process_flag(trap_exit, true), StateData = "called format_status", - {ok,Pid} = gen_statem:start(?MODULE, {state_data,StateData}, []), + {ok,Pid} = + gen_statem:start( + ?MODULE, start_arg(Config, {state_data,StateData}), []), %% bad return value in the gen_statem loop {{bad_return_value,badreturn},_} = ?EXPECT_FAILURE(gen_statem:call(Pid, badreturn), Reason), @@ -562,7 +610,9 @@ terminate_crash_format(Config) when is_list(Config) -> error_logger_forwarder:register(), OldFl = process_flag(trap_exit, true), StateData = crash_terminate, - {ok,Pid} = gen_statem:start(?MODULE, {state_data,StateData}, []), + {ok,Pid} = + gen_statem:start( + ?MODULE, start_arg(Config, {state_data,StateData}), []), stop_it(Pid), Self = self(), receive @@ -595,7 +645,9 @@ terminate_crash_format(Config) when is_list(Config) -> get_state(Config) when is_list(Config) -> State = self(), - {ok,Pid} = gen_statem:start(?MODULE, {state_data,State}, []), + {ok,Pid} = + gen_statem:start( + ?MODULE, start_arg(Config, {state_data,State}), []), {idle,State} = sys:get_state(Pid), {idle,State} = sys:get_state(Pid, 5000), stop_it(Pid), @@ -603,13 +655,16 @@ get_state(Config) when is_list(Config) -> %% check that get_state can handle a name being an atom (pid is %% already checked by the previous test) {ok,Pid2} = - gen_statem:start({local,gstm}, ?MODULE, {state_data,State}, []), + gen_statem:start( + {local,gstm}, ?MODULE, start_arg(Config, {state_data,State}), []), {idle,State} = sys:get_state(gstm), {idle,State} = sys:get_state(gstm, 5000), stop_it(Pid2), %% check that get_state works when pid is sys suspended - {ok,Pid3} = gen_statem:start(?MODULE, {state_data,State}, []), + {ok,Pid3} = + gen_statem:start( + ?MODULE, start_arg(Config, {state_data,State}), []), {idle,State} = sys:get_state(Pid3), ok = sys:suspend(Pid3), {idle,State} = sys:get_state(Pid3, 5000), @@ -619,7 +674,9 @@ get_state(Config) when is_list(Config) -> replace_state(Config) when is_list(Config) -> State = self(), - {ok, Pid} = gen_statem:start(?MODULE, {state_data,State}, []), + {ok, Pid} = + gen_statem:start( + ?MODULE, start_arg(Config, {state_data,State}), []), {idle,State} = sys:get_state(Pid), NState1 = "replaced", Replace1 = fun({StateName, _}) -> {StateName,NState1} end, @@ -650,7 +707,9 @@ replace_state(Config) when is_list(Config) -> hibernate(Config) when is_list(Config) -> OldFl = process_flag(trap_exit, true), - {ok,Pid0} = gen_statem:start_link(?MODULE, hiber_now, []), + {ok,Pid0} = + gen_statem:start_link( + ?MODULE, start_arg(Config, hiber_now), []), is_in_erlang_hibernate(Pid0), stop_it(Pid0), receive @@ -659,7 +718,8 @@ hibernate(Config) when is_list(Config) -> ?t:fail(gen_statem_did_not_die) end, - {ok,Pid} = gen_statem:start_link(?MODULE, hiber, []), + {ok,Pid} = + gen_statem:start_link(?MODULE, start_arg(Config, hiber), []), true = ({current_function,{erlang,hibernate,3}} =/= erlang:process_info(Pid,current_function)), hibernating = gen_statem:call(Pid, hibernate_sync), @@ -1031,6 +1091,14 @@ verify_empty_msgq() -> [] = ?t:messages_get(), ok. +start_arg(Config, Arg) -> + case lists:keyfind(init_ops, 1, Config) of + {_,Ops} -> + {init_ops,Arg,Ops}; + false -> + Arg + end. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% The State Machine @@ -1052,7 +1120,16 @@ init(hiber_now) -> {ok,hiber_idle,[],[hibernate]}; init({state_data, StateData}) -> {ok,idle,StateData}; -init(_) -> +init({init_ops,Arg,InitOps}) -> + case init(Arg) of + {ok,State,Data,Ops} -> + {ok,State,Data,InitOps++Ops}; + {ok,State,Data} -> + {ok,State,Data,InitOps}; + Other -> + Other + end; +init([]) -> {ok,idle,state_data}. terminate(_, _State, crash_terminate) -> @@ -1247,6 +1324,35 @@ handle_common_events(cast, {'alive?',Pid}, _, State, Data) -> handle_common_events(_, _, _, _, _) -> undefined. +%% Dispatcher to test callback_mode handle_event_function +%% +%% Wrap the state in a 1 element list just to test non-atom +%% states. Note that the state from init/1 is not wrapped +%% so both atom and non-atom states are tested. +handle_event(Type, Event, PrevState, State, Data) -> + PrevStateName = unwrap(PrevState), + StateName = unwrap(State), + case + ?MODULE:StateName( + Type, Event, PrevStateName, StateName, Data) of + {next_state,NewState,NewStateData} -> + {next_state,wrap(NewState),NewStateData}; + {next_state,NewState,NewStateData,StateOps} -> + {next_state,wrap(NewState),NewStateData,StateOps}; + Other -> + Other + end. + +unwrap([State]) -> + State; +unwrap(State) -> + State. + +wrap(State) -> + [State]. + + + code_change(_OldVsn, State, StateData, _Extra) -> {ok,State,StateData}. -- cgit v1.2.3 From b0357eba067295b61fac0128344d8c49a07254cf Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Wed, 17 Feb 2016 17:38:36 +0100 Subject: Clean up error report printouts --- lib/stdlib/src/gen_statem.erl | 76 ++++++++++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 23 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index e6bb38fc2c..782e027b29 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -217,6 +217,22 @@ event_type(Type) -> false end. + + +-define( + STACKTRACE(), + try throw(ok) catch _ -> erlang:get_stacktrace() end). + +-define( + TERMINATE(Reason, Debug, S, Q), + terminate( + exit, + begin Reason end, + ?STACKTRACE(), + begin Debug end, + begin S end, + begin Q end)). + %%%========================================================================== %%% API @@ -455,7 +471,7 @@ enter(Module, Options, State, StateData, Server, InitOps, Parent) -> State, StateData, [{retry,false}|StateOps]); [Reason] -> - terminate(Reason, Debug, S, []) + ?TERMINATE(Reason, Debug, S, []) end. %%%========================================================================== @@ -510,7 +526,7 @@ system_continue(Parent, Debug, S) -> loop(Parent, Debug, S). system_terminate(Reason, _Parent, Debug, S) -> - terminate(Reason, Debug, S, []). + ?TERMINATE(Reason, Debug, S, []). system_code_change( #{module := Module, @@ -646,7 +662,7 @@ loop_receive(Parent, Debug, #{timer := Timer} = S) -> %% EXIT is not a 2-tuple and therefore %% not an event and has no event_type(), %% but this will stand out in the crash report... - terminate(Reason, Debug, S, [EXIT]); + ?TERMINATE(Reason, Debug, S, [EXIT]); {timeout,Timer,Content} when Timer =/= undefined -> loop_receive( Parent, Debug, S, {timeout,Content}, undefined); @@ -727,7 +743,7 @@ loop_events( loop_event_result(Parent, Debug, S, Events, Event, Result) -> case Result of {stop,Reason,NewStateData} -> - terminate( + ?TERMINATE( Reason, Debug, S#{state_data := NewStateData}, [Event|Events]); @@ -742,9 +758,10 @@ loop_event_result(Parent, Debug, S, Events, Event, Result) -> [Reply] end, BadReplies = - reply_then_terminate(Reason, Debug, NewS, Q, Replies), + reply_then_terminate( + exit, Reason, ?STACKTRACE(), Debug, NewS, Q, Replies), %% Since we got back here Replies was bad - terminate( + ?TERMINATE( {bad_return_value,{stop,Reason,BadReplies,NewStateData}}, Debug, NewS, Q); {next_state,NewState,NewStateData} -> @@ -757,7 +774,7 @@ loop_event_result(Parent, Debug, S, Events, Event, Result) -> Parent, Debug, S, Events, Event, NewState, NewStateData, StateOps); _ -> - terminate( + ?TERMINATE( {bad_return_value,Result}, Debug, S, [Event|Events]) end. @@ -815,14 +832,14 @@ loop_event_state_ops( postponed := P}, Q, Timer); [Reason,Debug] -> - terminate(Reason, Debug, S, [Event|Events]); + ?TERMINATE(Reason, Debug, S, [Event|Events]); [Class,Reason,Stacktrace,Debug] -> terminate( Class, Reason, Stacktrace, Debug, S, [Event|Events]) end; %% [Reason] -> - terminate(Reason, Debug0, S, [Event|Events]) + ?TERMINATE(Reason, Debug0, S, [Event|Events]) end. %%--------------------------------------------------------------------------- @@ -921,13 +938,15 @@ process_state_operations([Operation|Operations] = OOs, Debug, S, Q, P) -> end end. -reply_then_terminate(Reason, Debug, S, Q, []) -> - terminate(Reason, Debug, S, Q); -reply_then_terminate(Reason, Debug, S, Q, [R|Rs] = RRs) -> +reply_then_terminate(Class, Reason, Stacktrace, Debug, S, Q, []) -> + terminate(Class, Reason, Stacktrace, Debug, S, Q); +reply_then_terminate( + Class, Reason, Stacktrace, Debug, S, Q, [R|Rs] = RRs) -> case R of {reply,{_To,_Tag}=Client,Reply} -> NewDebug = do_reply(Debug, S, Client, Reply), - reply_then_terminate(Reason, NewDebug, S, Q, Rs); + reply_then_terminate( + Class, Reason, Stacktrace, NewDebug, S, Q, Rs); _ -> RRs % bad_return_value end. @@ -1034,12 +1053,9 @@ cancel_timer(TimerRef) -> end. -terminate(Reason, Debug, S, Q) -> - terminate(exit, Reason, [], Debug, S, Q). -%% terminate( Class, Reason, Stacktrace, Debug, - #{name := Name, module := Module, + #{module := Module, state := State, state_data := StateData} = S, Q) -> try Module:terminate(Reason, State, StateData) of @@ -1049,7 +1065,7 @@ terminate( C:R -> ST = erlang:get_stacktrace(), error_info( - C, R, ST, Debug, Name, Q, + C, R, ST, Debug, S, Q, format_status(terminate, get(), S)), erlang:raise(C, R, ST) end, @@ -1059,7 +1075,7 @@ terminate( {shutdown,_} -> ok; _ -> error_info( - Class, Reason, Stacktrace, Debug, Name, Q, + Class, Reason, Stacktrace, Debug, S, Q, format_status(terminate, get(), S)) end, case Stacktrace of @@ -1070,7 +1086,10 @@ terminate( end. error_info( - Class, Reason, Stacktrace, Debug, Name, Q, FmtStateData) -> + Class, Reason, Stacktrace, Debug, + #{name := Name, callback_mode := CallbackMode, + state := State, postponed := P}, + Q, FmtStateData) -> {FixedReason,FixedStacktrace} = case Stacktrace of [{M,F,Args,_}|ST] @@ -1079,7 +1098,13 @@ error_info( false -> {{'module could not be loaded',M},ST}; _ -> - Arity = length(Args), + Arity = + if + is_list(Args) -> + length(Args); + is_integer(Args) -> + Args + end, case erlang:function_exported(M, F, Arity) of true -> {Reason,Stacktrace}; @@ -1100,6 +1125,9 @@ error_info( end ++ "** When Server state = ~p~n" ++ "** Reason for termination = ~w:~p~n" ++ + "** State = ~p~n" ++ + "** Callback mode = ~p~n" ++ + "** Queued/Posponed = ~w/~w~n" ++ case FixedStacktrace of [] -> ""; @@ -1110,10 +1138,12 @@ error_info( [Name | case Q of [] -> - [FmtStateData,Class,FixedReason]; + []; [Event|_] -> - [Event,FmtStateData,Class,FixedReason] + [Event] end] ++ + [FmtStateData,Class,FixedReason, + State,CallbackMode,length(Q),length(P)] ++ case FixedStacktrace of [] -> []; -- cgit v1.2.3 From 972a8fcb57a64c9a0dc7587e9072c15758e36da2 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 18 Feb 2016 10:22:50 +0100 Subject: Rename insert_event -> next_event --- lib/stdlib/doc/src/gen_statem.xml | 130 ++++++++++++++++++++++------------- lib/stdlib/src/gen_statem.erl | 15 ++-- lib/stdlib/test/gen_statem_SUITE.erl | 2 +- 3 files changed, 91 insertions(+), 56 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index f608e10469..5de1812b41 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -110,11 +110,28 @@ erlang:'!' -----> Module:StateName/5 states so you do not accidentally postpone one event forever creating an infinite busy loop.

-

- An event can can be postponed causing it to be retried - after the state has changed, or more precisely; - after a state change all postponed events are retried - in the new state. +

The gen_statem enqueues incoming events in order of arrival + and presents these to the + state function + in that order. The state function can postpone an event + so it is not retried in the current state. + After a state change all enqueued events (including postponed) + are again presented to the state function. +

+

The + state function + can insert events using the + + state_operation() next_event + + and such an event is inserted as the next to present + to the state function. That is: as if it is + the oldest incoming event. There is a dedicated + + event_type() internal + + that can be used for such events making it impossible + to mistake for an external event.

A gen_statem handles system messages as documented in sys. @@ -302,7 +319,12 @@ erlang:'!' -----> Module:StateName/5 -

Option that only is valid when initializing the gen_statem

+

Option that only is valid when initializing the gen_statem + that is it can be returned from + Module:init/1 + or given to + enter_loop/5,6. +

@@ -332,7 +354,7 @@ erlang:'!' -----> Module:StateName/5

Either a state_option() - of which the first occurence + of which the last occurence in the containing list takes precedence, or a state_operation() @@ -340,42 +362,34 @@ erlang:'!' -----> Module:StateName/5 the containing list.

These may be returned from the - state function - or from Module:init/1. -

-

The gen_statem enqueues postponed events and - not yet processed events in order of arrival, except for - an event that a callback function inserts with - {insert_event,EventType,EventContent} that is inserted - as the next event to process. + state function, + from Module:init/1 + or given to + enter_loop/5,6.

-

When the state machine changes states all enqueued events - becomes not yet processed to be processed before the old - not yet processed events. In other words; the order of arrival - is retained. -

-

The processing order is:

+

The processing order for a state change is:

If the option retry is true - the current event is enqueued as postponed to be retried. + the current event is postponed. - If the state changes all postponed events - are transferred to not yet processed to be processed - before other not yet processed events. + If the state changes the queue of incoming events + is reset to start with the oldest postponed. All operations are processed in order of appearance. - The timeout option is processed if present. - So a state timer may be started or a timeout zero event - may be inserted as if just received. + The timeout option is processed if present, + so a state timer may be started or a timeout zero event + may be enqueued as the newest incoming. The (possibly new) state function - is called with the next not yet processed event - if there is any, otherwise the gen_statem goes into receive + is called with the oldest enqueued event if there is any, + otherwise the gen_statem goes into receive or hibernation (if the option hibernate is true) to wait for the next message. In hibernation the next - non-system event awakens the gen_statem. + non-system event awakens the gen_statem, or rather + the next incoming message awakens the gen_statem but if it + is a system event it goes back into hibernation. @@ -383,12 +397,21 @@ erlang:'!' -----> Module:StateName/5 +

If multiple state options of the same type are present + in the containing list these are set in the list order + and the last value is kept. +

retry {retry,Retry} If Retry =:= true or plain retry postpone the current event to be retried after a state change. + This option is ignored when returned from + Module:init/1 + or given to + enter_loop/5,6 + since there is no event to postpone in those cases. hibernate {hibernate,Hibernate} @@ -397,9 +420,9 @@ erlang:'!' -----> Module:StateName/5 proc_lib:hibernate/3 before receive to wait for a new event. - If there are not yet processed events the - hibernate operation is ignored as if an event - just arrived and awakened the gen_statem. + If there are enqueued events the hibernate operation + is ignored as if an event just arrived and awakened + the gen_statem. {timeout,Time,Msg} @@ -426,14 +449,20 @@ erlang:'!' -----> Module:StateName/5 +

The state operations are executed in the containing + list order. This matters for next_event where + the last one in the list will become the next event to present + to the state functions. Regarding the other operations + it is only for {remove_event,EventPredicate} + that the order may matter. +

- {insert_event,EventType,EventContent} + {next_event,EventType,EventContent} - Insert the given event as the next to process - before any other not yet processed events. + Insert the given event as the next to process. An event of type internal @@ -982,7 +1011,13 @@ erlang:'!' -----> Module:StateName/5 gen_statem:call/2, gen_statem:cast/2 or as a normal process message this function is called. - If the EventType is + If + callback_mode + is state_functions then Module:StateName/5 is called, + and if it is handle_event_function + then Module:handle_event/5 is called. +

+

If EventType is {call,Client} the client is waiting for a reply. The reply can be sent from this or from any other @@ -995,23 +1030,24 @@ erlang:'!' -----> Module:StateName/5 gen_statem:reply(Client, Reply) .

-

StateName is rarely useful. One exception is if - you call a common event handling function from your state +

StateName is useful in some odd cases for example + if you call a common event handling function from your state function then you might want to pass StateName.

-

PrevStateName and PrevState are rarely useful. - They can be used when you want to do something only at the - first event in a state. Note that when gen_statem enters - its first state this is set to a reference() since - that can not match the state. +

PrevStateName and PrevState are useful + in some odd cases for example when you want to do something + only at the first event in a state. + Note that when gen_statem enters its first state + this is set to a reference() + since that can not match equal to any state.

If this function returns with a new state that does not match equal (=/=) to the current state all postponed events will be retried in the new state.

See state_op() - for the operations that can be done by gen_statem - after returning from this function. + for options that can be set and operations that can be done + by gen_statem after returning from this function.

diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 782e027b29..52fde7df7b 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -88,7 +88,7 @@ %% These can occur multiple times and are executed in order %% of appearence in the state_op() list reply_operation() | - {'insert_event', % Insert event as the next to handle + {'next_event', % Insert event as the next to handle EventType :: event_type(), EventContent :: term()} | {'remove_event', % Remove the oldest matching (=:=) event @@ -469,7 +469,7 @@ enter(Module, Options, State, StateData, Server, InitOps, Parent) -> S#{callback_mode := CallbackMode}, [], {event,undefined}, State, StateData, - [{retry,false}|StateOps]); + StateOps++[{retry,false}]); [Reason] -> ?TERMINATE(Reason, Debug, S, []) end. @@ -846,10 +846,10 @@ loop_event_state_ops( %% Server helpers collect_init_options(InitOps) -> - collect_init_options(lists:reverse(InitOps), state_functions, []). + collect_init_options(InitOps, state_functions, []). %% Keep the last of each kind collect_init_options([], CallbackMode, StateOps) -> - {CallbackMode,StateOps}; + {CallbackMode,lists:reverse(StateOps)}; collect_init_options([InitOp|InitOps] = IOIOs, CallbackMode, StateOps) -> case InitOp of {callback_mode,Mode} @@ -864,12 +864,11 @@ collect_init_options([InitOp|InitOps] = IOIOs, CallbackMode, StateOps) -> end. collect_state_options(StateOps) -> - collect_state_options( - lists:reverse(StateOps), false, false, undefined, []). + collect_state_options(StateOps, false, false, undefined, []). %% Keep the last of each kind collect_state_options( [], Retry, Hibernate, Timeout, Operations) -> - {Retry,Hibernate,Timeout,Operations}; + {Retry,Hibernate,Timeout,lists:reverse(Operations)}; collect_state_options( [StateOp|StateOps] = SOSOs, Retry, Hibernate, Timeout, Operations) -> case StateOp of @@ -909,7 +908,7 @@ process_state_operations([Operation|Operations] = OOs, Debug, S, Q, P) -> {reply,{_To,_Tag}=Client,Reply} -> NewDebug = do_reply(Debug, S, Client, Reply), process_state_operations(Operations, NewDebug, S, Q, P); - {insert_event,Type,Content} -> + {next_event,Type,Content} -> case event_type(Type) of true -> process_state_operations( diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 8a96f0e8e2..facc36fb9f 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -1180,7 +1180,7 @@ timeout(timeout, idle, idle, timeout, {From,Time}) -> TRefC2 = erlang:start_timer(Time, self(), cancel2), {next_state,timeout2,{From,Time,TRef2}, [{cancel_timer, TRefC1}, - {insert_event,internal,{cancel_timer,TRefC2}}]}; + {next_event,internal,{cancel_timer,TRefC2}}]}; timeout(_, _, _, State, Data) -> {next_state,State,Data}. -- cgit v1.2.3 From fdd72bc94149c7e279b5c4fd5a040af1886a4c72 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 18 Feb 2016 11:17:42 +0100 Subject: Rename retry -> postpone --- lib/stdlib/doc/src/gen_statem.xml | 15 ++++++------- lib/stdlib/src/gen_statem.erl | 42 ++++++++++++++++++------------------ lib/stdlib/test/gen_statem_SUITE.erl | 11 ++++++++++ 3 files changed, 39 insertions(+), 29 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 5de1812b41..aeab09615c 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -369,7 +369,7 @@ erlang:'!' -----> Module:StateName/5

The processing order for a state change is:

- If the option retry is true + If the option postpone is true the current event is postponed. If the state changes the queue of incoming events @@ -402,10 +402,10 @@ erlang:'!' -----> Module:StateName/5 and the last value is kept.

- retry - {retry,Retry} - If Retry =:= true - or plain retry postpone the current event + postpone + {postpone,Postpone} + If Postpone =:= true + or plain postpone postpone the current event to be retried after a state change. This option is ignored when returned from Module:init/1 @@ -434,9 +434,8 @@ erlang:'!' -----> Module:StateName/5 event counts just like a new in this respect. If Time =:= infinity or Time =:= 0 no timer is started but for zero time the timeout - event is enqued as just received after all - other already received events. - Also note that it is not possible + event is immediately enqueued as the newest received. + Also note that it is not possible nor needed to cancel this timeout using the state_operation() diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 52fde7df7b..32174687cf 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -70,15 +70,15 @@ %% First NewState and NewStateData are set, %% then all state_operations() are executed in order of %% apperance. Postponing the current event is performed - %% (iff state_option() 'retry' is 'true'). + %% (iff state_option() 'postpone' is 'true'). %% Lastly pending events are processed or if there are %% no pending events the server goes into receive %% or hibernate (iff state_option() 'hibernate' is 'true') state_option() | state_operation(). -type state_option() :: %% The first of each kind in the state_op() list takes precedence - 'retry' | % Postpone the current event to a different (=/=) state - {'retry', Retry :: boolean()} | + 'postpone' | % Postpone the current event to a different (=/=) state + {'postpone', Postpone :: boolean()} | 'hibernate' | % Hibernate the server instead of going into receive {'hibernate', Hibernate :: boolean()} | (Timeout :: timeout()) | % {timeout,Timeout} @@ -469,7 +469,7 @@ enter(Module, Options, State, StateData, Server, InitOps, Parent) -> S#{callback_mode := CallbackMode}, [], {event,undefined}, State, StateData, - StateOps++[{retry,false}]); + StateOps++[{postpone,false}]); [Reason] -> ?TERMINATE(Reason, Debug, S, []) end. @@ -782,9 +782,9 @@ loop_event_state_ops( Parent, Debug0, #{state := State, postponed := P0} = S, Events, Event, NewState, NewStateData, StateOps) -> case collect_state_options(StateOps) of - {Retry,Hibernate,Timeout,Operations} -> - P1 = % Move current event to postponed if Retry - case Retry of + {Postpone,Hibernate,Timeout,Operations} -> + P1 = % Move current event to postponed if Postpone + case Postpone of true -> [Event|P0]; false -> @@ -804,9 +804,9 @@ loop_event_state_ops( NewDebug = sys_debug( Debug, S, - case Retry of + case Postpone of true -> - {retry,Event,NewState}; + {postpone,Event,NewState}; false -> {consume,Event,NewState} end), @@ -867,38 +867,38 @@ collect_state_options(StateOps) -> collect_state_options(StateOps, false, false, undefined, []). %% Keep the last of each kind collect_state_options( - [], Retry, Hibernate, Timeout, Operations) -> - {Retry,Hibernate,Timeout,lists:reverse(Operations)}; + [], Postpone, Hibernate, Timeout, Operations) -> + {Postpone,Hibernate,Timeout,lists:reverse(Operations)}; collect_state_options( - [StateOp|StateOps] = SOSOs, Retry, Hibernate, Timeout, Operations) -> + [StateOp|StateOps] = SOSOs, Postpone, Hibernate, Timeout, Operations) -> case StateOp of - retry -> + postpone -> collect_state_options( StateOps, true, Hibernate, Timeout, Operations); - {retry,NewRetry} when is_boolean(NewRetry) -> + {postpone,NewPostpone} when is_boolean(NewPostpone) -> collect_state_options( - StateOps, NewRetry, Hibernate, Timeout, Operations); - {retry,_} -> + StateOps, NewPostpone, Hibernate, Timeout, Operations); + {postpone,_} -> [{bad_state_ops,SOSOs}]; hibernate -> collect_state_options( - StateOps, Retry, true, Timeout, Operations); + StateOps, Postpone, true, Timeout, Operations); {hibernate,NewHibernate} when is_boolean(NewHibernate) -> collect_state_options( - StateOps, Retry, NewHibernate, Timeout, Operations); + StateOps, Postpone, NewHibernate, Timeout, Operations); {hibernate,_} -> [{bad_state_ops,SOSOs}]; {timeout,infinity,_} -> % Ignore since it will never time out collect_state_options( - StateOps, Retry, Hibernate, undefined, Operations); + StateOps, Postpone, Hibernate, undefined, Operations); {timeout,Time,_} = NewTimeout when is_integer(Time), Time >= 0 -> collect_state_options( - StateOps, Retry, Hibernate, NewTimeout, Operations); + StateOps, Postpone, Hibernate, NewTimeout, Operations); {timeout,_,_} -> [{bad_state_ops,SOSOs}]; _ -> % Collect others as operations collect_state_options( - StateOps, Retry, Hibernate, Timeout, [StateOp|Operations]) + StateOps, Postpone, Hibernate, Timeout, [StateOp|Operations]) end. process_state_operations([], Debug, _S, Q, P) -> diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index facc36fb9f..8b619b6a60 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -1032,7 +1032,10 @@ do_connect(STM) -> gen_statem:cast(STM, {connect,self()}), wfor(accept), check_state(STM, wfor_conf), + Tag = make_ref(), + gen_statem:cast(STM, {ping,self(),Tag}), gen_statem:cast(STM, confirm), + wfor({pong,Tag}), check_state(STM, connected), ok. @@ -1071,7 +1074,10 @@ do_sync_connect(STM) -> check_state(STM, idle), accept = gen_statem:call(STM, connect), check_state(STM, wfor_conf), + Tag = make_ref(), + gen_statem:cast(STM, {ping,self(),Tag}), yes = gen_statem:call(STM, confirm), + wfor({pong,Tag}), check_state(STM, connected), ok. @@ -1202,6 +1208,8 @@ timeout3(_, _, _, State, Data) -> wfor_conf({call,From}, confirm, _, _, Data) -> {next_state,connected,Data, [{reply,From,yes}]}; +wfor_conf(cast, {ping,_,_}, _, State, Data) -> + {next_state,State,Data,[postpone]}; wfor_conf(cast, confirm, _, _, Data) -> {next_state,connected,Data}; wfor_conf(Type, Content, PrevState, State, Data) -> @@ -1229,6 +1237,9 @@ connected({call,From}, disconnect, _, _, Data) -> [{reply,From,yes}]}; connected(cast, disconnect, _, _, Data) -> {next_state,idle,Data}; +connected(cast, {ping,Pid,Tag}, _, State, Data) -> + Pid ! {pong,Tag}, + {next_state,State,Data}; connected(Type, Content, PrevState, State, Data) -> case handle_common_events(Type, Content, PrevState, State, Data) of undefined -> -- cgit v1.2.3 From 82f34a7a9de85b4afc0dac4c9c426939264c5039 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 18 Feb 2016 16:06:57 +0100 Subject: Write some convenience helpers --- lib/stdlib/doc/src/gen_statem.xml | 23 +++++++++++++++++++++ lib/stdlib/src/gen_statem.erl | 39 +++++++++++++++++++++++++++++------- lib/stdlib/test/gen_statem_SUITE.erl | 24 +++++++++++----------- 3 files changed, 67 insertions(+), 19 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index aeab09615c..5fbedb12f8 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -534,6 +534,13 @@ erlang:'!' -----> Module:StateName/5 + + {stop,Reason} + + The same as + {stop,Reason,[],StateData} + but keeps the old StateData. + {stop,Reason,NewStateData} @@ -551,6 +558,19 @@ erlang:'!' -----> Module:StateName/5 Module:terminate/3 with Reason. + + + {next_state,StateOps} + + + The same as + + {next_state,State,StateData,StateOps} + but keeps the old State and StateData. + Believe it or not, but this one has actually + been proven useful to throw/1 from deep down + in the state logic. + {next_state,NewState,NewStateData} @@ -805,6 +825,7 @@ erlang:'!' -----> Module:StateName/5 + Send a reply to a client @@ -820,6 +841,8 @@ erlang:'!' -----> Module:StateName/5 {call,Client} argument to the state function. + Client and Reply + an also be specified using ReplyOperation.

A reply sent with this function will not be visible diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 32174687cf..486f61b1ed 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -25,7 +25,7 @@ stop/1,stop/3, cast/2,call/2,call/3, enter_loop/4,enter_loop/5,enter_loop/6, - reply/2]). + reply/1,reply/2]). %% gen callbacks -export( @@ -105,17 +105,21 @@ {'reply', % Reply to a client Client :: client(), Reply :: term()}. -type state_callback_result() :: - {stop, % Stop the server + {'stop', % Stop the server + Reason :: term()} | + {'stop', % Stop the server Reason :: term(), NewStateData :: state_data()} | - {stop, % Stop the server + {'stop', % Stop the server Reason :: term(), Replies :: [reply_operation()] | reply_operation(), NewStateData :: state_data()} | - {next_state, % {next_state,NewState,NewStateData,[]} + {'next_state', % {next_state,State,StateData,StateOps} % Actually useful + StateOps :: [state_op()] | state_op()} | + {'next_state', % {next_state,NewState,NewStateData,[]} NewState :: state(), NewStateData :: state_data()} | - {next_state, % State transition, maybe to the same state + {'next_state', % State transition, maybe to the same state NewState :: state(), NewStateData :: state_data(), StateOps :: [state_op()] | state_op()}. @@ -379,6 +383,10 @@ call(ServerRef, Request, Timeout) -> end. %% Reply from a state machine callback to whom awaits in call/2 +-spec reply(ReplyOperation :: reply_operation()) -> ok. +reply({reply,{_To,_Tag}=Client,Reply}) -> + reply(Client, Reply). +%% -spec reply(Client :: client(), Reply :: term()) -> ok. reply({To,Tag}, Reply) -> Msg = {Tag,Reply}, @@ -742,6 +750,8 @@ loop_events( %% Interpret all callback return value variants loop_event_result(Parent, Debug, S, Events, Event, Result) -> case Result of + {stop,Reason} -> + ?TERMINATE(Reason, Debug, S, [Event|Events]); {stop,Reason,NewStateData} -> ?TERMINATE( Reason, Debug, @@ -764,6 +774,11 @@ loop_event_result(Parent, Debug, S, Events, Event, Result) -> ?TERMINATE( {bad_return_value,{stop,Reason,BadReplies,NewStateData}}, Debug, NewS, Q); + {next_state,StateOps} -> + #{state := State, state_data := StateData} = S, + loop_event_state_ops( + Parent, Debug, S, Events, Event, + State, StateData, StateOps); {next_state,NewState,NewStateData} -> loop_event_state_ops( Parent, Debug, S, Events, Event, @@ -846,7 +861,12 @@ loop_event_state_ops( %% Server helpers collect_init_options(InitOps) -> - collect_init_options(InitOps, state_functions, []). + if + is_list(InitOps) -> + collect_init_options(InitOps, state_functions, []); + true -> + collect_init_options([InitOps], state_functions, []) + end. %% Keep the last of each kind collect_init_options([], CallbackMode, StateOps) -> {CallbackMode,lists:reverse(StateOps)}; @@ -864,7 +884,12 @@ collect_init_options([InitOp|InitOps] = IOIOs, CallbackMode, StateOps) -> end. collect_state_options(StateOps) -> - collect_state_options(StateOps, false, false, undefined, []). + if + is_list(StateOps) -> + collect_state_options(StateOps, false, false, undefined, []); + true -> + collect_state_options([StateOps], false, false, undefined, []) + end. %% Keep the last of each kind collect_state_options( [], Postpone, Hibernate, Timeout, Operations) -> diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 8b619b6a60..6249ada027 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -1158,11 +1158,10 @@ idle(cast, badreturn, _, _, _Data) -> badreturn; idle({call,_From}, badreturn, _, _, _Data) -> badreturn; -idle({call,From}, {delayed_answer,T}, _, State, Data) -> +idle({call,From}, {delayed_answer,T}, _, _, _) -> receive after T -> - {next_state,State,Data, - [{reply,From,delayed}]} + throw({next_state,{reply,From,delayed}}) end; idle({call,From}, {timeout,Time}, _, State, _Data) -> {next_state,timeout,{From,Time}, @@ -1172,9 +1171,10 @@ idle(Type, Content, PrevState, State, Data) -> undefined -> case Type of {call,From} -> - {next_state,State,Data,[{reply,From,'eh?'}]}; + throw({next_state,[{reply,From,'eh?'}]}); _ -> - {stop,{unexpected,State,PrevState,Type,Content},Data} + throw( + {stop,{unexpected,State,PrevState,Type,Content}}) end; Result -> Result @@ -1208,8 +1208,8 @@ timeout3(_, _, _, State, Data) -> wfor_conf({call,From}, confirm, _, _, Data) -> {next_state,connected,Data, [{reply,From,yes}]}; -wfor_conf(cast, {ping,_,_}, _, State, Data) -> - {next_state,State,Data,[postpone]}; +wfor_conf(cast, {ping,_,_}, _, _, _) -> + {next_state,[postpone]}; wfor_conf(cast, confirm, _, _, Data) -> {next_state,connected,Data}; wfor_conf(Type, Content, PrevState, State, Data) -> @@ -1220,7 +1220,7 @@ wfor_conf(Type, Content, PrevState, State, Data) -> {next_state,idle,Data, [{reply,From,'eh?'}]}; _ -> - {next_state,State,Data} + throw({next_state,[]}) end; Result -> Result @@ -1320,12 +1320,12 @@ handle_common_events(cast, {get,Pid}, _, State, Data) -> {next_state,State,Data}; handle_common_events({call,From}, stop, _, _, Data) -> {stop,normal,[{reply,From,stopped}],Data}; -handle_common_events(cast, stop, _, _, Data) -> - {stop,normal,Data}; +handle_common_events(cast, stop, _, _, _) -> + {stop,normal}; handle_common_events({call,From}, {stop,Reason}, _, _, Data) -> {stop,Reason,{reply,From,stopped},Data}; -handle_common_events(cast, {stop,Reason}, _, _, Data) -> - {stop,Reason,Data}; +handle_common_events(cast, {stop,Reason}, _, _, _) -> + {stop,Reason}; handle_common_events({call,From}, 'alive?', _, State, Data) -> {next_state,State,Data, [{reply,From,yes}]}; -- cgit v1.2.3 From 26a7af61fbffae90c0968d945ae8b146582ba068 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 18 Feb 2016 16:38:57 +0100 Subject: Change initial PrevState to 'undefined' --- lib/stdlib/doc/src/gen_statem.xml | 27 ++++++++++++++++++++++++--- lib/stdlib/src/gen_statem.erl | 7 ++++--- 2 files changed, 28 insertions(+), 6 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 5fbedb12f8..9d98763973 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -110,6 +110,14 @@ erlang:'!' -----> Module:StateName/5 states so you do not accidentally postpone one event forever creating an infinite busy loop.

+

Any state name or any state value (depending on + callback_mode) + is permitted with a small gotcha regarding the state + undefined that is used as the previous state when + the first gen_statem state function is called. + You might need to know about this faked state if you + inspect the previous state argument in your state functions. +

The gen_statem enqueues incoming events in order of arrival and presents these to the state function @@ -118,6 +126,12 @@ erlang:'!' -----> Module:StateName/5 After a state change all enqueued events (including postponed) are again presented to the state function.

+

The gen_statem event queue model is sufficient to emulate + the normal process message queue and selective receive + with postponing an event corresponding to not matching + it in a receive statement and changing states corresponding + to entering a new receive statement. +

The state function can insert events using the @@ -133,6 +147,15 @@ erlang:'!' -----> Module:StateName/5 that can be used for such events making it impossible to mistake for an external event.

+

Inserting an event replaces the trick of calling your own + state handling functions that you often would have to + resort to in e.g gen_fsm + to force processing a faked event before others. + If you for example in gen_statem postpone an event + in one state and then call some other state function of yours, + you have not changed states and hence the postponed event + will not be retried, which is logical but might be confusing. +

A gen_statem handles system messages as documented in sys. The sys module @@ -1011,7 +1034,6 @@ erlang:'!' -----> Module:StateName/5 EventContent = term() PrevStateName = state_name() - | reference() StateName = state_name() @@ -1060,8 +1082,7 @@ erlang:'!' -----> Module:StateName/5 in some odd cases for example when you want to do something only at the first event in a state. Note that when gen_statem enters its first state - this is set to a reference() - since that can not match equal to any state. + this is set to undefined.

If this function returns with a new state that does not match equal (=/=) to the current state diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 486f61b1ed..e03e22b087 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -459,13 +459,13 @@ do_send(Proc, Msg) -> enter(Module, Options, State, StateData, Server, InitOps, Parent) -> Name = gen:get_proc_name(Server), Debug = gen:debug_options(Name, Options), - PrevState = make_ref(), + PrevState = undefined, S = #{ callback_mode => state_functions, module => Module, name => Name, prev_state => PrevState, - state => PrevState, + state => PrevState, % Will be discarded by loop_event_state_ops state_data => StateData, timer => undefined, postponed => [], @@ -475,7 +475,8 @@ enter(Module, Options, State, StateData, Server, InitOps, Parent) -> loop_event_state_ops( Parent, Debug, S#{callback_mode := CallbackMode}, - [], {event,undefined}, + [], + {event,undefined}, % Will be discarded by {postpone,false} State, StateData, StateOps++[{postpone,false}]); [Reason] -> -- cgit v1.2.3 From 65767cfc36cf8658b45d68b3c17d4bd612198165 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 19 Feb 2016 09:22:52 +0100 Subject: Add {keep_state,...} and {keep_state_and_data,...} --- lib/stdlib/doc/src/gen_statem.xml | 61 ++++++++++++++++++++++++++---------- lib/stdlib/src/gen_statem.erl | 46 +++++++++++++++++++-------- lib/stdlib/test/gen_statem_SUITE.erl | 44 +++++++++++++++----------- 3 files changed, 102 insertions(+), 49 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 9d98763973..f66f399137 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -475,8 +475,9 @@ erlang:'!' -----> Module:StateName/5 list order. This matters for next_event where the last one in the list will become the next event to present to the state functions. Regarding the other operations - it is only for {remove_event,EventPredicate} - that the order may matter. + it is only for remove_event with + EventPredicate + and for reply_operation() that the order may matter.

@@ -581,19 +582,6 @@ erlang:'!' -----> Module:StateName/5 Module:terminate/3 with Reason.
- - - {next_state,StateOps} - - - The same as - - {next_state,State,StateData,StateOps} - but keeps the old State and StateData. - Believe it or not, but this one has actually - been proven useful to throw/1 from deep down - in the state logic. - {next_state,NewState,NewStateData} @@ -614,8 +602,47 @@ erlang:'!' -----> Module:StateName/5 The gen_statem will do a state transition to NewState - (which may be the same as State) - and execute all StateOps + (which may be the same as the current state) + and execute all StateOps + + + + {keep_state,NewStateData}} + + + The same as + + {keep_state,NewStateData,[]} + + + + + {keep_state,NewStateData,StateOps} + + + The gen_statem will keep the current state, or + do a state transition to the current state if you like, + and execute all StateOps + + + + {keep_state_and_data} + + + The same as + + {keep_state_and_data,[]} + + + + + {keep_state_and_data,StateOps} + + + The gen_statem will keep the current state, or + do a state transition to the current state if you like, + also keep the current state data, + and execute all StateOps
diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index e03e22b087..3da4443f13 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -114,14 +114,20 @@ Reason :: term(), Replies :: [reply_operation()] | reply_operation(), NewStateData :: state_data()} | - {'next_state', % {next_state,State,StateData,StateOps} % Actually useful - StateOps :: [state_op()] | state_op()} | {'next_state', % {next_state,NewState,NewStateData,[]} NewState :: state(), NewStateData :: state_data()} | {'next_state', % State transition, maybe to the same state NewState :: state(), NewStateData :: state_data(), + StateOps :: [state_op()] | state_op()} | + {'keep_state', % {keep_state,NewStateData,[]} + NewStateData :: state_data()} | + {'keep_state', + NewStateData :: state_data(), + StateOps :: [state_op()] | state_op()} | + {'keep_state_and_data'} | % {keep_state_and_data,[]} + {'keep_state_and_data', StateOps :: [state_op()] | state_op()}. %% The state machine init function. It is called only once and @@ -477,7 +483,7 @@ enter(Module, Options, State, StateData, Server, InitOps, Parent) -> S#{callback_mode := CallbackMode}, [], {event,undefined}, % Will be discarded by {postpone,false} - State, StateData, + PrevState, State, StateData, StateOps++[{postpone,false}]); [Reason] -> ?TERMINATE(Reason, Debug, S, []) @@ -749,7 +755,10 @@ loop_events( end. %% Interpret all callback return value variants -loop_event_result(Parent, Debug, S, Events, Event, Result) -> +loop_event_result( + Parent, Debug, + #{state := State, state_data := StateData} = S, + Events, Event, Result) -> case Result of {stop,Reason} -> ?TERMINATE(Reason, Debug, S, [Event|Events]); @@ -775,28 +784,39 @@ loop_event_result(Parent, Debug, S, Events, Event, Result) -> ?TERMINATE( {bad_return_value,{stop,Reason,BadReplies,NewStateData}}, Debug, NewS, Q); - {next_state,StateOps} -> - #{state := State, state_data := StateData} = S, - loop_event_state_ops( - Parent, Debug, S, Events, Event, - State, StateData, StateOps); {next_state,NewState,NewStateData} -> loop_event_state_ops( Parent, Debug, S, Events, Event, - NewState, NewStateData, []); + State, NewState, NewStateData, []); {next_state,NewState,NewStateData,StateOps} when is_list(StateOps) -> loop_event_state_ops( Parent, Debug, S, Events, Event, - NewState, NewStateData, StateOps); + State, NewState, NewStateData, StateOps); + {keep_state,NewStateData} -> + loop_event_state_ops( + Parent, Debug, S, Events, Event, + State, State, NewStateData, []); + {keep_state,NewStateData,StateOps} -> + loop_event_state_ops( + Parent, Debug, S, Events, Event, + State, State, NewStateData, StateOps); + {keep_state_and_data} -> + loop_event_state_ops( + Parent, Debug, S, Events, Event, + State, State, StateData, []); + {keep_state_and_data,StateOps} -> + loop_event_state_ops( + Parent, Debug, S, Events, Event, + State, State, StateData, StateOps); _ -> ?TERMINATE( {bad_return_value,Result}, Debug, S, [Event|Events]) end. loop_event_state_ops( - Parent, Debug0, #{state := State, postponed := P0} = S, Events, Event, - NewState, NewStateData, StateOps) -> + Parent, Debug0, #{postponed := P0} = S, Events, Event, + State, NewState, NewStateData, StateOps) -> case collect_state_options(StateOps) of {Postpone,Hibernate,Timeout,Operations} -> P1 = % Move current event to postponed if Postpone diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 6249ada027..92dc59e843 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -1158,10 +1158,10 @@ idle(cast, badreturn, _, _, _Data) -> badreturn; idle({call,_From}, badreturn, _, _, _Data) -> badreturn; -idle({call,From}, {delayed_answer,T}, _, _, _) -> +idle({call,From}, {delayed_answer,T}, _, _, Data) -> receive after T -> - throw({next_state,{reply,From,delayed}}) + throw({keep_state,Data,{reply,From,delayed}}) end; idle({call,From}, {timeout,Time}, _, State, _Data) -> {next_state,timeout,{From,Time}, @@ -1171,7 +1171,7 @@ idle(Type, Content, PrevState, State, Data) -> undefined -> case Type of {call,From} -> - throw({next_state,[{reply,From,'eh?'}]}); + throw({keep_state,Data,[{reply,From,'eh?'}]}); _ -> throw( {stop,{unexpected,State,PrevState,Type,Content}}) @@ -1209,7 +1209,7 @@ wfor_conf({call,From}, confirm, _, _, Data) -> {next_state,connected,Data, [{reply,From,yes}]}; wfor_conf(cast, {ping,_,_}, _, _, _) -> - {next_state,[postpone]}; + {keep_state_and_data,[postpone]}; wfor_conf(cast, confirm, _, _, Data) -> {next_state,connected,Data}; wfor_conf(Type, Content, PrevState, State, Data) -> @@ -1220,7 +1220,7 @@ wfor_conf(Type, Content, PrevState, State, Data) -> {next_state,idle,Data, [{reply,From,'eh?'}]}; _ -> - throw({next_state,[]}) + throw({keep_state_and_data}) end; Result -> Result @@ -1341,26 +1341,32 @@ handle_common_events(_, _, _, _, _) -> %% states. Note that the state from init/1 is not wrapped %% so both atom and non-atom states are tested. handle_event(Type, Event, PrevState, State, Data) -> - PrevStateName = unwrap(PrevState), - StateName = unwrap(State), - case - ?MODULE:StateName( + PrevStateName = unwrap_state(PrevState), + StateName = unwrap_state(State), + try ?MODULE:StateName( Type, Event, PrevStateName, StateName, Data) of - {next_state,NewState,NewStateData} -> - {next_state,wrap(NewState),NewStateData}; - {next_state,NewState,NewStateData,StateOps} -> - {next_state,wrap(NewState),NewStateData,StateOps}; - Other -> - Other + Result -> + wrap_result(Result) + catch + throw:Result -> + erlang:raise( + throw, wrap_result(Result), erlang:get_stacktrace()) end. -unwrap([State]) -> +unwrap_state([State]) -> State; -unwrap(State) -> +unwrap_state(State) -> State. -wrap(State) -> - [State]. +wrap_result(Result) -> + case Result of + {next_state,NewState,NewStateData} -> + {next_state,[NewState],NewStateData}; + {next_state,NewState,NewStateData,StateOps} -> + {next_state,[NewState],NewStateData,StateOps}; + Other -> + Other + end. -- cgit v1.2.3 From fc1e649e3613f18dec8514921d0439ddcca73bdb Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 19 Feb 2016 09:39:46 +0100 Subject: Add reply([Reply]) --- lib/stdlib/doc/src/gen_statem.xml | 5 ++++- lib/stdlib/src/gen_statem.erl | 7 +++++-- lib/stdlib/test/gen_statem_SUITE.erl | 10 ++++++---- 3 files changed, 15 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index f66f399137..5f9ec6736e 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -892,7 +892,10 @@ erlang:'!' -----> Module:StateName/5 argument to the state function. Client and Reply - an also be specified using ReplyOperation. + can also be specified using a + + reply_operation() + and multiple replies with a list of them.

A reply sent with this function will not be visible diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 3da4443f13..8aa8afd091 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -389,9 +389,12 @@ call(ServerRef, Request, Timeout) -> end. %% Reply from a state machine callback to whom awaits in call/2 --spec reply(ReplyOperation :: reply_operation()) -> ok. +-spec reply([reply_operation()] | reply_operation()) -> ok. reply({reply,{_To,_Tag}=Client,Reply}) -> - reply(Client, Reply). + reply(Client, Reply); +reply(Replies) when is_list(Replies) -> + [reply(Reply) || Reply <- Replies], + ok. %% -spec reply(Client :: client(), Reply :: term()) -> ok. reply({To,Tag}, Reply) -> diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 92dc59e843..4ac4acd189 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -1153,7 +1153,8 @@ idle(cast, {connect,Pid}, _, _, Data) -> Pid ! accept, {next_state,wfor_conf,Data}; idle({call,From}, connect, _, _, Data) -> - {next_state,wfor_conf,Data,[{reply,From,accept}]}; + gen_statem:reply(From, accept), + {next_state,wfor_conf,Data}; idle(cast, badreturn, _, _, _Data) -> badreturn; idle({call,_From}, badreturn, _, _, _Data) -> @@ -1161,7 +1162,8 @@ idle({call,_From}, badreturn, _, _, _Data) -> idle({call,From}, {delayed_answer,T}, _, _, Data) -> receive after T -> - throw({keep_state,Data,{reply,From,delayed}}) + gen_statem:reply({reply,From,delayed}), + throw({keep_state,Data}) end; idle({call,From}, {timeout,Time}, _, State, _Data) -> {next_state,timeout,{From,Time}, @@ -1200,8 +1202,8 @@ timeout2(_, _, _, State, Data) -> {next_state,State,Data}. timeout3(info, {timeout,TRef2,Result}, _, _, {From,TRef2}) -> - {next_state,idle,state, - [{reply,From,Result}]}; + gen_statem:reply([{reply,From,Result}]), + {next_state,idle,state}; timeout3(_, _, _, State, Data) -> {next_state,State,Data}. -- cgit v1.2.3 From 8cc0a419f847103199524e3d922a1087d23d8d8a Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 19 Feb 2016 10:31:03 +0100 Subject: Shorten taglists in the documentation --- lib/stdlib/doc/src/gen_statem.xml | 193 +++++++++++--------------------------- 1 file changed, 57 insertions(+), 136 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 5f9ec6736e..9cddce98da 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -207,19 +207,19 @@ erlang:'!' -----> Module:StateName/5 above.

It can be:

- - the pid(), - Name, - if the gen_statem is locally registered, - - {Name,Node}, - if the gen_statem is locally registered at another node, or + + pid() + LocalName + The gen_statem is locally registered. + Name, Node + The gen_statem is locally registered on another node. - {global,GlobalName}, - if the gen_statem is globally registered. + GlobalName + The gen_statem is globally registered + in global. - {via,RegMod,ViaName}, - if the gen_statem is registered through + RegMod, ViaName + The gen_statem is registered through an alternative process registry. The registry callback module RegMod should export the functions @@ -230,7 +230,7 @@ erlang:'!' -----> Module:StateName/5 Thus, {via,global,GlobalName} is the same as {global,GlobalName}. - +
@@ -268,9 +268,10 @@ erlang:'!' -----> Module:StateName/5

Client address to use when replying through for example the - state_op() - {reply,Client,Reply} to a client - that has called the gen_statem server using + + state_op() {reply,Client,Reply} + + to a client that has called the gen_statem server using call/2.

@@ -426,7 +427,6 @@ erlang:'!' -----> Module:StateName/5

postpone - {postpone,Postpone} If Postpone =:= true or plain postpone postpone the current event to be retried after a state change. @@ -437,7 +437,6 @@ erlang:'!' -----> Module:StateName/5 since there is no event to postpone in those cases. hibernate - {hibernate,Hibernate} If Hibernate =:= true or plain hibernate hibernate the gen_statem by calling @@ -447,17 +446,16 @@ erlang:'!' -----> Module:StateName/5 is ignored as if an event just arrived and awakened the gen_statem. - - {timeout,Time,Msg} + timeout Generate an event of type timeout after Time milliseconds unless some other event is received before that time. Note that a retried event counts just like a new in this respect. - If Time =:= infinity or Time =:= 0 - no timer is started but for zero time the timeout - event is immediately enqueued as the newest received. + If Time =:= infinity no timer is started. + If Time =:= 0 the timeout event + is immediately enqueued as the newest received. Also note that it is not possible nor needed to cancel this timeout using the @@ -480,58 +478,45 @@ erlang:'!' -----> Module:StateName/5 and for reply_operation() that the order may matter.

- - - {next_event,EventType,EventContent} - - - Insert the given event as the next to process. + next_event + Insert the given EventType + and EventContent the next to process. An event of type internal should be used when you want to reliably distinguish an event inserted this way from any external event. - - - {remove_event,EventType,EventContent} - - + remove_event Remove the oldest queued event - that matches equal to the given event. - - - - {remove_event,EventPredicate} - - - Remove the oldest queued event for which - the EventPredicate returns true. + that matches equal to EventType + and EventContent or for which + EventPredicate returns true. - {cancel_timer,TimerRef} - Uses TimerRef when calling + cancel_timer + Cancel the timer by calling erlang:cancel_timer/2 - to cancel a timer, cleans the gen_statem's - message queue from any late timeout message from - the timer, and removes any late timeout message - from the queued events using +
with TimerRef, + clean the process message queue from any late timeout message, + and removes any late timeout message + from the gen_statem event queue using {remove_event,EventPredicate} above. This is a convenience function that saves quite some lines of code and testing time over doing it from the primitives mentioned above.
- {demonitor,MonitorRef} - Like {cancel_timer,_} above but for + demonitor + Like cancel_timer above but for demonitor/2 - . + with MonitorRef. {unlink,Id} Like {cancel_timer,_} above but for unlink/1 - . + with Id.
@@ -539,109 +524,45 @@ erlang:'!' -----> Module:StateName/5 - - - {reply,Client,Reply} - - Reply to a client that called - call/2. - Client must be the term from the - - {call,Client} - argument to the - state function. - - +

Reply to a client that called + call/2. + Client must be the term from the + + {call,Client} + argument to the + state function. +

- - {stop,Reason} - - The same as - {stop,Reason,[],StateData} - but keeps the old StateData. - - - {stop,Reason,NewStateData} - - The same as - {stop,Reason,[],NewStateData} - - {stop, - Reason, - Replies, - NewStateData} - - The gen_statem will first send all - Replies and then terminate by calling + stop + Send all Replies if given, + then terminate the gen_statem by calling Module:terminate/3 - with Reason. + with Reason and + NewStateData, if given. - - - {next_state,NewState,NewStateData} - - - The same as - - {next_state,NewState,NewStateData,[]} - - - - - {next_state, - NewState, - NewStateData, - StateOps} - - + next_state The gen_statem will do a state transition to NewState - (which may be the same as the current state) + (which may be the same as the current state), + set NewStateData and execute all StateOps - - - {keep_state,NewStateData}} - - - The same as - - {keep_state,NewStateData,[]} - - - - - {keep_state,NewStateData,StateOps} - - + keep_state The gen_statem will keep the current state, or do a state transition to the current state if you like, + set NewStateData and execute all StateOps - - - {keep_state_and_data} - - - The same as - - {keep_state_and_data,[]} - - - - - {keep_state_and_data,StateOps} - - + keep_state_and_data The gen_statem will keep the current state, or do a state transition to the current state if you like, - also keep the current state data, + keep the current state data, and execute all StateOps -- cgit v1.2.3 From 4d903e7e0f12d40461efda84ee169e8e65cf4c71 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 19 Feb 2016 10:47:01 +0100 Subject: Write gen_statem with code font in the docs --- lib/stdlib/doc/src/gen_statem.xml | 253 ++++++++++++++++++++------------------ 1 file changed, 134 insertions(+), 119 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 9cddce98da..c685d24b78 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -40,14 +40,14 @@ callback function for all states.

- A generic state machine process (gen_statem) implemented using - this module will have a standard set of interface functions + A generic state machine process (gen_statem) implemented + using this module will have a standard set of interface functions and include functionality for tracing and error reporting. It will also fit into an OTP supervision tree. Refer to OTP Design Principles for more information.

-

A gen_statem assumes all specific parts to be located in a +

A gen_statem assumes all specific parts to be located in a callback module exporting a pre-defined set of functions. The relationship between the behaviour functions and the callback functions can be illustrated as follows:

@@ -74,17 +74,17 @@ erlang:'!' -----> Module:StateName/5 and how to respond.

If a callback function fails or returns a bad value, - the gen_statem will terminate. An exception of class + the gen_statem will terminate. An exception of class throw, however, is not regarded as an error but as a valid return.

The "state function" for a specific state - in a gen_statem is the callback function that is called + in a gen_statem is the callback function that is called for all events in this state, and is selected depending on callback_mode - that the implementation selects during gen_statem init. + that the implementation selects during gen_statem init.

When callback_mode @@ -114,11 +114,11 @@ erlang:'!' -----> Module:StateName/5 callback_mode) is permitted with a small gotcha regarding the state undefined that is used as the previous state when - the first gen_statem state function is called. + the first gen_statem state function is called. You might need to know about this faked state if you inspect the previous state argument in your state functions.

-

The gen_statem enqueues incoming events in order of arrival +

The gen_statem enqueues incoming events in order of arrival and presents these to the state function in that order. The state function can postpone an event @@ -126,8 +126,8 @@ erlang:'!' -----> Module:StateName/5 After a state change all enqueued events (including postponed) are again presented to the state function.

-

The gen_statem event queue model is sufficient to emulate - the normal process message queue and selective receive +

The gen_statem event queue model is sufficient + to emulate the normal process message queue and selective receive with postponing an event corresponding to not matching it in a receive statement and changing states corresponding to entering a new receive statement. @@ -151,23 +151,25 @@ erlang:'!' -----> Module:StateName/5 state handling functions that you often would have to resort to in e.g gen_fsm to force processing a faked event before others. - If you for example in gen_statem postpone an event + If you for example in gen_statem postpone an event in one state and then call some other state function of yours, you have not changed states and hence the postponed event will not be retried, which is logical but might be confusing.

-

A gen_statem handles system messages as documented in +

A gen_statem handles system messages as documented in sys. The sys module - can be used for debugging a gen_statem. + can be used for debugging a gen_statem.

-

Note that a gen_statem does not trap exit signals automatically, - this must be explicitly initiated by the callback module. +

Note that a gen_statem does not trap exit signals + automatically, this must be explicitly initiated by + the callback module.

Unless otherwise stated, all functions in this module fail if - the specified gen_statem does not exist or if bad arguments are given. + the specified gen_statem does not exist or + if bad arguments are given.

-

The gen_statem process can go into hibernation (see +

The gen_statem process can go into hibernation (see erlang:hibernate/3 ) if a @@ -187,7 +189,8 @@ erlang:'!' -----> Module:StateName/5 -

Name specification to use when starting a gen_statem server. +

Name specification to use when starting + agen_statem server. See start_link/3 and @@ -200,7 +203,8 @@ erlang:'!' -----> Module:StateName/5 -

Server specification to use when addressing a gen_statem server. +

Server specification to use when addressing + a gen_statem server. See call/2 and server_name() @@ -210,16 +214,17 @@ erlang:'!' -----> Module:StateName/5 pid() LocalName - The gen_statem is locally registered. + The gen_statem is locally registered. Name, Node - The gen_statem is locally registered on another node. + The gen_statem is locally registered + on another node. GlobalName - The gen_statem is globally registered + The gen_statem is globally registered in global. RegMod, ViaName - The gen_statem is registered through + The gen_statem is registered through an alternative process registry. The registry callback module RegMod should export the functions @@ -237,7 +242,7 @@ erlang:'!' -----> Module:StateName/5

Debug option that can be used when starting - a gen_statem server through for example + a gen_statem server through for example enter_loop/4.

For every entry in Dbgs @@ -250,7 +255,7 @@ erlang:'!' -----> Module:StateName/5

Options that can be used when starting - a gen_statem server through for example + a gen_statem server through for example start_link/3.

@@ -271,7 +276,7 @@ erlang:'!' -----> Module:StateName/5 state_op() {reply,Client,Reply} - to a client that has called the gen_statem server using + to a client that has called the gen_statem server using call/2.

@@ -316,7 +321,7 @@ erlang:'!' -----> Module:StateName/5 originate from the corresponding API functions. For calls the event contain whom to reply to. Type info originates from normal messages sent - to the gen_statem process. + to the gen_statem process. It is also possible for the state machine implementation to insert events to itself, in particular of types @@ -343,8 +348,8 @@ erlang:'!' -----> Module:StateName/5 -

Option that only is valid when initializing the gen_statem - that is it can be returned from +

Option that only is valid when initializing + the gen_statem that is it can be returned from Module:init/1 or given to enter_loop/5,6. @@ -408,12 +413,12 @@ erlang:'!' -----> Module:StateName/5 The (possibly new) state function is called with the oldest enqueued event if there is any, - otherwise the gen_statem goes into receive + otherwise the gen_statem goes into receive or hibernation (if the option hibernate is true) to wait for the next message. In hibernation the next - non-system event awakens the gen_statem, or rather - the next incoming message awakens the gen_statem but if it - is a system event it goes back into hibernation. + non-system event awakens the gen_statem, or rather + the next incoming message awakens the gen_statem + but if it is a system event it goes back into hibernation. @@ -438,13 +443,14 @@ erlang:'!' -----> Module:StateName/5 hibernate If Hibernate =:= true - or plain hibernate hibernate the gen_statem by calling + or plain hibernate hibernate the gen_statem + by calling proc_lib:hibernate/3 before receive to wait for a new event. If there are enqueued events the hibernate operation is ignored as if an event just arrived and awakened - the gen_statem. + the gen_statem. timeout @@ -500,7 +506,7 @@ erlang:'!' -----> Module:StateName/5 with TimerRef, clean the process message queue from any late timeout message, and removes any late timeout message - from the gen_statem event queue using + from the gen_statem event queue using {remove_event,EventPredicate} above. This is a convenience function that saves quite some lines of code and testing time over doing it from @@ -540,27 +546,27 @@ erlang:'!' -----> Module:StateName/5 stop Send all Replies if given, - then terminate the gen_statem by calling + then terminate the gen_statem by calling Module:terminate/3 with Reason and NewStateData, if given. next_state - The gen_statem will do a state transition to + The gen_statem will do a state transition to NewState (which may be the same as the current state), set NewStateData and execute all StateOps keep_state - The gen_statem will keep the current state, or + The gen_statem will keep the current state, or do a state transition to the current state if you like, set NewStateData and execute all StateOps keep_state_and_data - The gen_statem will keep the current state, or + The gen_statem will keep the current state, or do a state transition to the current state if you like, keep the current state data, and execute all StateOps @@ -575,17 +581,18 @@ erlang:'!' -----> Module:StateName/5 - Create a linked gen_statem process + Create a linked gen_statem process -

Creates a gen_statem process according to OTP design principles +

Creates a gen_statem process according + to OTP design principles (using proc_lib primitives) that is linked to the calling process. - This is essential when the gen_statem shall be part of + This is essential when the gen_statem shall be part of a supervision tree so it gets linked to its supervisor.

-

The gen_statem process calls +

The gen_statem process calls Module:init/1 to initialize the server. To ensure a synchronized start-up procedure, start_link/3,4 does not return until @@ -595,10 +602,10 @@ erlang:'!' -----> Module:StateName/5

ServerName specifies the server_name() - to register for the gen_statem. - If the gen_statem is started with start_link/3 + to register for the gen_statem. + If the gen_statem is started with start_link/3 no ServerName is provided and - the gen_statem is not registered. + the gen_statem is not registered.

Module is the name of the callback module.

Args is an arbitrary term which is passed as @@ -607,9 +614,9 @@ erlang:'!' -----> Module:StateName/5 .

If the option {timeout,Time} is present in - Options, the gen_statem is allowed to spend - Time milliseconds initializing or it will be - terminated and the start function will return + Options, the gen_statem + is allowed to spend Time milliseconds initializing + or it will be terminated and the start function will return {error,timeout} . @@ -624,18 +631,19 @@ erlang:'!' -----> Module:StateName/5 as option list to the spawn_opt BIF which is used to spawn - the gen_statem. + the gen_statem.

Using the spawn option monitor is currently not allowed, but will cause this function to fail with reason badarg.

-

If the gen_statem is successfully created and initialized - this function returns +

If the gen_statem is successfully created + and initialized this function returns {ok,Pid}, - where Pid is the pid() of the gen_statem. + where Pid is the pid() + of the gen_statem. If there already exists a process with the specified ServerName this function returns @@ -669,9 +677,9 @@ erlang:'!' -----> Module:StateName/5 - Create a stand-alone gen_statem process + Create a stand-alone gen_statem process -

Creates a stand-alone gen_statem process according to +

Creates a stand-alone gen_statem process according to OTP design principles (using proc_lib primitives). @@ -700,12 +708,12 @@ erlang:'!' -----> Module:StateName/5 Synchronously stop a generic server -

Orders the gen_statem +

Orders the gen_statem ServerRef to exit with the given Reason and waits for it to terminate. - The gen_statem will call + The gen_statem will call Module:terminate/3 before exiting. @@ -735,14 +743,14 @@ erlang:'!' -----> Module:StateName/5 - Make a synchronous call to a gen_statem + Make a synchronous call to a gen_statem -

Makes a synchronous call to the gen_statem +

Makes a synchronous call to the gen_statem ServerRef by sending a request and waiting until its reply arrives. - The gen_statem will call the + The gen_statem will call the state function with event_type() {call,Client} and event content @@ -771,7 +779,7 @@ erlang:'!' -----> Module:StateName/5

-

The call may fail for example if the gen_statem dies +

The call may fail for example if the gen_statem dies before or during this function call.

@@ -779,14 +787,15 @@ erlang:'!' -----> Module:StateName/5 - Send an asynchronous event to a gen_statem + Send an asynchronous event to a gen_statem -

Sends an asynchronous event to the gen_statem +

Sends an asynchronous event to the gen_statem ServerRef and returns ok immediately, - ignoring if the destination node or gen_statem does not exist. - The gen_statem will call the + ignoring if the destination node or gen_statem + does not exist. + The gen_statem will call the state function with event_type() cast and event content @@ -800,8 +809,8 @@ erlang:'!' -----> Module:StateName/5 Send a reply to a client -

This function can be used by a gen_statem to explicitly send - a reply to a client that called +

This function can be used by a gen_statem + to explicitly send a reply to a client that called call/2 when the reply cannot be defined in the return value of the @@ -828,7 +837,7 @@ erlang:'!' -----> Module:StateName/5 - Enter the gen_statem receive loop + Enter the gen_statem receive loop

The same as enter_loop/6 @@ -841,7 +850,7 @@ erlang:'!' -----> Module:StateName/5 - Enter the gen_statem receive loop + Enter the gen_statem receive loop

If Server_or_StateOps is a list() the same as @@ -862,25 +871,27 @@ erlang:'!' -----> Module:StateName/5 - Enter the gen_statem receive loop + Enter the gen_statem receive loop -

Makes an the calling process become a gen_statem. Does not return, - instead the calling process will enter the gen_statem receive - loop and become a gen_statem server. The process - must have been started using one of the start - functions in +

Makes an the calling process become a gen_statem. + Does not return, instead the calling process will enter + the gen_statem receive loop and become + a gen_statem server. + The process must have been started + using one of the start functions in proc_lib. The user is responsible for any initialization of the process, including registering a name for it.

This function is useful when a more complex initialization - procedure is needed than the gen_statem behaviour provides. + procedure is needed than + the gen_statem behaviour provides.

Module, Options and Server have the same meanings as when calling - gen_statem:start[_link]/3,4 + gen_statem:start[_link]/3,4 . However, the @@ -912,7 +923,7 @@ erlang:'!' -----> Module:StateName/5

CALLBACK FUNCTIONS

The following functions should be exported from a - gen_statem callback module. + gen_statem callback module.

@@ -937,10 +948,10 @@ erlang:'!' -----> Module:StateName/5 -

Whenever a gen_statem is started using - gen_statem:start_link/3,4 +

Whenever a gen_statem is started using + start_link/3,4 or - gen_statem:start/3,4, + start/3,4, this function is called by the new process to initialize the implementation loop data.

@@ -950,7 +961,7 @@ erlang:'!' -----> Module:StateName/5 return {ok,State,StateData} or {ok,State,StateData,StateOps}. State is the state - of the gen_statem. + of the gen_statem.

The StateOps are executed when entering the first @@ -958,14 +969,14 @@ erlang:'!' -----> Module:StateName/5 state function.

This function allows an option to select the callback mode - of the gen_statem. See + of the gen_statem. See init_option. This option is not allowed from the state function(s).

If something goes wrong during the initialization the function should return {stop,Reason} or ignore. See - gen_statem:start_link/3,4. + start_link/3,4.

@@ -1002,9 +1013,9 @@ erlang:'!' -----> Module:StateName/5 -

Whenever a gen_statem receives an event from - gen_statem:call/2, - gen_statem:cast/2 or +

Whenever a gen_statem receives an event from + call/2, + cast/2 or as a normal process message this function is called. If callback_mode @@ -1022,7 +1033,7 @@ erlang:'!' -----> Module:StateName/5 Replies or by calling - gen_statem:reply(Client, Reply) + reply(Client, Reply) .

StateName is useful in some odd cases for example @@ -1032,7 +1043,7 @@ erlang:'!' -----> Module:StateName/5

PrevStateName and PrevState are useful in some odd cases for example when you want to do something only at the first event in a state. - Note that when gen_statem enters its first state + Note that when gen_statem enters its first state this is set to undefined.

If this function returns with a new state that @@ -1041,7 +1052,7 @@ erlang:'!' -----> Module:StateName/5

See state_op() for options that can be set and operations that can be done - by gen_statem after returning from this function. + by gen_statem after returning from this function.

@@ -1060,45 +1071,49 @@ erlang:'!' -----> Module:StateName/5 Ignored = term() -

This function is called by a gen_statem when it is about to - terminate. It should be the opposite of +

This function is called by a gen_statem + when it is about to terminate. It should be the opposite of Module:init/1 and do any necessary cleaning up. When it returns, - the gen_statem terminates with Reason. The return + the gen_statem terminates with Reason. The return value is ignored.

Reason is a term denoting the stop reason and State - is the internal state of the gen_statem. + is the internal state of the gen_statem.

-

Reason depends on why the gen_statem is terminating. +

Reason depends on why the gen_statem + is terminating. If it is because another callback function has returned a stop tuple {stop,Reason} in StateOps, Reason will have the value specified in that tuple. If it is due to a failure, Reason is the error reason.

-

If the gen_statem is part of a supervision tree and is +

If the gen_statem is part of a supervision tree and is ordered by its supervisor to terminate, this function will be called with Reason = shutdown if the following conditions apply:

- the gen_statem has been set to trap exit signals, and + the gen_statem has been set + to trap exit signals, and + the shutdown strategy as defined in the supervisor's child specification is an integer timeout value, not brutal_kill. -

Even if the gen_statem is not part of a supervision tree, - this function will be called if it receives an 'EXIT' - message from its parent. Reason will be the same as in - the 'EXIT' message. +

Even if the gen_statem is not + part of a supervision tree, this function will be called + if it receives an 'EXIT' message from its parent. + Reason will be the same as + in the 'EXIT' message.

-

Otherwise, the gen_statem will be immediately terminated. +

Otherwise, the gen_statem will be immediately terminated.

Note that for any other reason than normal, - shutdown, or {shutdown,Term} the gen_statem is - assumed to terminate due to an error and - an error report is issued using + shutdown, or {shutdown,Term} + the gen_statem is assumed to terminate due to an error + and an error report is issued using error_logger:format/2 . @@ -1126,7 +1141,7 @@ erlang:'!' -----> Module:StateName/5 Reason = term() -

This function is called by a gen_statem when it should +

This function is called by a gen_statem when it should update its internal state during a release upgrade/downgrade, i.e. when the instruction {update,Module,Change,...} where Change={advanced,Extra} is given in @@ -1144,7 +1159,7 @@ erlang:'!' -----> Module:StateName/5 is the checksum of the BEAM file.

OldState and OldStateData is the internal state - of the gen_statem. + of the gen_statem.

Extra is passed as-is from the {advanced,Extra} part of the update instruction. @@ -1163,7 +1178,7 @@ erlang:'!' -----> Module:StateName/5 Status Optional function for providing a term describing the - current gen_statem status + current gen_statem status Opt = normal | terminate PDict = [{Key, Value}] @@ -1180,43 +1195,43 @@ erlang:'!' -----> Module:StateName/5

This callback is optional, so callback modules need not - export it. The gen_statem module provides a default + export it. The gen_statem module provides a default implementation of this function that returns the callback module state.

-

This function is called by a gen_statem process when:

+

This function is called by a gen_statem process when:

One of sys:get_status/1,2 - is invoked to get the gen_statem status. Opt is set + is invoked to get the gen_statem status. Opt is set to the atom normal for this case. - The gen_statem terminates abnormally and logs an error. + The gen_statem terminates abnormally and logs an error. Opt is set to the atom terminate for this case.

This function is useful for customising the form and - appearance of the gen_statem status for these cases. A + appearance of the gen_statem status for these cases. A callback module wishing to customise the sys:get_status/1,2 return value as well as how its status appears in termination error logs exports an instance of format_status/2 that returns a term - describing the current status of the gen_statem. + describing the current status of the gen_statem.

-

PDict is the current value of the gen_statem's +

PDict is the current value of the gen_statem's process dictionary.

State - is the internal state of the gen_statem. + is the internal state of the gen_statem.

The function should return Status, a term that customises the details of the current state and status of - the gen_statem. There are no restrictions on the + the gen_statem. There are no restrictions on the form Status can take, but for the sys:get_status/1,2 @@ -1224,7 +1239,7 @@ erlang:'!' -----> Module:StateName/5 is normal), the recommended form for the Status value is [{data, [{"State", Term}]}] where Term provides relevant details of - the gen_statem state. Following this recommendation isn't + the gen_statem state. Following this recommendation isn't required, but doing so will make the callback module status consistent with the rest of the -- cgit v1.2.3 From 898e66f07dce8b7b33874255bb3ea1c6f5534d34 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 19 Feb 2016 15:26:33 +0100 Subject: Update terminology to data(), transition_op(), etc --- lib/stdlib/doc/src/gen_statem.xml | 222 +++++++++++--------- lib/stdlib/src/gen_statem.erl | 382 ++++++++++++++++++----------------- lib/stdlib/test/gen_statem_SUITE.erl | 44 ++-- 3 files changed, 338 insertions(+), 310 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index c685d24b78..15e9584360 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -94,7 +94,7 @@ erlang:'!' -----> Module:StateName/5 Module:StateName/5 . - This naturally collects all code for a specific state + This gathers all code for a specific state in one function and hence dispatches on state first.

When @@ -135,8 +135,8 @@ erlang:'!' -----> Module:StateName/5

The state function can insert events using the - - state_operation() next_event + + transition_action() next_event and such an event is inserted as the next to present to the state function. That is: as if it is @@ -176,8 +176,8 @@ erlang:'!' -----> Module:StateName/5 state function or Module:init/1 specifies 'hibernate' in the returned - StateOps list. - This might be useful if the server is expected to be idle + Ops + list. This might be useful if the server is expected to be idle for a long time. However use this feature with care since hibernation implies at least two garbage collections (when hibernating and shortly after waking up) and that is not @@ -273,8 +273,8 @@ erlang:'!' -----> Module:StateName/5

Client address to use when replying through for example the - - state_op() {reply,Client,Reply} + + transition_op() {reply,Client,Reply} to a client that has called the gen_statem server using call/2. @@ -296,19 +296,21 @@ erlang:'!' -----> Module:StateName/5 callback_mode is state_functions, which is the default, - the state has to be of this type i.e an atom(). + the state has to be of this type.

- + -

A term() in which the state machine implementation - should store any state data it needs. The difference between - this data and the - state() +

A term in which the state machine implementation + should store any server data it needs. The difference between + this and the state() itself is that a change in this data does not cause - postponed events to be retried. + postponed events to be retried. Hence if a change + in this data would change the set of events that + are handled than that data item should be made + a part of the state.

@@ -334,8 +336,8 @@ erlang:'!' -----> Module:StateName/5

A fun() of arity 2 that takes an event and returns a boolean. - When used in {remove_event,RemoveEventPredicate} - from state_op(). + When used in {remove_event,RemoveEventPredicate} from + transition_op(). The event for which the predicate returns true will be removed.

@@ -378,17 +380,16 @@ erlang:'!' -----> Module:StateName/5
- +

Either a - - state_option() + + transition_option() of which the last occurence in the containing list takes precedence, or a - - state_operation() - that are performed in order of - the containing list. + + transition_action() + performed in the order of the containing list.

These may be returned from the state function, @@ -404,7 +405,7 @@ erlang:'!' -----> Module:StateName/5 If the state changes the queue of incoming events is reset to start with the oldest postponed. - All operations are processed in order of appearance. + All actions are processed in order of appearance. The timeout option is processed if present, so a state timer may be started or a timeout zero event @@ -424,9 +425,9 @@ erlang:'!' -----> Module:StateName/5 - + -

If multiple state options of the same type are present +

If multiple state options of the same kind are present in the containing list these are set in the list order and the last value is kept.

@@ -448,7 +449,7 @@ erlang:'!' -----> Module:StateName/5 proc_lib:hibernate/3 before receive to wait for a new event. - If there are enqueued events the hibernate operation + If there are enqueued events the hibernate action is ignored as if an event just arrived and awakened the gen_statem. @@ -464,8 +465,8 @@ erlang:'!' -----> Module:StateName/5 is immediately enqueued as the newest received. Also note that it is not possible nor needed to cancel this timeout using the - - state_operation() + + transition_action() cancel_timer. This timeout is cancelled automatically by any event. @@ -473,17 +474,20 @@ erlang:'!' -----> Module:StateName/5
- + -

The state operations are executed in the containing - list order. This matters for next_event where - the last one in the list will become the next event to present - to the state functions. Regarding the other operations - it is only for remove_event with - EventPredicate - and for reply_operation() that the order may matter. +

The state transition actions are executed + in the containing list order. This matters + for next_event where the last one in the list + will become the next event to present + to the state functions. Regarding the other actions + it is only for remove_event with + EventPredicate + and for reply_action() that the order may matter.

+ reply_action() + Reply to a calling client. next_event Insert the given EventType and EventContent the next to process. @@ -518,7 +522,7 @@ erlang:'!' -----> Module:StateName/5 demonitor/2 with MonitorRef. - {unlink,Id} + unlink Like {cancel_timer,_} above but for unlink/1 @@ -528,7 +532,7 @@ erlang:'!' -----> Module:StateName/5
- +

Reply to a client that called call/2. @@ -550,26 +554,26 @@ erlang:'!' -----> Module:StateName/5 Module:terminate/3 with Reason and - NewStateData, if given. + NewData, if given. next_state The gen_statem will do a state transition to NewState (which may be the same as the current state), - set NewStateData - and execute all StateOps + set NewData + and execute all Ops keep_state The gen_statem will keep the current state, or do a state transition to the current state if you like, - set NewStateData - and execute all StateOps + set NewData + and execute all Ops keep_state_and_data The gen_statem will keep the current state, or do a state transition to the current state if you like, - keep the current state data, - and execute all StateOps + keep the current server data, + and execute all Ops @@ -614,7 +618,7 @@ erlang:'!' -----> Module:StateName/5 .

If the option {timeout,Time} is present in - Options, the gen_statem + Opts, the gen_statem is allowed to spend Time milliseconds initializing or it will be terminated and the start function will return @@ -623,15 +627,14 @@ erlang:'!' -----> Module:StateName/5

If the option {debug,Dbgs} - is present in Options, debugging through + is present in Opts, debugging through sys is activated.

-

If the option {spawn_opt,SOpts} is present in - Options, SOpts will be passed - as option list to the spawn_opt BIF - which is used to - spawn - the gen_statem. +

If the option {spawn_opt,SpawnOpts} is present in + Opts, SpawnOpts will be passed + as option list to + spawn_opt/2 + which is used to spawn the gen_statem process.

Using the spawn option monitor is currently not @@ -760,7 +763,9 @@ erlang:'!' -----> Module:StateName/5 state function returns with {reply,Client,Reply} as one - state_op(), + + transition_op() + , and that Reply becomes the return value of this function.

@@ -823,8 +828,8 @@ erlang:'!' -----> Module:StateName/5 state function. Client and Reply can also be specified using a - - reply_operation() + + reply_action() and multiple replies with a list of them.

@@ -852,20 +857,20 @@ erlang:'!' -----> Module:StateName/5 Enter the gen_statem receive loop -

If Server_or_StateOps is a list() +

If Server_or_Ops is a list() the same as enter_loop/6 except that no server_name() must have been registered and - StateOps = Server_or_StateOps. + Ops = Server_or_Ops.

Otherwise the same as enter_loop/6 with - Server = Server_or_StateOps and - StateOps = []. + Server = Server_or_Ops and + Ops = [].

@@ -887,7 +892,7 @@ erlang:'!' -----> Module:StateName/5 procedure is needed than the gen_statem behaviour provides.

-

Module, Options and +

Module, Opts and Server have the same meanings as when calling @@ -898,8 +903,8 @@ erlang:'!' -----> Module:StateName/5 server_name() name must have been registered accordingly before this function is called.

-

State, StateData - and StateOps +

State, Data + and Ops have the same meanings as in the return value of Module:init/1. Also, the callback module Module @@ -933,15 +938,14 @@ erlang:'!' -----> Module:StateName/5 Initialize process and internal state Args = term() - Result = {ok,State,StateData} -  | {ok,State,StateData,StateOps} + Result = {ok,State,Data} +  | {ok,State,Data,Ops}  | {stop,Reason} | ignore State = state() - StateData = - state_data() + Data = data() - StateOps = - [state_op() + Ops = + [transition_op() | init_option()] Reason = term() @@ -953,17 +957,17 @@ erlang:'!' -----> Module:StateName/5 or start/3,4, this function is called by the new process to initialize - the implementation loop data. + the implementation state and server data.

Args is the Args argument provided to the start function.

If the initialization is successful, the function should - return {ok,State,StateData} or - {ok,State,StateData,StateOps}. + return {ok,State,Data} or + {ok,State,Data,Ops}. State is the state of the gen_statem.

-

The StateOps +

The Ops are executed when entering the first state just as for a state function. @@ -978,15 +982,19 @@ erlang:'!' -----> Module:StateName/5 or ignore. See start_link/3,4.

+

This function may use + throw, + to return its value. +

Module:StateName(EventType, EventContent, - PrevStateName, StateName, StateData) -> Result + PrevStateName, StateName, Data) -> Result Module:handle_event(EventType, EventContent, - PrevState, State, StateData) -> Result + PrevState, State, Data) -> Result Handle an event @@ -1003,8 +1011,8 @@ erlang:'!' -----> Module:StateName/5 PrevState = State = state() - StateData = NewStateData = - state_data() + Data = NewData = + data() Result = @@ -1029,8 +1037,8 @@ erlang:'!' -----> Module:StateName/5 from this or from any other state function by returning with {reply,Client,Reply} in - StateOps, in - Replies + Ops, in + Replies or by calling reply(Client, Reply) @@ -1050,23 +1058,25 @@ erlang:'!' -----> Module:StateName/5 does not match equal (=/=) to the current state all postponed events will be retried in the new state.

-

See state_op() - for options that can be set and operations that can be done +

See + transition_op() + for options that can be set and actions that can be done by gen_statem after returning from this function.

+

These functions may use + throw, + to return its value. +

- Module:terminate(Reason, State, StateData) -> Ignored + Module:terminate(Reason, State, Data) -> Ignored Clean up before termination Reason = normal | shutdown | {shutdown,term()} | term() State = state() - StateData = - - state_data() - + Data = data() Ignored = term() @@ -1085,7 +1095,7 @@ erlang:'!' -----> Module:StateName/5 is terminating. If it is because another callback function has returned a stop tuple {stop,Reason} in - StateOps, + Ops, Reason will have the value specified in that tuple. If it is due to a failure, Reason is the error reason.

@@ -1118,11 +1128,15 @@ erlang:'!' -----> Module:StateName/5 error_logger:format/2 .

+

This function may use + throw, + to return its value. +

- Module:code_change(OldVsn, OldState, OldStateData, Extra) -> + Module:code_change(OldVsn, OldState, OldData, Extra) -> Result Update the internal state during upgrade/downgrade @@ -1131,12 +1145,12 @@ erlang:'!' -----> Module:StateName/5   Vsn = term() OldState = NewState = term() Extra = term() - Result = {ok,{NewState,NewStateData}} | Reason + Result = {ok,{NewState,NewData}} | Reason OldState = NewState = state() - OldStateData = NewStateData = - state_data() + OldData = NewData = + data() Reason = term() @@ -1158,7 +1172,7 @@ erlang:'!' -----> Module:StateName/5 Module. If no such attribute is defined, the version is the checksum of the BEAM file.

-

OldState and OldStateData is the internal state +

OldState and OldData is the internal state of the gen_statem.

Extra is passed as-is from the {advanced,Extra} @@ -1166,15 +1180,19 @@ erlang:'!' -----> Module:StateName/5

If successful, the function shall return the updated internal state in an - {ok,{NewState,NewStateData}} tuple. + {ok,{NewState,NewData}} tuple.

If the function returns Reason, the ongoing upgrade will fail and roll back to the old release.

+

This function may use + throw, + to return its value. +

- Module:format_status(Opt, [PDict,State,StateData]) -> + Module:format_status(Opt, [PDict,State,Data]) -> Status Optional function for providing a term describing the @@ -1185,8 +1203,8 @@ erlang:'!' -----> Module:StateName/5 State = state() - StateData = - state_data() + Data = + data() Key = term() Value = term() @@ -1197,7 +1215,8 @@ erlang:'!' -----> Module:StateName/5

This callback is optional, so callback modules need not export it. The gen_statem module provides a default implementation of this function that returns the callback - module state. + module state and data. The default function will also + be used if this callback fails.

This function is called by a gen_statem process when:

@@ -1229,6 +1248,9 @@ erlang:'!' -----> Module:StateName/5

State is the internal state of the gen_statem.

+

Data + is the internal server data of the gen_statem. +

The function should return Status, a term that customises the details of the current state and status of the gen_statem. There are no restrictions on the @@ -1250,6 +1272,10 @@ erlang:'!' -----> Module:StateName/5 state representations to avoid having large state terms printed in logfiles.

+

This function may use + throw, + to return its value. +

diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 8aa8afd091..1ca2e1009c 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -57,7 +57,7 @@ state_name() | % For state callback function StateName/5 term(). % For state callback function handle_event/5 -type state_name() :: atom(). --type state_data() :: term(). +-type data() :: term(). -type event_type() :: {'call',Client :: client()} | 'cast' | 'info' | 'timeout' | 'internal'. @@ -66,17 +66,18 @@ -type init_option() :: {'callback_mode', callback_mode()}. -type callback_mode() :: 'state_functions' | 'handle_event_function'. --type state_op() :: - %% First NewState and NewStateData are set, - %% then all state_operations() are executed in order of +-type transition_op() :: + %% First NewState and NewData are set, + %% then all transition_action()s are executed in order of %% apperance. Postponing the current event is performed - %% (iff state_option() 'postpone' is 'true'). + %% (iff transition_option() 'postpone' is 'true'). %% Lastly pending events are processed or if there are %% no pending events the server goes into receive - %% or hibernate (iff state_option() 'hibernate' is 'true') - state_option() | state_operation(). --type state_option() :: - %% The first of each kind in the state_op() list takes precedence + %% or hibernate (iff transition_option() 'hibernate' is 'true') + transition_option() | transition_action(). +-type transition_option() :: + %% The last of each kind in the transition_op() + %% list takes precedence 'postpone' | % Postpone the current event to a different (=/=) state {'postpone', Postpone :: boolean()} | 'hibernate' | % Hibernate the server instead of going into receive @@ -84,10 +85,10 @@ (Timeout :: timeout()) | % {timeout,Timeout} {'timeout', % Generate a ('timeout', Msg, ...) event after Time Time :: timeout(), Msg :: term()}. --type state_operation() :: +-type transition_action() :: %% These can occur multiple times and are executed in order - %% of appearence in the state_op() list - reply_operation() | + %% of appearence in the transition_op() list + reply_action() | {'next_event', % Insert event as the next to handle EventType :: event_type(), EventContent :: term()} | @@ -101,7 +102,7 @@ MonitorRef :: reference()} | {'unlink', % Unlink and clean up mess(ages) Id :: pid() | port()}. --type reply_operation() :: +-type reply_action() :: {'reply', % Reply to a client Client :: client(), Reply :: term()}. -type state_callback_result() :: @@ -109,34 +110,34 @@ Reason :: term()} | {'stop', % Stop the server Reason :: term(), - NewStateData :: state_data()} | + NewData :: data()} | {'stop', % Stop the server Reason :: term(), - Replies :: [reply_operation()] | reply_operation(), - NewStateData :: state_data()} | - {'next_state', % {next_state,NewState,NewStateData,[]} + Replies :: [reply_action()] | reply_action(), + NewData :: data()} | + {'next_state', % {next_state,NewState,NewData,[]} NewState :: state(), - NewStateData :: state_data()} | + NewData :: data()} | {'next_state', % State transition, maybe to the same state NewState :: state(), - NewStateData :: state_data(), - StateOps :: [state_op()] | state_op()} | - {'keep_state', % {keep_state,NewStateData,[]} - NewStateData :: state_data()} | + NewData :: data(), + Ops :: [transition_op()] | transition_op()} | + {'keep_state', % {keep_state,NewData,[]} + NewData :: data()} | {'keep_state', - NewStateData :: state_data(), - StateOps :: [state_op()] | state_op()} | + NewData :: data(), + Ops :: [transition_op()] | transition_op()} | {'keep_state_and_data'} | % {keep_state_and_data,[]} {'keep_state_and_data', - StateOps :: [state_op()] | state_op()}. + Ops :: [transition_op()] | transition_op()}. %% The state machine init function. It is called only once and %% the server is not running until this function has returned %% an {ok, ...} tuple. Thereafter the state callbacks are called %% for all events to this server. -callback init(Args :: term()) -> - {'ok', state(), state_data()} | - {'ok', state(), state_data(), [state_op()|init_option()]} | + {'ok', state(), data()} | + {'ok', state(), data(), [transition_op()|init_option()]} | 'ignore' | {'stop', Reason :: term()}. @@ -152,7 +153,7 @@ EventContent :: term(), PrevStateName :: state_name() | reference(), StateName :: state_name(), % Current state - StateData :: state_data()) -> + Data :: data()) -> state_callback_result(). %% %% Callback for callback_mode =:= handle_event_function. @@ -164,7 +165,7 @@ EventContent :: term(), PrevState :: state(), State :: state(), % Current state - StateData :: state_data()) -> + Data :: data()) -> state_callback_result(). %% Clean up before the server terminates. @@ -172,7 +173,7 @@ Reason :: 'normal' | 'shutdown' | {'shutdown', term()} | term(), State :: state(), - StateData :: state_data()) -> + Data :: data()) -> any(). %% Note that the new code can expect to get an OldState from @@ -181,9 +182,9 @@ -callback code_change( OldVsn :: term() | {'down', term()}, OldState :: state(), - OldStateData :: state_data(), + OldData :: data(), Extra :: term()) -> - {ok, {NewState :: state(), NewStateData :: state_data()}}. + {ok, {NewState :: state(), NewData :: data()}}. %% Format the callback module state in some sensible that is %% often condensed way. For StatusOption =:= 'normal' the perferred @@ -193,7 +194,7 @@ StatusOption, [ [{Key :: term(), Value :: term()}] | state() | - state_data()]) -> + data()]) -> Status :: term() when StatusOption :: 'normal' | 'terminate'. @@ -264,38 +265,38 @@ event_type(Type) -> -type start_opt() :: debug_opt() | {'timeout', Time :: timeout()} - | {'spawn_opt', SOpts :: [proc_lib:spawn_option()]}. + | {'spawn_opt', [proc_lib:spawn_option()]}. -type start_ret() :: {'ok', pid()} | 'ignore' | {'error', term()}. %% Start a state machine -spec start( - Module :: module(), Args :: term(), Options :: [start_opt()]) -> + Module :: module(), Args :: term(), Opts :: [start_opt()]) -> start_ret(). -start(Module, Args, Options) -> - gen:start(?MODULE, nolink, Module, Args, Options). +start(Module, Args, Opts) -> + gen:start(?MODULE, nolink, Module, Args, Opts). %% -spec start( ServerName :: server_name(), - Module :: module(), Args :: term(), Options :: [start_opt()]) -> + Module :: module(), Args :: term(), Opts :: [start_opt()]) -> start_ret(). -start(ServerName, Module, Args, Options) -> - gen:start(?MODULE, nolink, ServerName, Module, Args, Options). +start(ServerName, Module, Args, Opts) -> + gen:start(?MODULE, nolink, ServerName, Module, Args, Opts). %% Start and link to a state machine -spec start_link( - Module :: module(), Args :: term(), Options :: [start_opt()]) -> + Module :: module(), Args :: term(), Opts :: [start_opt()]) -> start_ret(). -start_link(Module, Args, Options) -> - gen:start(?MODULE, link, Module, Args, Options). +start_link(Module, Args, Opts) -> + gen:start(?MODULE, link, Module, Args, Opts). %% -spec start_link( ServerName :: server_name(), - Module :: module(), Args :: term(), Options :: [start_opt()]) -> + Module :: module(), Args :: term(), Opts :: [start_opt()]) -> start_ret(). -start_link(ServerName, Module, Args, Options) -> - gen:start(?MODULE, link, ServerName, Module, Args, Options). +start_link(ServerName, Module, Args, Opts) -> + gen:start(?MODULE, link, ServerName, Module, Args, Opts). %% Stop a state machine -spec stop(ServerRef :: server_ref()) -> ok. @@ -389,7 +390,7 @@ call(ServerRef, Request, Timeout) -> end. %% Reply from a state machine callback to whom awaits in call/2 --spec reply([reply_operation()] | reply_operation()) -> ok. +-spec reply([reply_action()] | reply_action()) -> ok. reply({reply,{_To,_Tag}=Client,Reply}) -> reply(Client, Reply); reply(Replies) when is_list(Replies) -> @@ -411,39 +412,39 @@ reply({To,Tag}, Reply) -> %% started by proc_lib into a state machine using %% the same arguments as you would have returned from init/1 -spec enter_loop( - Module :: module(), Options :: [debug_opt()], - State :: state(), StateData :: state_data()) -> + Module :: module(), Opts :: [debug_opt()], + State :: state(), Data :: data()) -> no_return(). -enter_loop(Module, Options, State, StateData) -> - enter_loop(Module, Options, State, StateData, self()). +enter_loop(Module, Opts, State, Data) -> + enter_loop(Module, Opts, State, Data, self()). %% -spec enter_loop( - Module :: module(), Options :: [debug_opt()], - State :: state(), StateData :: state_data(), - Server_or_StateOps :: - server_name() | pid() | [state_op()|init_option()]) -> + Module :: module(), Opts :: [debug_opt()], + State :: state(), Data :: data(), + Server_or_Ops :: + server_name() | pid() | [transition_op()|init_option()]) -> no_return(). -enter_loop(Module, Options, State, StateData, Server_or_StateOps) -> +enter_loop(Module, Opts, State, Data, Server_or_Ops) -> if - is_list(Server_or_StateOps) -> + is_list(Server_or_Ops) -> enter_loop( - Module, Options, State, StateData, - self(), Server_or_StateOps); + Module, Opts, State, Data, + self(), Server_or_Ops); true -> enter_loop( - Module, Options, State, StateData, - Server_or_StateOps, []) + Module, Opts, State, Data, + Server_or_Ops, []) end. %% -spec enter_loop( - Module :: module(), Options :: [debug_opt()], - State :: state(), StateData :: state_data(), + Module :: module(), Opts :: [debug_opt()], + State :: state(), Data :: data(), Server :: server_name() | pid(), - StateOps :: [state_op()|init_option()]) -> + Ops :: [transition_op()|init_option()]) -> no_return(). -enter_loop(Module, Options, State, StateData, Server, StateOps) -> +enter_loop(Module, Opts, State, Data, Server, Ops) -> Parent = gen:get_parent(), - enter(Module, Options, State, StateData, Server, StateOps, Parent). + enter(Module, Opts, State, Data, Server, Ops, Parent). %%--------------------------------------------------------------------------- %% API helpers @@ -465,29 +466,29 @@ do_send(Proc, Msg) -> end. %% Here init_it and all enter_loop functions converge -enter(Module, Options, State, StateData, Server, InitOps, Parent) -> +enter(Module, Opts, State, Data, Server, InitOps, Parent) -> Name = gen:get_proc_name(Server), - Debug = gen:debug_options(Name, Options), + Debug = gen:debug_options(Name, Opts), PrevState = undefined, S = #{ callback_mode => state_functions, module => Module, name => Name, prev_state => PrevState, - state => PrevState, % Will be discarded by loop_event_state_ops - state_data => StateData, + state => PrevState, % Will be discarded by loop_event_transition_ops + data => Data, timer => undefined, postponed => [], hibernate => false}, case collect_init_options(InitOps) of - {CallbackMode,StateOps} -> - loop_event_state_ops( + {CallbackMode,Ops} -> + loop_event_transition_ops( Parent, Debug, S#{callback_mode := CallbackMode}, [], {event,undefined}, % Will be discarded by {postpone,false} - PrevState, State, StateData, - StateOps++[{postpone,false}]); + PrevState, State, Data, + Ops++[{postpone,false}]); [Reason] -> ?TERMINATE(Reason, Debug, S, []) end. @@ -495,13 +496,13 @@ enter(Module, Options, State, StateData, Server, InitOps, Parent) -> %%%========================================================================== %%% gen callbacks -init_it(Starter, Parent, ServerRef, Module, Args, Options) -> +init_it(Starter, Parent, ServerRef, Module, Args, Opts) -> try Module:init(Args) of Result -> - init_result(Starter, Parent, ServerRef, Module, Result, Options) + init_result(Starter, Parent, ServerRef, Module, Result, Opts) catch Result -> - init_result(Starter, Parent, ServerRef, Module, Result, Options); + init_result(Starter, Parent, ServerRef, Module, Result, Opts); Class:Reason -> gen:unregister_name(ServerRef), proc_lib:init_ack(Starter, {error,Reason}), @@ -511,18 +512,14 @@ init_it(Starter, Parent, ServerRef, Module, Args, Options) -> %%--------------------------------------------------------------------------- %% gen callbacks helpers -init_result(Starter, Parent, ServerRef, Module, Result, Options) -> +init_result(Starter, Parent, ServerRef, Module, Result, Opts) -> case Result of - {ok,State,StateData} -> + {ok,State,Data} -> proc_lib:init_ack(Starter, {ok,self()}), - enter( - Module, Options, State, StateData, ServerRef, - [], Parent); - {ok,State,StateData,StateOps} -> + enter(Module, Opts, State, Data, ServerRef, [], Parent); + {ok,State,Data,Ops} -> proc_lib:init_ack(Starter, {ok,self()}), - enter( - Module, Options, State, StateData, ServerRef, - StateOps, Parent); + enter(Module, Opts, State, Data, ServerRef, Ops, Parent); {stop,Reason} -> gen:unregister_name(ServerRef), proc_lib:init_ack(Starter, {error,Reason}), @@ -549,32 +546,32 @@ system_terminate(Reason, _Parent, Debug, S) -> system_code_change( #{module := Module, state := State, - state_data := StateData} = S, + data := Data} = S, _Mod, OldVsn, Extra) -> case - try Module:code_change(OldVsn, State, StateData, Extra) + try Module:code_change(OldVsn, State, Data, Extra) catch Result -> Result end of - {ok,{NewState,NewStateData}} -> + {ok,{NewState,NewData}} -> {ok, S#{ state := NewState, - state_data := NewStateData}}; + data := NewData}}; Error -> Error end. -system_get_state(#{state := State, state_data := StateData}) -> - {ok,{State,StateData}}. +system_get_state(#{state := State, data := Data}) -> + {ok,{State,Data}}. system_replace_state( StateFun, #{state := State, - state_data := StateData} = S) -> - {NewState,NewStateData} = Result = StateFun({State,StateData}), - {ok,Result,S#{state := NewState, state_data := NewStateData}}. + data := Data} = S) -> + {NewState,NewData} = Result = StateFun({State,Data}), + {ok,Result,S#{state := NewState, data := NewData}}. format_status( Opt, @@ -642,7 +639,7 @@ wakeup_from_hibernate(Parent, Debug, S) -> loop_receive(Parent, Debug, S). %%%========================================================================== -%%% STate Machine engine implementation of proc_lib/gen server +%%% State Machine engine implementation of proc_lib/gen server %% Server loop, consists of all loop* functions %% and some detours through sys and proc_lib @@ -717,7 +714,7 @@ loop_events( module := Module, prev_state := PrevState, state := State, - state_data := StateData} = S, + data := Data} = S, [{Type,Content} = Event|Events] = Q, Timer) -> _ = (Timer =/= undefined) andalso cancel_timer(Timer), @@ -728,7 +725,7 @@ loop_events( state_functions -> State end, - try Module:Func(Type, Content, PrevState, State, StateData) of + try Module:Func(Type, Content, PrevState, State, Data) of Result -> loop_event_result( Parent, Debug, S, Events, Event, Result) @@ -741,7 +738,7 @@ loop_events( %% of calling a nonexistent state function case erlang:get_stacktrace() of [{Module,Func, - [Type,Content,PrevState,State,StateData]=Args, + [Type,Content,PrevState,State,Data]=Args, _} |Stacktrace] -> terminate( @@ -760,18 +757,18 @@ loop_events( %% Interpret all callback return value variants loop_event_result( Parent, Debug, - #{state := State, state_data := StateData} = S, + #{state := State, data := Data} = S, Events, Event, Result) -> case Result of {stop,Reason} -> ?TERMINATE(Reason, Debug, S, [Event|Events]); - {stop,Reason,NewStateData} -> + {stop,Reason,NewData} -> ?TERMINATE( Reason, Debug, - S#{state_data := NewStateData}, + S#{data := NewData}, [Event|Events]); - {stop,Reason,Reply,NewStateData} -> - NewS = S#{state_data := NewStateData}, + {stop,Reason,Reply,NewData} -> + NewS = S#{data := NewData}, Q = [Event|Events], Replies = if @@ -785,43 +782,43 @@ loop_event_result( exit, Reason, ?STACKTRACE(), Debug, NewS, Q, Replies), %% Since we got back here Replies was bad ?TERMINATE( - {bad_return_value,{stop,Reason,BadReplies,NewStateData}}, + {bad_return_value,{stop,Reason,BadReplies,NewData}}, Debug, NewS, Q); - {next_state,NewState,NewStateData} -> - loop_event_state_ops( + {next_state,NewState,NewData} -> + loop_event_transition_ops( Parent, Debug, S, Events, Event, - State, NewState, NewStateData, []); - {next_state,NewState,NewStateData,StateOps} - when is_list(StateOps) -> - loop_event_state_ops( + State, NewState, NewData, []); + {next_state,NewState,NewData,Ops} + when is_list(Ops) -> + loop_event_transition_ops( Parent, Debug, S, Events, Event, - State, NewState, NewStateData, StateOps); - {keep_state,NewStateData} -> - loop_event_state_ops( + State, NewState, NewData, Ops); + {keep_state,NewData} -> + loop_event_transition_ops( Parent, Debug, S, Events, Event, - State, State, NewStateData, []); - {keep_state,NewStateData,StateOps} -> - loop_event_state_ops( + State, State, NewData, []); + {keep_state,NewData,Ops} -> + loop_event_transition_ops( Parent, Debug, S, Events, Event, - State, State, NewStateData, StateOps); + State, State, NewData, Ops); {keep_state_and_data} -> - loop_event_state_ops( + loop_event_transition_ops( Parent, Debug, S, Events, Event, - State, State, StateData, []); - {keep_state_and_data,StateOps} -> - loop_event_state_ops( + State, State, Data, []); + {keep_state_and_data,Ops} -> + loop_event_transition_ops( Parent, Debug, S, Events, Event, - State, State, StateData, StateOps); + State, State, Data, Ops); _ -> ?TERMINATE( {bad_return_value,Result}, Debug, S, [Event|Events]) end. -loop_event_state_ops( +loop_event_transition_ops( Parent, Debug0, #{postponed := P0} = S, Events, Event, - State, NewState, NewStateData, StateOps) -> - case collect_state_options(StateOps) of - {Postpone,Hibernate,Timeout,Operations} -> + State, NewState, NewData, Ops) -> + case collect_transition_options(Ops) of + {Postpone,Hibernate,Timeout,Actions} -> P1 = % Move current event to postponed if Postpone case Postpone of true -> @@ -837,8 +834,8 @@ loop_event_state_ops( {lists:reverse(P1, Events),[]} end, %% - case process_state_operations( - Operations, Debug0, S, Q2, P2) of + case process_transition_actions( + Actions, Debug0, S, Q2, P2) of {Debug,Q3,P} -> NewDebug = sys_debug( @@ -865,7 +862,7 @@ loop_event_state_ops( S#{ prev_state := State, state := NewState, - state_data := NewStateData, + data := NewData, timer := Timer, hibernate := Hibernate, postponed := P}, @@ -892,92 +889,96 @@ collect_init_options(InitOps) -> collect_init_options([InitOps], state_functions, []) end. %% Keep the last of each kind -collect_init_options([], CallbackMode, StateOps) -> - {CallbackMode,lists:reverse(StateOps)}; -collect_init_options([InitOp|InitOps] = IOIOs, CallbackMode, StateOps) -> +collect_init_options([], CallbackMode, Ops) -> + {CallbackMode,lists:reverse(Ops)}; +collect_init_options( + [InitOp|InitOps] = AllInitOps, CallbackMode, Ops) -> case InitOp of {callback_mode,Mode} when Mode =:= state_functions; Mode =:= handle_event_function -> - collect_init_options(InitOps, Mode, StateOps); + collect_init_options(InitOps, Mode, Ops); {callback_mode,_} -> - [{bad_init_ops,IOIOs}]; - _ -> % Collect others as StateOps + [{bad_init_ops,AllInitOps}]; + _ -> % Collect others as Ops collect_init_options( - InitOps, CallbackMode, [InitOp|StateOps]) + InitOps, CallbackMode, [InitOp|Ops]) end. -collect_state_options(StateOps) -> +collect_transition_options(Ops) -> if - is_list(StateOps) -> - collect_state_options(StateOps, false, false, undefined, []); + is_list(Ops) -> + collect_transition_options( + Ops, false, false, undefined, []); true -> - collect_state_options([StateOps], false, false, undefined, []) + collect_transition_options( + [Ops], false, false, undefined, []) end. %% Keep the last of each kind -collect_state_options( - [], Postpone, Hibernate, Timeout, Operations) -> - {Postpone,Hibernate,Timeout,lists:reverse(Operations)}; -collect_state_options( - [StateOp|StateOps] = SOSOs, Postpone, Hibernate, Timeout, Operations) -> - case StateOp of +collect_transition_options( + [], Postpone, Hibernate, Timeout, Actions) -> + {Postpone,Hibernate,Timeout,lists:reverse(Actions)}; +collect_transition_options( + [Op|Ops] = AllOps, Postpone, Hibernate, Timeout, Actions) -> + case Op of postpone -> - collect_state_options( - StateOps, true, Hibernate, Timeout, Operations); + collect_transition_options( + Ops, true, Hibernate, Timeout, Actions); {postpone,NewPostpone} when is_boolean(NewPostpone) -> - collect_state_options( - StateOps, NewPostpone, Hibernate, Timeout, Operations); + collect_transition_options( + Ops, NewPostpone, Hibernate, Timeout, Actions); {postpone,_} -> - [{bad_state_ops,SOSOs}]; + [{bad_ops,AllOps}]; hibernate -> - collect_state_options( - StateOps, Postpone, true, Timeout, Operations); + collect_transition_options( + Ops, Postpone, true, Timeout, Actions); {hibernate,NewHibernate} when is_boolean(NewHibernate) -> - collect_state_options( - StateOps, Postpone, NewHibernate, Timeout, Operations); + collect_transition_options( + Ops, Postpone, NewHibernate, Timeout, Actions); {hibernate,_} -> - [{bad_state_ops,SOSOs}]; + [{bad_ops,AllOps}]; {timeout,infinity,_} -> % Ignore since it will never time out - collect_state_options( - StateOps, Postpone, Hibernate, undefined, Operations); + collect_transition_options( + Ops, Postpone, Hibernate, undefined, Actions); {timeout,Time,_} = NewTimeout when is_integer(Time), Time >= 0 -> - collect_state_options( - StateOps, Postpone, Hibernate, NewTimeout, Operations); + collect_transition_options( + Ops, Postpone, Hibernate, NewTimeout, Actions); {timeout,_,_} -> - [{bad_state_ops,SOSOs}]; - _ -> % Collect others as operations - collect_state_options( - StateOps, Postpone, Hibernate, Timeout, [StateOp|Operations]) + [{bad_ops,AllOps}]; + _ -> % Collect others as actions + collect_transition_options( + Ops, Postpone, Hibernate, Timeout, [Op|Actions]) end. -process_state_operations([], Debug, _S, Q, P) -> +process_transition_actions([], Debug, _S, Q, P) -> {Debug,Q,P}; -process_state_operations([Operation|Operations] = OOs, Debug, S, Q, P) -> - case Operation of +process_transition_actions( + [Action|Actions] = AllActions, Debug, S, Q, P) -> + case Action of {reply,{_To,_Tag}=Client,Reply} -> NewDebug = do_reply(Debug, S, Client, Reply), - process_state_operations(Operations, NewDebug, S, Q, P); + process_transition_actions(Actions, NewDebug, S, Q, P); {next_event,Type,Content} -> case event_type(Type) of true -> - process_state_operations( - Operations, Debug, S, [{Type,Content}|Q], P); + process_transition_actions( + Actions, Debug, S, [{Type,Content}|Q], P); false -> - [{bad_state_ops,OOs},Debug] + [{bad_ops,AllActions},Debug] end; _ -> - %% All others are remove operations - case remove_fun(Operation) of + %% All others are remove actions + case remove_fun(Action) of false -> - process_state_operations( - Operations, Debug, S, Q, P); + process_transition_actions( + Actions, Debug, S, Q, P); undefined -> - [{bad_state_ops,OOs},Debug]; + [{bad_ops,AllActions},Debug]; RemoveFun when is_function(RemoveFun, 2) -> case remove_event(RemoveFun, Q, P) of {NewQ,NewP} -> - process_state_operations( - Operations, Debug, S, NewQ, NewP); + process_transition_actions( + Actions, Debug, S, NewQ, NewP); Error -> Error ++ [Debug] end; @@ -1023,7 +1024,8 @@ remove_event(RemoveFun, Q, P) -> [Class,Reason,erlang:get_stacktrace()] end. -%% Do the given state operation and create an event removal predicate fun() +%% Do the given transition action and create +%% an event removal predicate fun() remove_fun({remove_event,Type,Content}) -> fun (T, C) when T =:= Type, C =:= Content -> true; (_, _) -> false @@ -1104,9 +1106,9 @@ cancel_timer(TimerRef) -> terminate( Class, Reason, Stacktrace, Debug, #{module := Module, - state := State, state_data := StateData} = S, + state := State, data := Data} = S, Q) -> - try Module:terminate(Reason, State, StateData) of + try Module:terminate(Reason, State, Data) of _ -> ok catch _ -> ok; @@ -1137,7 +1139,7 @@ error_info( Class, Reason, Stacktrace, Debug, #{name := Name, callback_mode := CallbackMode, state := State, postponed := P}, - Q, FmtStateData) -> + Q, FmtData) -> {FixedReason,FixedStacktrace} = case Stacktrace of [{M,F,Args,_}|ST] @@ -1190,7 +1192,7 @@ error_info( [Event|_] -> [Event] end] ++ - [FmtStateData,Class,FixedReason, + [FmtData,Class,FixedReason, State,CallbackMode,length(Q),length(P)] ++ case FixedStacktrace of [] -> @@ -1205,22 +1207,22 @@ error_info( %% Call Module:format_status/2 or return a default value format_status( Opt, PDict, - #{module := Module, state := State, state_data := StateData}) -> + #{module := Module, state := State, data := Data}) -> case erlang:function_exported(Module, format_status, 2) of true -> - try Module:format_status(Opt, [PDict,State,StateData]) + try Module:format_status(Opt, [PDict,State,Data]) catch Result -> Result; _:_ -> - format_status_default(Opt, State, StateData) + format_status_default(Opt, State, Data) end; false -> - format_status_default(Opt, State, StateData) + format_status_default(Opt, State, Data) end. %% The default Module:format_status/2 -format_status_default(Opt, State, StateData) -> - SSD = {State,StateData}, +format_status_default(Opt, State, Data) -> + SSD = {State,Data}, case Opt of terminate -> SSD; diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 4ac4acd189..65a8d35645 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -573,10 +573,10 @@ call_format_status(Config) when is_list(Config) -> error_format_status(Config) when is_list(Config) -> error_logger_forwarder:register(), OldFl = process_flag(trap_exit, true), - StateData = "called format_status", + Data = "called format_status", {ok,Pid} = gen_statem:start( - ?MODULE, start_arg(Config, {state_data,StateData}), []), + ?MODULE, start_arg(Config, {data,Data}), []), %% bad return value in the gen_statem loop {{bad_return_value,badreturn},_} = ?EXPECT_FAILURE(gen_statem:call(Pid, badreturn), Reason), @@ -585,7 +585,7 @@ error_format_status(Config) when is_list(Config) -> {Pid, "** State machine"++_, [Pid,{{call,_},badreturn}, - {formatted,idle,StateData}, + {formatted,idle,Data}, exit,{bad_return_value,badreturn}|_]}} -> ok; Other when is_tuple(Other), element(1, Other) =:= error -> @@ -609,10 +609,10 @@ error_format_status(Config) when is_list(Config) -> terminate_crash_format(Config) when is_list(Config) -> error_logger_forwarder:register(), OldFl = process_flag(trap_exit, true), - StateData = crash_terminate, + Data = crash_terminate, {ok,Pid} = gen_statem:start( - ?MODULE, start_arg(Config, {state_data,StateData}), []), + ?MODULE, start_arg(Config, {data,Data}), []), stop_it(Pid), Self = self(), receive @@ -621,7 +621,7 @@ terminate_crash_format(Config) when is_list(Config) -> "** State machine"++_, [Pid, {{call,{Self,_}},stop}, - {formatted,idle,StateData}, + {formatted,idle,Data}, exit,{crash,terminate}|_]}} -> ok; Other when is_tuple(Other), element(1, Other) =:= error -> @@ -647,7 +647,7 @@ get_state(Config) when is_list(Config) -> State = self(), {ok,Pid} = gen_statem:start( - ?MODULE, start_arg(Config, {state_data,State}), []), + ?MODULE, start_arg(Config, {data,State}), []), {idle,State} = sys:get_state(Pid), {idle,State} = sys:get_state(Pid, 5000), stop_it(Pid), @@ -656,7 +656,7 @@ get_state(Config) when is_list(Config) -> %% already checked by the previous test) {ok,Pid2} = gen_statem:start( - {local,gstm}, ?MODULE, start_arg(Config, {state_data,State}), []), + {local,gstm}, ?MODULE, start_arg(Config, {data,State}), []), {idle,State} = sys:get_state(gstm), {idle,State} = sys:get_state(gstm, 5000), stop_it(Pid2), @@ -664,7 +664,7 @@ get_state(Config) when is_list(Config) -> %% check that get_state works when pid is sys suspended {ok,Pid3} = gen_statem:start( - ?MODULE, start_arg(Config, {state_data,State}), []), + ?MODULE, start_arg(Config, {data,State}), []), {idle,State} = sys:get_state(Pid3), ok = sys:suspend(Pid3), {idle,State} = sys:get_state(Pid3, 5000), @@ -676,7 +676,7 @@ replace_state(Config) when is_list(Config) -> State = self(), {ok, Pid} = gen_statem:start( - ?MODULE, start_arg(Config, {state_data,State}), []), + ?MODULE, start_arg(Config, {data,State}), []), {idle,State} = sys:get_state(Pid), NState1 = "replaced", Replace1 = fun({StateName, _}) -> {StateName,NState1} end, @@ -1124,8 +1124,8 @@ init(hiber) -> {ok,hiber_idle,[]}; init(hiber_now) -> {ok,hiber_idle,[],[hibernate]}; -init({state_data, StateData}) -> - {ok,idle,StateData}; +init({data, Data}) -> + {ok,idle,Data}; init({init_ops,Arg,InitOps}) -> case init(Arg) of {ok,State,Data,Ops} -> @@ -1136,7 +1136,7 @@ init({init_ops,Arg,InitOps}) -> Other end; init([]) -> - {ok,idle,state_data}. + {ok,idle,data}. terminate(_, _State, crash_terminate) -> exit({crash,terminate}); @@ -1362,20 +1362,20 @@ unwrap_state(State) -> wrap_result(Result) -> case Result of - {next_state,NewState,NewStateData} -> - {next_state,[NewState],NewStateData}; - {next_state,NewState,NewStateData,StateOps} -> - {next_state,[NewState],NewStateData,StateOps}; + {next_state,NewState,NewData} -> + {next_state,[NewState],NewData}; + {next_state,NewState,NewData,StateOps} -> + {next_state,[NewState],NewData,StateOps}; Other -> Other end. -code_change(_OldVsn, State, StateData, _Extra) -> - {ok,State,StateData}. +code_change(_OldVsn, State, Data, _Extra) -> + {ok,State,Data}. -format_status(terminate, [_Pdict,State,StateData]) -> - {formatted,State,StateData}; -format_status(normal, [_Pdict,_State,_StateData]) -> +format_status(terminate, [_Pdict,State,Data]) -> + {formatted,State,Data}; +format_status(normal, [_Pdict,_State,_Data]) -> [format_status_called]. -- cgit v1.2.3 From 867c27c5b03c846aef1e5fe4b3ee63de5f3a7f32 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 19 Feb 2016 15:36:13 +0100 Subject: Hide Data in default format_status/2 if callback crashes --- lib/stdlib/doc/src/gen_statem.xml | 13 ++++++++----- lib/stdlib/src/gen_statem.erl | 4 +++- 2 files changed, 11 insertions(+), 6 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 15e9584360..8462f5ff5f 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -1212,11 +1212,13 @@ erlang:'!' -----> Module:StateName/5 -

This callback is optional, so callback modules need not +

This callback is optional, so a callback module need not export it. The gen_statem module provides a default - implementation of this function that returns the callback - module state and data. The default function will also - be used if this callback fails. + implementation of this function that returns + {State,Data}. If this callback fails the default + function will return {State,Info} + where Info informs of the crash but no details, + to hide possibly sensitive data.

This function is called by a gen_statem process when:

@@ -1270,7 +1272,8 @@ erlang:'!' -----> Module:StateName/5

One use for this function is to return compact alternative state representations to avoid having large state terms - printed in logfiles. + printed in logfiles. Another is to hide sensitive data from + being written to the error log.

This function may use throw, diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 1ca2e1009c..b580eaab97 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -1214,7 +1214,9 @@ format_status( catch Result -> Result; _:_ -> - format_status_default(Opt, State, Data) + format_status_default( + Opt, State, + "Module:format_status/2 crashed") end; false -> format_status_default(Opt, State, Data) -- cgit v1.2.3 From 759548838fa8b27eaa574233c9897d9578540a5a Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Mon, 22 Feb 2016 14:55:52 +0100 Subject: Make callback_option() mandatory --- lib/stdlib/doc/src/gen_statem.xml | 80 ++++++++++----------- lib/stdlib/src/gen_statem.erl | 131 +++++++++++++++++------------------ lib/stdlib/test/gen_statem_SUITE.erl | 53 ++++++++------ 3 files changed, 131 insertions(+), 133 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 8462f5ff5f..bd210a0d22 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -190,7 +190,7 @@ erlang:'!' -----> Module:StateName/5

Name specification to use when starting - agen_statem server. + a gen_statem server. See start_link/3 and @@ -347,17 +347,6 @@ erlang:'!' -----> Module:StateName/5

- - - -

Option that only is valid when initializing - the gen_statem that is it can be returned from - Module:init/1 - or given to - enter_loop/5,6. -

-
-
@@ -459,7 +448,7 @@ erlang:'!' -----> Module:StateName/5 type timeout after Time milliseconds unless some other event is received before that time. Note that a retried - event counts just like a new in this respect. + or inserted event counts just like a new in this respect. If Time =:= infinity no timer is started. If Time =:= 0 the timeout event is immediately enqueued as the newest received. @@ -468,7 +457,7 @@ erlang:'!' -----> Module:StateName/5 transition_action() cancel_timer. - This timeout is cancelled automatically by any event. + This timeout is cancelled automatically by any other event. @@ -478,9 +467,9 @@ erlang:'!' -----> Module:StateName/5

The state transition actions are executed in the containing list order. This matters - for next_event where the last one in the list - will become the next event to present - to the state functions. Regarding the other actions + for next_event where the last such in the list + will become the next event to process by + the current state function. Regarding the other actions it is only for remove_event with EventPredicate and for reply_action() that the order may matter. @@ -490,7 +479,9 @@ erlang:'!' -----> Module:StateName/5 Reply to a calling client. next_event Insert the given EventType - and EventContent the next to process. + and EventContent as the next to process. + This will bypass any events in the process mailbox as well + as any other queued events. An event of type internal @@ -841,11 +832,11 @@ erlang:'!' -----> Module:StateName/5 - + Enter the gen_statem receive loop

The same as - enter_loop/6 + enter_loop/7 except that no server_name() @@ -854,12 +845,12 @@ erlang:'!' -----> Module:StateName/5 - + Enter the gen_statem receive loop

If Server_or_Ops is a list() the same as - enter_loop/6 + enter_loop/7 except that no server_name() @@ -867,7 +858,7 @@ erlang:'!' -----> Module:StateName/5 Ops = Server_or_Ops.

Otherwise the same as - enter_loop/6 + enter_loop/7 with Server = Server_or_Ops and Ops = []. @@ -875,7 +866,7 @@ erlang:'!' -----> Module:StateName/5 - + Enter the gen_statem receive loop

Makes an the calling process become a gen_statem. @@ -903,8 +894,8 @@ erlang:'!' -----> Module:StateName/5 server_name() name must have been registered accordingly before this function is called.

-

State, Data - and Ops +

CallbackMode, State, + Data and Ops have the same meanings as in the return value of Module:init/1. Also, the callback module Module @@ -938,15 +929,17 @@ erlang:'!' -----> Module:StateName/5 Initialize process and internal state Args = term() - Result = {ok,State,Data} -  | {ok,State,Data,Ops} + Result = {CallbackMode,State,Data} +  | {CallbackMode,State,Data,Ops}  | {stop,Reason} | ignore + CallbackMode = + callback_mode() + State = state() Data = data() Ops = - [transition_op() - | init_option()] + [transition_op()] Reason = term() @@ -962,21 +955,20 @@ erlang:'!' -----> Module:StateName/5

Args is the Args argument provided to the start function.

If the initialization is successful, the function should - return {ok,State,Data} or - {ok,State,Data,Ops}. - State is the state + return {CallbackMode,State,Data} or + {CallbackMode,State,Data,Ops}. + CallbackMode selects the + callback_mode(). of the gen_statem. + State is the state() + of the gen_statem and + Data the server data()

The Ops are executed when entering the first state just as for a state function.

-

This function allows an option to select the callback mode - of the gen_statem. See - init_option. - This option is not allowed from the state function(s). -

If something goes wrong during the initialization the function should return {stop,Reason} or ignore. See @@ -984,7 +976,7 @@ erlang:'!' -----> Module:StateName/5

This function may use throw, - to return its value. + to return Result.

@@ -1065,7 +1057,7 @@ erlang:'!' -----> Module:StateName/5

These functions may use throw, - to return its value. + to return Result.

@@ -1130,7 +1122,7 @@ erlang:'!' -----> Module:StateName/5

This function may use throw, - to return its value. + to return Ignored, which is ignored anyway.

@@ -1184,11 +1176,11 @@ erlang:'!' -----> Module:StateName/5

If the function returns Reason, the ongoing upgrade will fail and roll back to the old release.

-

This function may use throw, - to return its value. + to return Result or Reason.

+ @@ -1277,7 +1269,7 @@ erlang:'!' -----> Module:StateName/5

This function may use throw, - to return its value. + to return Status.

diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index b580eaab97..32799f5afc 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -24,7 +24,7 @@ [start/3,start/4,start_link/3,start_link/4, stop/1,stop/3, cast/2,call/2,call/3, - enter_loop/4,enter_loop/5,enter_loop/6, + enter_loop/5,enter_loop/6,enter_loop/7, reply/1,reply/2]). %% gen callbacks @@ -63,8 +63,6 @@ 'info' | 'timeout' | 'internal'. -type event_predicate() :: % Return true for the event in question fun((event_type(), term()) -> boolean()). --type init_option() :: - {'callback_mode', callback_mode()}. -type callback_mode() :: 'state_functions' | 'handle_event_function'. -type transition_op() :: %% First NewState and NewData are set, @@ -136,12 +134,12 @@ %% an {ok, ...} tuple. Thereafter the state callbacks are called %% for all events to this server. -callback init(Args :: term()) -> - {'ok', state(), data()} | - {'ok', state(), data(), [transition_op()|init_option()]} | + {callback_mode(), state(), data()} | + {callback_mode(), state(), data(), [transition_op()]} | 'ignore' | {'stop', Reason :: term()}. -%% Example callback for callback_mode =:= state_functions +%% Example state callback for callback_mode() =:= state_functions %% state name 'state_name'. %% %% In this mode all states has to be type state_name() i.e atom(). @@ -156,7 +154,7 @@ Data :: data()) -> state_callback_result(). %% -%% Callback for callback_mode =:= handle_event_function. +%% State callback for callback_mode() =:= handle_event_function. %% %% Note that state callbacks and only state callbacks have arity 5 %% and that is intended. @@ -199,7 +197,8 @@ StatusOption :: 'normal' | 'terminate'. -optional_callbacks( - [format_status/2, % Has got a default implementation + [init/1, % One may use enter_loop/5,6,7 instead + format_status/2, % Has got a default implementation %% state_name/5, % Example for callback_mode =:= state_functions: %% there has to be a StateName/5 callback function for every StateName. @@ -207,6 +206,16 @@ handle_event/5]). % For callback_mode =:= handle_event_function %% Type validation functions +callback_mode(CallbackMode) -> + case CallbackMode of + state_functions -> + true; + handle_event_function -> + true; + _ -> + false + end. +%% client({Pid,Tag}) when is_pid(Pid), is_reference(Tag) -> true; client(_) -> @@ -413,38 +422,41 @@ reply({To,Tag}, Reply) -> %% the same arguments as you would have returned from init/1 -spec enter_loop( Module :: module(), Opts :: [debug_opt()], + CallbackMode :: callback_mode(), State :: state(), Data :: data()) -> no_return(). -enter_loop(Module, Opts, State, Data) -> - enter_loop(Module, Opts, State, Data, self()). +enter_loop(Module, Opts, CallbackMode, State, Data) -> + enter_loop(Module, Opts, CallbackMode, State, Data, self()). %% -spec enter_loop( Module :: module(), Opts :: [debug_opt()], + CallbackMode :: callback_mode(), State :: state(), Data :: data(), Server_or_Ops :: - server_name() | pid() | [transition_op()|init_option()]) -> + server_name() | pid() | [transition_op()]) -> no_return(). -enter_loop(Module, Opts, State, Data, Server_or_Ops) -> +enter_loop(Module, Opts, CallbackMode, State, Data, Server_or_Ops) -> if is_list(Server_or_Ops) -> enter_loop( - Module, Opts, State, Data, + Module, Opts, CallbackMode, State, Data, self(), Server_or_Ops); true -> enter_loop( - Module, Opts, State, Data, + Module, Opts, CallbackMode, State, Data, Server_or_Ops, []) end. %% -spec enter_loop( Module :: module(), Opts :: [debug_opt()], + CallbackMode :: callback_mode(), State :: state(), Data :: data(), Server :: server_name() | pid(), - Ops :: [transition_op()|init_option()]) -> + Ops :: [transition_op()]) -> no_return(). -enter_loop(Module, Opts, State, Data, Server, Ops) -> +enter_loop(Module, Opts, CallbackMode, State, Data, Server, Ops) -> Parent = gen:get_parent(), - enter(Module, Opts, State, Data, Server, Ops, Parent). + enter(Module, Opts, CallbackMode, State, Data, Server, Ops, Parent). %%--------------------------------------------------------------------------- %% API helpers @@ -465,37 +477,40 @@ do_send(Proc, Msg) -> ok end. -%% Here init_it and all enter_loop functions converge -enter(Module, Opts, State, Data, Server, InitOps, Parent) -> - Name = gen:get_proc_name(Server), - Debug = gen:debug_options(Name, Opts), - PrevState = undefined, - S = #{ - callback_mode => state_functions, - module => Module, - name => Name, - prev_state => PrevState, - state => PrevState, % Will be discarded by loop_event_transition_ops - data => Data, - timer => undefined, - postponed => [], - hibernate => false}, - case collect_init_options(InitOps) of - {CallbackMode,Ops} -> +%% Here init_it/6 and enter_loop/5,6,7 functions converge +enter(Module, Opts, CallbackMode, State, Data, Server, Ops, Parent) + when is_atom(Module), is_pid(Parent) -> + case callback_mode(CallbackMode) of + true -> + Name = gen:get_proc_name(Server), + Debug = gen:debug_options(Name, Opts), + PrevState = undefined, + S = #{ + callback_mode => CallbackMode, + module => Module, + name => Name, + prev_state => PrevState, + state => PrevState, % Discarded by loop_event_transition_ops + data => Data, + timer => undefined, + postponed => [], + hibernate => false}, loop_event_transition_ops( - Parent, Debug, - S#{callback_mode := CallbackMode}, - [], - {event,undefined}, % Will be discarded by {postpone,false} + Parent, Debug, S, [], + {event,undefined}, % Discarded due to {postpone,false} PrevState, State, Data, Ops++[{postpone,false}]); - [Reason] -> - ?TERMINATE(Reason, Debug, S, []) + false -> + erlang:error( + badarg, + [Module,Opts,CallbackMode,State,Data,Server,Ops,Parent]) end. %%%========================================================================== %%% gen callbacks +init_it(Starter, self, ServerRef, Module, Args, Opts) -> + init_it(Starter, self(), ServerRef, Module, Args, Opts); init_it(Starter, Parent, ServerRef, Module, Args, Opts) -> try Module:init(Args) of Result -> @@ -514,12 +529,16 @@ init_it(Starter, Parent, ServerRef, Module, Args, Opts) -> init_result(Starter, Parent, ServerRef, Module, Result, Opts) -> case Result of - {ok,State,Data} -> + {CallbackMode,State,Data} -> proc_lib:init_ack(Starter, {ok,self()}), - enter(Module, Opts, State, Data, ServerRef, [], Parent); - {ok,State,Data,Ops} -> + enter( + Module, Opts, CallbackMode, State, Data, + ServerRef, [], Parent); + {CallbackMode,State,Data,Ops} -> proc_lib:init_ack(Starter, {ok,self()}), - enter(Module, Opts, State, Data, ServerRef, Ops, Parent); + enter( + Module, Opts, CallbackMode, State, Data, + ServerRef, Ops, Parent); {stop,Reason} -> gen:unregister_name(ServerRef), proc_lib:init_ack(Starter, {error,Reason}), @@ -881,30 +900,6 @@ loop_event_transition_ops( %%--------------------------------------------------------------------------- %% Server helpers -collect_init_options(InitOps) -> - if - is_list(InitOps) -> - collect_init_options(InitOps, state_functions, []); - true -> - collect_init_options([InitOps], state_functions, []) - end. -%% Keep the last of each kind -collect_init_options([], CallbackMode, Ops) -> - {CallbackMode,lists:reverse(Ops)}; -collect_init_options( - [InitOp|InitOps] = AllInitOps, CallbackMode, Ops) -> - case InitOp of - {callback_mode,Mode} - when Mode =:= state_functions; - Mode =:= handle_event_function -> - collect_init_options(InitOps, Mode, Ops); - {callback_mode,_} -> - [{bad_init_ops,AllInitOps}]; - _ -> % Collect others as Ops - collect_init_options( - InitOps, CallbackMode, [InitOp|Ops]) - end. - collect_transition_options(Ops) -> if is_list(Ops) -> diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 65a8d35645..a8b4d16f23 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -36,7 +36,8 @@ all() -> {group, abnormal}, {group, abnormal_handle_event}, shutdown, - {group, sys}, hibernate, enter_loop]. + {group, sys}, + hibernate, enter_loop]. groups() -> [{start, [], @@ -73,7 +74,7 @@ init_per_group(GroupName, Config) GroupName =:= stop_handle_event; GroupName =:= abnormal_handle_event; GroupName =:= sys_handle_event -> - [{init_ops,[{callback_mode,handle_event_function}]}|Config]; + [{callback_mode,handle_event_function}|Config]; init_per_group(_GroupName, Config) -> Config. @@ -86,6 +87,8 @@ init_per_testcase(_CaseName, Config) -> %%% dbg:tracer(), %%% dbg:p(all, c), %%% dbg:tpl(gen_statem, cx), +%%% dbg:tpl(proc_lib, cx), +%%% dbg:tpl(gen, cx), %%% dbg:tpl(sys, cx), [{watchdog, Dog} | Config]. @@ -901,8 +904,11 @@ enter_loop(Config) when is_list(Config) -> end, %% Process not started using proc_lib + CallbackMode = state_functions, Pid4 = - spawn_link(gen_statem, enter_loop, [?MODULE,[],state0,[]]), + spawn_link( + gen_statem, enter_loop, + [?MODULE,[],CallbackMode,state0,[]]), receive {'EXIT',Pid4,process_was_not_started_by_proc_lib} -> ok @@ -976,16 +982,21 @@ enter_loop(Reg1, Reg2) -> anon -> ignore end, proc_lib:init_ack({ok, self()}), + CallbackMode = state_functions, case Reg2 of local -> - gen_statem:enter_loop(?MODULE, [], state0, [], {local,armitage}); + gen_statem:enter_loop( + ?MODULE, [], CallbackMode, state0, [], {local,armitage}); global -> - gen_statem:enter_loop(?MODULE, [], state0, [], {global,armitage}); + gen_statem:enter_loop( + ?MODULE, [], CallbackMode, state0, [], {global,armitage}); via -> - gen_statem:enter_loop(?MODULE, [], state0, [], - {via, dummy_via, armitage}); + gen_statem:enter_loop( + ?MODULE, [], CallbackMode, state0, [], + {via, dummy_via, armitage}); anon -> - gen_statem:enter_loop(?MODULE, [], state0, []) + gen_statem:enter_loop( + ?MODULE, [], CallbackMode, state0, []) end. %% @@ -1098,9 +1109,9 @@ verify_empty_msgq() -> ok. start_arg(Config, Arg) -> - case lists:keyfind(init_ops, 1, Config) of - {_,Ops} -> - {init_ops,Arg,Ops}; + case lists:keyfind(callback_mode, 1, Config) of + {_,CallbackMode} -> + {callback_mode,CallbackMode,Arg}; false -> Arg end. @@ -1119,24 +1130,24 @@ init(stop_shutdown) -> {stop,shutdown}; init(sleep) -> ?t:sleep(1000), - {ok,idle,data}; + {state_functions,idle,data}; init(hiber) -> - {ok,hiber_idle,[]}; + {state_functions,hiber_idle,[]}; init(hiber_now) -> - {ok,hiber_idle,[],[hibernate]}; + {state_functions,hiber_idle,[],[hibernate]}; init({data, Data}) -> - {ok,idle,Data}; -init({init_ops,Arg,InitOps}) -> + {state_functions,idle,Data}; +init({callback_mode,CallbackMode,Arg}) -> case init(Arg) of - {ok,State,Data,Ops} -> - {ok,State,Data,InitOps++Ops}; - {ok,State,Data} -> - {ok,State,Data,InitOps}; + {_,State,Data,Ops} -> + {CallbackMode,State,Data,Ops}; + {_,State,Data} -> + {CallbackMode,State,Data}; Other -> Other end; init([]) -> - {ok,idle,data}. + {state_functions,idle,data}. terminate(_, _State, crash_terminate) -> exit({crash,terminate}); -- cgit v1.2.3 From 793fe40e3883bca7c37b71fb59616a4ec8d379b1 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 23 Feb 2016 16:55:45 +0100 Subject: Rename Client -> Caller --- lib/stdlib/doc/src/gen_statem.xml | 50 +++++++++++++++++++-------------------- lib/stdlib/src/gen_statem.erl | 42 ++++++++++++++++---------------- 2 files changed, 46 insertions(+), 46 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index bd210a0d22..5ae930f48b 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -270,13 +270,13 @@ erlang:'!' -----> Module:StateName/5
- + -

Client address to use when replying through for example the +

Destination to use when replying through for example the - transition_op() {reply,Client,Reply} + transition_op() {reply,Caller,Reply} - to a client that has called the gen_statem server using + to a process that has called the gen_statem server using call/2.

@@ -318,7 +318,7 @@ erlang:'!' -----> Module:StateName/5

External events are of 3 different type: - {call,Client}, cast or info. + {call,Caller}, cast or info. Calls (synchronous) and casts (asynchronous) originate from the corresponding API functions. For calls the event contain whom to reply to. @@ -476,7 +476,7 @@ erlang:'!' -----> Module:StateName/5

reply_action() - Reply to a calling client. + Reply to a caller. next_event Insert the given EventType and EventContent as the next to process. @@ -525,11 +525,11 @@ erlang:'!' -----> Module:StateName/5 -

Reply to a client that called +

Reply to a caller waiting for a reply in call/2. - Client must be the term from the + Caller must be the term from the - {call,Client} + {call,Caller} argument to the state function.

@@ -747,13 +747,13 @@ erlang:'!' -----> Module:StateName/5 The gen_statem will call the state function with event_type() - {call,Client} and event content + {call,Caller} and event content Request.

A Reply is generated when a state function returns with - {reply,Client,Reply} as one + {reply,Caller,Reply} as one transition_op() , @@ -803,21 +803,21 @@ erlang:'!' -----> Module:StateName/5 - Send a reply to a client + Reply to a caller

This function can be used by a gen_statem - to explicitly send a reply to a client that called + to explicitly send a reply to a process that waits in call/2 when the reply cannot be defined in - the return value of the + the return value of a state function.

-

Client must be the term from the +

Caller must be the term from the - {call,Client} + {call,Caller} argument to the state function. - Client and Reply + Caller and Reply can also be specified using a reply_action() @@ -975,7 +975,7 @@ erlang:'!' -----> Module:StateName/5 start_link/3,4.

This function may use - throw, + throw to return Result.

@@ -1024,16 +1024,16 @@ erlang:'!' -----> Module:StateName/5 then Module:handle_event/5 is called.

If EventType is - {call,Client} - the client is waiting for a reply. The reply can be sent + {call,Caller} + the caller is waiting for a reply. The reply can be sent from this or from any other state function - by returning with {reply,Client,Reply} in + by returning with {reply,Caller,Reply} in Ops, in Replies or by calling - reply(Client, Reply) + reply(Caller, Reply) .

StateName is useful in some odd cases for example @@ -1121,7 +1121,7 @@ erlang:'!' -----> Module:StateName/5 .

This function may use - throw, + throw to return Ignored, which is ignored anyway.

@@ -1177,7 +1177,7 @@ erlang:'!' -----> Module:StateName/5

If the function returns Reason, the ongoing upgrade will fail and roll back to the old release.

This function may use - throw, + throw to return Result or Reason.

@@ -1268,7 +1268,7 @@ erlang:'!' -----> Module:StateName/5 being written to the error log.

This function may use - throw, + throw to return Status.

diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 32799f5afc..48dfaec42a 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -51,7 +51,7 @@ %%% Interface functions. %%%========================================================================== --type client() :: +-type caller() :: {To :: pid(), Tag :: term()}. % Reply-to specifier for call -type state() :: state_name() | % For state callback function StateName/5 @@ -59,7 +59,7 @@ -type state_name() :: atom(). -type data() :: term(). -type event_type() :: - {'call',Client :: client()} | 'cast' | + {'call',Caller :: caller()} | 'cast' | 'info' | 'timeout' | 'internal'. -type event_predicate() :: % Return true for the event in question fun((event_type(), term()) -> boolean()). @@ -101,8 +101,8 @@ {'unlink', % Unlink and clean up mess(ages) Id :: pid() | port()}. -type reply_action() :: - {'reply', % Reply to a client - Client :: client(), Reply :: term()}. + {'reply', % Reply to a caller + Caller :: caller(), Reply :: term()}. -type state_callback_result() :: {'stop', % Stop the server Reason :: term()} | @@ -216,13 +216,13 @@ callback_mode(CallbackMode) -> false end. %% -client({Pid,Tag}) when is_pid(Pid), is_reference(Tag) -> +caller({Pid,Tag}) when is_pid(Pid), is_reference(Tag) -> true; -client(_) -> +caller(_) -> false. %% -event_type({call,Client}) -> - client(Client); +event_type({call,Caller}) -> + caller(Caller); event_type(Type) -> case Type of cast -> @@ -341,7 +341,7 @@ cast(ServerRef, Msg) when is_pid(ServerRef) -> do_send(ServerRef, cast(Msg)). %% Call a state machine (synchronous; a reply is expected) that -%% arrives with type {call,Client} +%% arrives with type {call,Caller} -spec call(ServerRef :: server_ref(), Request :: term()) -> Reply :: term(). call(ServerRef, Request) -> call(ServerRef, Request, infinity). @@ -400,13 +400,13 @@ call(ServerRef, Request, Timeout) -> %% Reply from a state machine callback to whom awaits in call/2 -spec reply([reply_action()] | reply_action()) -> ok. -reply({reply,{_To,_Tag}=Client,Reply}) -> - reply(Client, Reply); +reply({reply,{_To,_Tag}=Caller,Reply}) -> + reply(Caller, Reply); reply(Replies) when is_list(Replies) -> [reply(Reply) || Reply <- Replies], ok. %% --spec reply(Client :: client(), Reply :: term()) -> ok. +-spec reply(Caller :: caller(), Reply :: term()) -> ok. reply({To,Tag}, Reply) -> Msg = {Tag,Reply}, try To ! Msg of @@ -703,8 +703,8 @@ loop_receive(Parent, Debug, #{timer := Timer} = S) -> _ -> Event = case Msg of - {'$gen_call',Client,Request} -> - {{call,Client},Request}; + {'$gen_call',Caller,Request} -> + {{call,Caller},Request}; {'$gen_cast',E} -> {cast,E}; _ -> @@ -950,8 +950,8 @@ process_transition_actions([], Debug, _S, Q, P) -> process_transition_actions( [Action|Actions] = AllActions, Debug, S, Q, P) -> case Action of - {reply,{_To,_Tag}=Client,Reply} -> - NewDebug = do_reply(Debug, S, Client, Reply), + {reply,{_To,_Tag}=Caller,Reply} -> + NewDebug = do_reply(Debug, S, Caller, Reply), process_transition_actions(Actions, NewDebug, S, Q, P); {next_event,Type,Content} -> case event_type(Type) of @@ -987,17 +987,17 @@ reply_then_terminate(Class, Reason, Stacktrace, Debug, S, Q, []) -> reply_then_terminate( Class, Reason, Stacktrace, Debug, S, Q, [R|Rs] = RRs) -> case R of - {reply,{_To,_Tag}=Client,Reply} -> - NewDebug = do_reply(Debug, S, Client, Reply), + {reply,{_To,_Tag}=Caller,Reply} -> + NewDebug = do_reply(Debug, S, Caller, Reply), reply_then_terminate( Class, Reason, Stacktrace, NewDebug, S, Q, Rs); _ -> RRs % bad_return_value end. -do_reply(Debug, S, Client, Reply) -> - reply(Client, Reply), - sys_debug(Debug, S, {out,Reply,Client}). +do_reply(Debug, S, Caller, Reply) -> + reply(Caller, Reply), + sys_debug(Debug, S, {out,Reply,Caller}). %% Remove oldest matching event from the queue(s) -- cgit v1.2.3 From 2b3a82ae44b222ce1146badcff972abb539d40ca Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 23 Feb 2016 17:58:45 +0100 Subject: Add {stop_and_reply,Reason,Replies [,Data]) Cleanup some error handling --- lib/stdlib/doc/src/gen_statem.xml | 9 +++- lib/stdlib/src/gen_statem.erl | 83 ++++++++++++++++++++---------------- lib/stdlib/test/gen_statem_SUITE.erl | 8 ++-- 3 files changed, 58 insertions(+), 42 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 5ae930f48b..096be3025e 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -540,7 +540,14 @@ erlang:'!' -----> Module:StateName/5 stop - Send all Replies if given, + Terminate the gen_statem by calling + + Module:terminate/3 + with Reason and + NewData, if given. + + stop_and_reply + Send all Replies then terminate the gen_statem by calling Module:terminate/3 diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 48dfaec42a..02c8d60c61 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -109,7 +109,10 @@ {'stop', % Stop the server Reason :: term(), NewData :: data()} | - {'stop', % Stop the server + {'stop_and_reply', % Reply then stop the server + Reason :: term(), + Replies :: [reply_action()] | reply_action()} | + {'stop_and_reply', % Reply then stop the server Reason :: term(), Replies :: [reply_action()] | reply_action(), NewData :: data()} | @@ -244,9 +247,9 @@ event_type(Type) -> try throw(ok) catch _ -> erlang:get_stacktrace() end). -define( - TERMINATE(Reason, Debug, S, Q), + TERMINATE(Class, Reason, Debug, S, Q), terminate( - exit, + Class, begin Reason end, ?STACKTRACE(), begin Debug end, @@ -560,7 +563,7 @@ system_continue(Parent, Debug, S) -> loop(Parent, Debug, S). system_terminate(Reason, _Parent, Debug, S) -> - ?TERMINATE(Reason, Debug, S, []). + ?TERMINATE(exit, Reason, Debug, S, []). system_code_change( #{module := Module, @@ -696,7 +699,7 @@ loop_receive(Parent, Debug, #{timer := Timer} = S) -> %% EXIT is not a 2-tuple and therefore %% not an event and has no event_type(), %% but this will stand out in the crash report... - ?TERMINATE(Reason, Debug, S, [EXIT]); + ?TERMINATE(exit, Reason, Debug, S, [EXIT]); {timeout,Timer,Content} when Timer =/= undefined -> loop_receive( Parent, Debug, S, {timeout,Content}, undefined); @@ -780,29 +783,26 @@ loop_event_result( Events, Event, Result) -> case Result of {stop,Reason} -> - ?TERMINATE(Reason, Debug, S, [Event|Events]); + ?TERMINATE(exit, Reason, Debug, S, [Event|Events]); {stop,Reason,NewData} -> - ?TERMINATE( - Reason, Debug, - S#{data := NewData}, - [Event|Events]); - {stop,Reason,Reply,NewData} -> NewS = S#{data := NewData}, Q = [Event|Events], - Replies = - if - is_list(Reply) -> - Reply; - true -> - [Reply] - end, - BadReplies = + ?TERMINATE(exit, Reason, Debug, NewS, Q); + {stop_and_reply,Reason,Replies} -> + Q = [Event|Events], + [Class,NewReason,Stacktrace,NewDebug] = + reply_then_terminate( + exit, Reason, ?STACKTRACE(), Debug, S, Q, Replies), + %% Since we got back here Replies was bad + terminate(Class, NewReason, Stacktrace, NewDebug, S, Q); + {stop_and_reply,Reason,Replies,NewData} -> + NewS = S#{data := NewData}, + Q = [Event|Events], + [Class,NewReason,Stacktrace,NewDebug] = reply_then_terminate( exit, Reason, ?STACKTRACE(), Debug, NewS, Q, Replies), %% Since we got back here Replies was bad - ?TERMINATE( - {bad_return_value,{stop,Reason,BadReplies,NewData}}, - Debug, NewS, Q); + terminate(Class, NewReason, Stacktrace, NewDebug, NewS, Q); {next_state,NewState,NewData} -> loop_event_transition_ops( Parent, Debug, S, Events, Event, @@ -830,7 +830,7 @@ loop_event_result( State, State, Data, Ops); _ -> ?TERMINATE( - {bad_return_value,Result}, Debug, S, [Event|Events]) + error, {bad_return_value,Result}, Debug, S, [Event|Events]) end. loop_event_transition_ops( @@ -886,15 +886,14 @@ loop_event_transition_ops( hibernate := Hibernate, postponed := P}, Q, Timer); - [Reason,Debug] -> - ?TERMINATE(Reason, Debug, S, [Event|Events]); [Class,Reason,Stacktrace,Debug] -> terminate( Class, Reason, Stacktrace, Debug, S, [Event|Events]) end; %% - [Reason] -> - ?TERMINATE(Reason, Debug0, S, [Event|Events]) + [Class,Reason,Stacktrace] -> + terminate( + Class, Reason, Stacktrace, Debug0, S, [Event|Events]) end. %%--------------------------------------------------------------------------- @@ -923,7 +922,7 @@ collect_transition_options( collect_transition_options( Ops, NewPostpone, Hibernate, Timeout, Actions); {postpone,_} -> - [{bad_ops,AllOps}]; + [error,{bad_ops,AllOps},?STACKTRACE()]; hibernate -> collect_transition_options( Ops, Postpone, true, Timeout, Actions); @@ -931,7 +930,7 @@ collect_transition_options( collect_transition_options( Ops, Postpone, NewHibernate, Timeout, Actions); {hibernate,_} -> - [{bad_ops,AllOps}]; + [error,{bad_ops,AllOps},?STACKTRACE()]; {timeout,infinity,_} -> % Ignore since it will never time out collect_transition_options( Ops, Postpone, Hibernate, undefined, Actions); @@ -939,7 +938,7 @@ collect_transition_options( collect_transition_options( Ops, Postpone, Hibernate, NewTimeout, Actions); {timeout,_,_} -> - [{bad_ops,AllOps}]; + [error,{bad_ops,AllOps},?STACKTRACE()]; _ -> % Collect others as actions collect_transition_options( Ops, Postpone, Hibernate, Timeout, [Op|Actions]) @@ -959,7 +958,7 @@ process_transition_actions( process_transition_actions( Actions, Debug, S, [{Type,Content}|Q], P); false -> - [{bad_ops,AllActions},Debug] + [error,{bad_ops,AllActions},?STACKTRACE(),Debug] end; _ -> %% All others are remove actions @@ -968,7 +967,7 @@ process_transition_actions( process_transition_actions( Actions, Debug, S, Q, P); undefined -> - [{bad_ops,AllActions},Debug]; + [error,{bad_ops,AllActions},?STACKTRACE(),Debug]; RemoveFun when is_function(RemoveFun, 2) -> case remove_event(RemoveFun, Q, P) of {NewQ,NewP} -> @@ -982,17 +981,27 @@ process_transition_actions( end end. -reply_then_terminate(Class, Reason, Stacktrace, Debug, S, Q, []) -> +reply_then_terminate(Class, Reason, Stacktrace, Debug, S, Q, Replies) -> + if + is_list(Replies) -> + do_reply_then_terminate( + Class, Reason, Stacktrace, Debug, S, Q, Replies); + true -> + do_reply_then_terminate( + Class, Reason, Stacktrace, Debug, S, Q, [Replies]) + end. +%% +do_reply_then_terminate(Class, Reason, Stacktrace, Debug, S, Q, []) -> terminate(Class, Reason, Stacktrace, Debug, S, Q); -reply_then_terminate( - Class, Reason, Stacktrace, Debug, S, Q, [R|Rs] = RRs) -> +do_reply_then_terminate( + Class, Reason, Stacktrace, Debug, S, Q, [R|Rs] = Replies) -> case R of {reply,{_To,_Tag}=Caller,Reply} -> NewDebug = do_reply(Debug, S, Caller, Reply), - reply_then_terminate( + do_reply_then_terminate( Class, Reason, Stacktrace, NewDebug, S, Q, Rs); _ -> - RRs % bad_return_value + [error,{bad_replies,Replies},?STACKTRACE(),Debug] end. do_reply(Debug, S, Caller, Reply) -> diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index a8b4d16f23..81182eff71 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -589,7 +589,7 @@ error_format_status(Config) when is_list(Config) -> "** State machine"++_, [Pid,{{call,_},badreturn}, {formatted,idle,Data}, - exit,{bad_return_value,badreturn}|_]}} -> + error,{bad_return_value,badreturn}|_]}} -> ok; Other when is_tuple(Other), element(1, Other) =:= error -> error_logger_forwarder:unregister(), @@ -1268,7 +1268,7 @@ connected(Type, Content, PrevState, State, Data) -> end. state0({call,From}, stop, _, _, Data) -> - {stop,normal,[{reply,From,stopped}],Data}; + {stop_and_reply,normal,[{reply,From,stopped}],Data}; state0(Type, Content, PrevState, State, Data) -> case handle_common_events(Type, Content, PrevState, State, Data) of undefined -> @@ -1332,11 +1332,11 @@ handle_common_events(cast, {get,Pid}, _, State, Data) -> Pid ! {state,State,Data}, {next_state,State,Data}; handle_common_events({call,From}, stop, _, _, Data) -> - {stop,normal,[{reply,From,stopped}],Data}; + {stop_and_reply,normal,[{reply,From,stopped}],Data}; handle_common_events(cast, stop, _, _, _) -> {stop,normal}; handle_common_events({call,From}, {stop,Reason}, _, _, Data) -> - {stop,Reason,{reply,From,stopped},Data}; + {stop_and_reply,Reason,{reply,From,stopped},Data}; handle_common_events(cast, {stop,Reason}, _, _, _) -> {stop,Reason}; handle_common_events({call,From}, 'alive?', _, State, Data) -> -- cgit v1.2.3 From 1958b93b4aa90883be5102d465f67f167549dea9 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Wed, 24 Feb 2016 09:41:34 +0100 Subject: Ditch State so StateName/5 -> StateName/4 --- lib/stdlib/doc/src/gen_statem.xml | 64 ++++++++------- lib/stdlib/src/gen_statem.erl | 36 +++++--- lib/stdlib/test/gen_statem_SUITE.erl | 155 +++++++++++++++++------------------ 3 files changed, 136 insertions(+), 119 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 096be3025e..8934d912c6 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -62,7 +62,7 @@ gen_statem:stop -----> Module:terminate/2 gen_statem:call gen_statem:cast erlang:send -erlang:'!' -----> Module:StateName/5 +erlang:'!' -----> Module:StateName/4 Module:handle_event/5 - -----> Module:terminate/3 @@ -84,18 +84,28 @@ erlang:'!' -----> Module:StateName/5 in a gen_statem is the callback function that is called for all events in this state, and is selected depending on callback_mode - that the implementation selects during gen_statem init. + that the implementation specifies during gen_statem init.

When callback_mode is state_functions the state has to be an atom and is used as the state function name. See - - Module:StateName/5 + + Module:StateName/4 . This gathers all code for a specific state in one function and hence dispatches on state first. + Note that in this mode the callback function + + Module:code_change/4 + makes the state name code_change + unusable. Actually you might get away with using it + as a state name but you will have to find a way to separate + the two callback roles e.g by observing that you can force + argument 3 into having different types for the different roles. + This is a slippery slope, though. It is easier + to simply not use code_change as a state name.

When callback_mode @@ -110,13 +120,11 @@ erlang:'!' -----> Module:StateName/5 states so you do not accidentally postpone one event forever creating an infinite busy loop.

-

Any state name or any state value (depending on - callback_mode) - is permitted with a small gotcha regarding the state - undefined that is used as the previous state when - the first gen_statem state function is called. - You might need to know about this faked state if you - inspect the previous state argument in your state functions. +

There is a small gotcha in both callback modes regarding + the state undefined that is used as the previous state + when the first state function is called. So either do not + use undefined as a state, or figure out + the implications yourself.

The gen_statem enqueues incoming events in order of arrival and presents these to the @@ -181,7 +189,7 @@ erlang:'!' -----> Module:StateName/5 for a long time. However use this feature with care since hibernation implies at least two garbage collections (when hibernating and shortly after waking up) and that is not - something you'd want to do between each event on a busy server. + something you would want to do between each event on a busy server.

@@ -350,13 +358,20 @@ erlang:'!' -----> Module:StateName/5 +

+ The callback_mode is selected when starting the + gen_statem using the return value from + Module:init/1 + or when calling + enter_loop/5-7. +

state_functions The state has to be of type state_name() and one callback function per state that is - - Module:StateName/5 + + Module:StateName/4 is used. This is the default. handle_event_function @@ -384,7 +399,7 @@ erlang:'!' -----> Module:StateName/5 state function, from Module:init/1 or given to - enter_loop/5,6. + enter_loop/6,7.

The processing order for a state change is:

@@ -990,7 +1005,7 @@ erlang:'!' -----> Module:StateName/5 Module:StateName(EventType, EventContent, - PrevStateName, StateName, Data) -> Result + PrevStateName, Data) -> Result Module:handle_event(EventType, EventContent, PrevState, State, Data) -> Result @@ -1004,9 +1019,6 @@ erlang:'!' -----> Module:StateName/5 PrevStateName = state_name() - StateName = - state_name() - PrevState = State = state() @@ -1023,10 +1035,9 @@ erlang:'!' -----> Module:StateName/5

Whenever a gen_statem receives an event from call/2, cast/2 or - as a normal process message this function is called. - If - callback_mode - is state_functions then Module:StateName/5 is called, + as a normal process message one of these functions is called. + If callback_mode + is state_functions then Module:StateName/4 is called, and if it is handle_event_function then Module:handle_event/5 is called.

@@ -1043,13 +1054,10 @@ erlang:'!' -----> Module:StateName/5 reply(Caller, Reply)
.

-

StateName is useful in some odd cases for example - if you call a common event handling function from your state - function then you might want to pass StateName. -

PrevStateName and PrevState are useful in some odd cases for example when you want to do something - only at the first event in a state. + only at the first event in a state, or when you need to + handle events differently depending on the previous state. Note that when gen_statem enters its first state this is set to undefined.

diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 02c8d60c61..fe84a428f6 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -152,8 +152,7 @@ -callback state_name( event_type(), EventContent :: term(), - PrevStateName :: state_name() | reference(), - StateName :: state_name(), % Current state + PrevStateName :: state_name(), % Previous state name Data :: data()) -> state_callback_result(). %% @@ -164,7 +163,7 @@ -callback handle_event( event_type(), EventContent :: term(), - PrevState :: state(), + PrevState :: state(), % Previous state State :: state(), % Current state Data :: data()) -> state_callback_result(). @@ -203,7 +202,7 @@ [init/1, % One may use enter_loop/5,6,7 instead format_status/2, % Has got a default implementation %% - state_name/5, % Example for callback_mode =:= state_functions: + state_name/4, % Example for callback_mode =:= state_functions: %% there has to be a StateName/5 callback function for every StateName. %% handle_event/5]). % For callback_mode =:= handle_event_function @@ -740,14 +739,13 @@ loop_events( [{Type,Content} = Event|Events] = Q, Timer) -> _ = (Timer =/= undefined) andalso cancel_timer(Timer), - Func = + try case CallbackMode of - handle_event_function -> - handle_event; state_functions -> - State - end, - try Module:Func(Type, Content, PrevState, State, Data) of + Module:State(Type, Content, PrevState, Data); + handle_event_function -> + Module:handle_event(Type, Content, PrevState, State, Data) + end of Result -> loop_event_result( Parent, Debug, S, Events, Event, Result) @@ -759,15 +757,27 @@ loop_events( %% Process an undef to check for the simple mistake %% of calling a nonexistent state function case erlang:get_stacktrace() of - [{Module,Func, - [Type,Content,PrevState,State,Data]=Args, + [{Module,State, + [Type,Content,PrevState,Data]=Args, _} - |Stacktrace] -> + |Stacktrace] + when CallbackMode =:= state_functions -> terminate( error, {undef_state_function,{Module,State,Args}}, Stacktrace, Debug, S, Q); + [{Module,handle_event, + [Type,Content,PrevState,State,Data]=Args, + _} + |Stacktrace] + when CallbackMode =:= handle_event_function -> + terminate( + error, + {undef_state_function, + {Module,handle_event,Args}}, + Stacktrace, + Debug, S, Q); Stacktrace -> terminate(error, undef, Stacktrace, Debug, S, Q) end; diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 81182eff71..3302171329 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -1160,73 +1160,73 @@ terminate(_Reason, _State, _Data) -> %% State functions -idle(cast, {connect,Pid}, _, _, Data) -> +idle(cast, {connect,Pid}, _, Data) -> Pid ! accept, {next_state,wfor_conf,Data}; -idle({call,From}, connect, _, _, Data) -> +idle({call,From}, connect, _, Data) -> gen_statem:reply(From, accept), {next_state,wfor_conf,Data}; -idle(cast, badreturn, _, _, _Data) -> +idle(cast, badreturn, _, _Data) -> badreturn; -idle({call,_From}, badreturn, _, _, _Data) -> +idle({call,_From}, badreturn, _, _Data) -> badreturn; -idle({call,From}, {delayed_answer,T}, _, _, Data) -> +idle({call,From}, {delayed_answer,T}, _, Data) -> receive after T -> gen_statem:reply({reply,From,delayed}), throw({keep_state,Data}) end; -idle({call,From}, {timeout,Time}, _, State, _Data) -> +idle({call,From}, {timeout,Time}, _, _Data) -> {next_state,timeout,{From,Time}, - [{timeout,Time,State}]}; -idle(Type, Content, PrevState, State, Data) -> - case handle_common_events(Type, Content, PrevState, State, Data) of + [{timeout,Time,idle}]}; +idle(Type, Content, PrevState, Data) -> + case handle_common_events(Type, Content, idle, Data) of undefined -> case Type of {call,From} -> throw({keep_state,Data,[{reply,From,'eh?'}]}); _ -> throw( - {stop,{unexpected,State,PrevState,Type,Content}}) + {stop,{unexpected,idle,Type,Content,PrevState}}) end; Result -> Result end. -timeout(timeout, idle, idle, timeout, {From,Time}) -> +timeout(timeout, idle, idle, {From,Time}) -> TRef2 = erlang:start_timer(Time, self(), ok), TRefC1 = erlang:start_timer(Time, self(), cancel1), TRefC2 = erlang:start_timer(Time, self(), cancel2), {next_state,timeout2,{From,Time,TRef2}, [{cancel_timer, TRefC1}, {next_event,internal,{cancel_timer,TRefC2}}]}; -timeout(_, _, _, State, Data) -> - {next_state,State,Data}. +timeout(_, _, _, Data) -> + {keep_state,Data}. timeout2( - internal, {cancel_timer,TRefC2}, timeout, _, {From,Time,TRef2}) -> + internal, {cancel_timer,TRefC2}, timeout, {From,Time,TRef2}) -> Time4 = Time * 4, receive after Time4 -> ok end, {next_state,timeout3,{From,TRef2}, [{cancel_timer,TRefC2}]}; -timeout2(_, _, _, State, Data) -> - {next_state,State,Data}. +timeout2(_, _, _, Data) -> + {keep_state,Data}. -timeout3(info, {timeout,TRef2,Result}, _, _, {From,TRef2}) -> +timeout3(info, {timeout,TRef2,Result}, _, {From,TRef2}) -> gen_statem:reply([{reply,From,Result}]), {next_state,idle,state}; -timeout3(_, _, _, State, Data) -> - {next_state,State,Data}. +timeout3(_, _, _, Data) -> + {keep_state,Data}. -wfor_conf({call,From}, confirm, _, _, Data) -> +wfor_conf({call,From}, confirm, _, Data) -> {next_state,connected,Data, [{reply,From,yes}]}; -wfor_conf(cast, {ping,_,_}, _, _, _) -> +wfor_conf(cast, {ping,_,_}, _, _) -> {keep_state_and_data,[postpone]}; -wfor_conf(cast, confirm, _, _, Data) -> +wfor_conf(cast, confirm, _, Data) -> {next_state,connected,Data}; -wfor_conf(Type, Content, PrevState, State, Data) -> - case handle_common_events(Type, Content, PrevState, State, Data) of +wfor_conf(Type, Content, _, Data) -> + case handle_common_events(Type, Content, wfor_conf, Data) of undefined -> case Type of {call,From} -> @@ -1239,113 +1239,113 @@ wfor_conf(Type, Content, PrevState, State, Data) -> Result end. -connected({call,From}, {msg,Ref}, _, State, Data) -> - {next_state,State,Data, +connected({call,From}, {msg,Ref}, _, Data) -> + {keep_state,Data, [{reply,From,{ack,Ref}}]}; -connected(cast, {msg,From,Ref}, _, State, Data) -> +connected(cast, {msg,From,Ref}, _, Data) -> From ! {ack,Ref}, - {next_state,State,Data}; -connected({call,From}, disconnect, _, _, Data) -> + {keep_state,Data}; +connected({call,From}, disconnect, _, Data) -> {next_state,idle,Data, [{reply,From,yes}]}; -connected(cast, disconnect, _, _, Data) -> +connected(cast, disconnect, _, Data) -> {next_state,idle,Data}; -connected(cast, {ping,Pid,Tag}, _, State, Data) -> +connected(cast, {ping,Pid,Tag}, _, Data) -> Pid ! {pong,Tag}, - {next_state,State,Data}; -connected(Type, Content, PrevState, State, Data) -> - case handle_common_events(Type, Content, PrevState, State, Data) of + {keep_state,Data}; +connected(Type, Content, _, Data) -> + case handle_common_events(Type, Content, connected, Data) of undefined -> case Type of {call,From} -> - {next_state,State,Data, + {keep_state,Data, [{reply,From,'eh?'}]}; _ -> - {next_state,State,Data} + {keep_state,Data} end; Result -> Result end. -state0({call,From}, stop, _, _, Data) -> +state0({call,From}, stop, _, Data) -> {stop_and_reply,normal,[{reply,From,stopped}],Data}; -state0(Type, Content, PrevState, State, Data) -> - case handle_common_events(Type, Content, PrevState, State, Data) of +state0(Type, Content, _, Data) -> + case handle_common_events(Type, Content, state0, Data) of undefined -> - {next_state,State,Data}; + {keep_state,Data}; Result -> Result end. -hiber_idle({call,From}, 'alive?', _, State, Data) -> - {next_state,State,Data, +hiber_idle({call,From}, 'alive?', _, Data) -> + {keep_state,Data, [{reply,From,'alive!'}]}; -hiber_idle({call,From}, hibernate_sync, _, _, Data) -> +hiber_idle({call,From}, hibernate_sync, _, Data) -> {next_state,hiber_wakeup,Data, [{reply,From,hibernating}, hibernate]}; -hiber_idle(info, hibernate_later, _, State, _) -> +hiber_idle(info, hibernate_later, _, _) -> Tref = erlang:start_timer(1000, self(), hibernate), - {next_state,State,Tref}; -hiber_idle(info, hibernate_now, _, State, Data) -> - {next_state,State,Data, + {keep_state,Tref}; +hiber_idle(info, hibernate_now, _, Data) -> + {keep_state,Data, [hibernate]}; -hiber_idle(info, {timeout,Tref,hibernate}, _, State, Tref) -> - {next_state,State,[], +hiber_idle(info, {timeout,Tref,hibernate}, _, Tref) -> + {keep_state,[], [hibernate]}; -hiber_idle(cast, hibernate_async, _, _, Data) -> +hiber_idle(cast, hibernate_async, _, Data) -> {next_state,hiber_wakeup,Data, [hibernate]}; -hiber_idle(Type, Content, PrevState, State, Data) -> - case handle_common_events(Type, Content, PrevState, State, Data) of +hiber_idle(Type, Content, _, Data) -> + case handle_common_events(Type, Content, hiber_idle, Data) of undefined -> - {next_state,State,Data}; + {keep_state,Data}; Result -> Result end. -hiber_wakeup({call,From}, wakeup_sync, _, _, Data) -> +hiber_wakeup({call,From}, wakeup_sync, _, Data) -> {next_state,hiber_idle,Data, [{reply,From,good_morning}]}; -hiber_wakeup({call,From}, snooze_sync, _, State, Data) -> - {next_state,State,Data, +hiber_wakeup({call,From}, snooze_sync, _, Data) -> + {keep_state,Data, [{reply,From,please_just_five_more}, hibernate]}; -hiber_wakeup(cast, wakeup_async, _, _, Data) -> +hiber_wakeup(cast, wakeup_async, _, Data) -> {next_state,hiber_idle,Data}; -hiber_wakeup(cast, snooze_async, _, State, Data) -> - {next_state,State,Data, +hiber_wakeup(cast, snooze_async, _, Data) -> + {keep_state,Data, [hibernate]}; -hiber_wakeup(Type, Content, PrevState, State, Data) -> - case handle_common_events(Type, Content, PrevState, State, Data) of +hiber_wakeup(Type, Content, _, Data) -> + case handle_common_events(Type, Content, hiber_wakeup, Data) of undefined -> - {next_state,State,Data}; + {keep_state,Data}; Result -> Result end. -handle_common_events({call,From}, get, _, State, Data) -> - {next_state,State,Data, +handle_common_events({call,From}, get, State, Data) -> + {keep_state,Data, [{reply,From,{state,State,Data}}]}; -handle_common_events(cast, {get,Pid}, _, State, Data) -> +handle_common_events(cast, {get,Pid}, State, Data) -> Pid ! {state,State,Data}, - {next_state,State,Data}; -handle_common_events({call,From}, stop, _, _, Data) -> + {keep_state,Data}; +handle_common_events({call,From}, stop, _, Data) -> {stop_and_reply,normal,[{reply,From,stopped}],Data}; -handle_common_events(cast, stop, _, _, _) -> +handle_common_events(cast, stop, _, _) -> {stop,normal}; -handle_common_events({call,From}, {stop,Reason}, _, _, Data) -> +handle_common_events({call,From}, {stop,Reason}, _, Data) -> {stop_and_reply,Reason,{reply,From,stopped},Data}; -handle_common_events(cast, {stop,Reason}, _, _, _) -> - {stop,Reason}; -handle_common_events({call,From}, 'alive?', _, State, Data) -> - {next_state,State,Data, +handle_common_events(cast, {stop,Reason}, _, _) -> + {stop,Reason}; +handle_common_events({call,From}, 'alive?', _, Data) -> + {keep_state,Data, [{reply,From,yes}]}; -handle_common_events(cast, {'alive?',Pid}, _, State, Data) -> +handle_common_events(cast, {'alive?',Pid}, _, Data) -> Pid ! yes, - {next_state,State,Data}; -handle_common_events(_, _, _, _, _) -> + {keep_state,Data}; +handle_common_events(_, _, _, _) -> undefined. %% Dispatcher to test callback_mode handle_event_function @@ -1356,8 +1356,7 @@ handle_common_events(_, _, _, _, _) -> handle_event(Type, Event, PrevState, State, Data) -> PrevStateName = unwrap_state(PrevState), StateName = unwrap_state(State), - try ?MODULE:StateName( - Type, Event, PrevStateName, StateName, Data) of + try ?MODULE:StateName(Type, Event, PrevStateName, Data) of Result -> wrap_result(Result) catch -- cgit v1.2.3 From 8b16506b0763d13b69aef3baeabef4729c708fe5 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Wed, 24 Feb 2016 15:50:29 +0100 Subject: Make first next_event in list arrive first Define options as actions that set options, rework the documentation about this. --- lib/stdlib/doc/src/gen_statem.xml | 298 ++++++++++++++++---------- lib/stdlib/src/gen_statem.erl | 439 ++++++++++++++++++++++---------------- 2 files changed, 436 insertions(+), 301 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 8934d912c6..1e41d616e9 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -143,8 +143,8 @@ erlang:'!' -----> Module:StateName/4

The state function can insert events using the - - transition_action() next_event + + action() next_event and such an event is inserted as the next to present to the state function. That is: as if it is @@ -158,12 +158,19 @@ erlang:'!' -----> Module:StateName/4

Inserting an event replaces the trick of calling your own state handling functions that you often would have to resort to in e.g gen_fsm - to force processing a faked event before others. + to force processing an inserted event before others. If you for example in gen_statem postpone an event in one state and then call some other state function of yours, you have not changed states and hence the postponed event will not be retried, which is logical but might be confusing.

+

+ See the type + + transition_option(). + + for the details of a state transition. +

A gen_statem handles system messages as documented in sys. The sys module @@ -184,7 +191,7 @@ erlang:'!' -----> Module:StateName/4 state function or Module:init/1 specifies 'hibernate' in the returned - Ops + Actions list. This might be useful if the server is expected to be idle for a long time. However use this feature with care since hibernation implies at least two garbage collections @@ -281,8 +288,8 @@ erlang:'!' -----> Module:StateName/4

Destination to use when replying through for example the - - transition_op() {reply,Caller,Reply} + + action() {reply,Caller,Reply} to a process that has called the gen_statem server using call/2. @@ -344,10 +351,11 @@ erlang:'!' -----> Module:StateName/4

A fun() of arity 2 that takes an event and returns a boolean. - When used in {remove_event,RemoveEventPredicate} from - transition_op(). - The event for which the predicate returns true will - be removed. + When used in the + action() + {remove_event,RemoveEventPredicate}, + the event for which the predicate returns true + will be removed.

The predicate may not use a throw exception @@ -384,40 +392,45 @@ erlang:'!' -----> Module:StateName/4 - + -

Either a - - transition_option() - of which the last occurence - in the containing list takes precedence, or a - - transition_action() - performed in the order of the containing list. -

-

These may be returned from the - state function, - from Module:init/1 - or given to - enter_loop/6,7. +

+ Transition options may be set by + actions + and they modify how the state transition is done:

-

The processing order for a state change is:

- If the option postpone is true - the current event is postponed. + All + actions + are processed in order of appearance. - If the state changes the queue of incoming events - is reset to start with the oldest postponed. + + If the + + transition_option() + + postpone + is true the current event is postponed. - All actions are processed in order of appearance. + + If the state changes the queue of incoming events + is reset to start with the oldest postponed. - The timeout option is processed if present, - so a state timer may be started or a timeout zero event + + If the + + transition_option() + + + timeout + + is set a state timer may be started or a timeout zero event may be enqueued as the newest incoming. - The (possibly new) - state function - is called with the oldest enqueued event if there is any, + + The (possibly new) + state function + is called with the oldest enqueued event if there is any, otherwise the gen_statem goes into receive or hibernation (if the option hibernate is true) to wait for the next message. In hibernation the next @@ -429,91 +442,145 @@ erlang:'!' -----> Module:StateName/4
- + + +

+ If true postpone the current event and retry + it when the state changes that is: + NewState =/= State. +

+
+
+ + + +

+ If true hibernate the gen_statem + by calling + + proc_lib:hibernate/3 + + before going into receive + to wait for a new external event. + If there are enqueued events the hibernate + is ignored as if an event just arrived and awakened + the gen_statem. +

+
+
+ + -

If multiple state options of the same kind are present - in the containing list these are set in the list order - and the last value is kept. +

+ Generate an event of + type timeout + after this time (in milliseconds) unless some other + event arrives in which case this timeout is cancelled. + Note that a retried or inserted event + counts just like a new in this respect. + If the value is infinity no timer is started. + If it is 0 the timeout event + is immediately enqueued as the newest received. + Also note that it is not possible nor needed + to cancel this timeout using the + + action() cancel_timer. + + since this timeout is cancelled automatically by any other event. +

+
+
+ + + +

These state transition actions may be invoked by + returning them from the + state function, + from Module:init/1 + or by giving them to + enter_loop/6,7. +

+

Actions are executed in the containing list order. + The order matters for some actions such as next_event + and reply_action(). The order can in peculiar cases + matter for remove_event with + EventPredicate versus other + event removal actions. +

+

+ The order also matters for actions that set + + transition options + + since setting an option overrides any previous + of the same kind, so the last in the containing list wins.

postpone - If Postpone =:= true - or plain postpone postpone the current event - to be retried after a state change. - This option is ignored when returned from + + Set the + + transition_option() postpone + + for this state transition. + This action is ignored when returned from Module:init/1 or given to enter_loop/5,6 since there is no event to postpone in those cases. hibernate - If Hibernate =:= true - or plain hibernate hibernate the gen_statem - by calling - - proc_lib:hibernate/3 - before receive to wait for a new event. - If there are enqueued events the hibernate action - is ignored as if an event just arrived and awakened - the gen_statem. + + Set the + + transition_option() hibernate + + for this state transition. - timeout - - Generate an event of - type timeout - after Time milliseconds unless some other - event is received before that time. Note that a retried - or inserted event counts just like a new in this respect. - If Time =:= infinity no timer is started. - If Time =:= 0 the timeout event - is immediately enqueued as the newest received. - Also note that it is not possible nor needed - to cancel this timeout using the - - transition_action() - cancel_timer. - This timeout is cancelled automatically by any other event. + timeout + + Set the + + transition_option() timeout + + to Time with the + EventContent as Msg + for the next state. - -
-
- - - -

The state transition actions are executed - in the containing list order. This matters - for next_event where the last such in the list - will become the next event to process by - the current state function. Regarding the other actions - it is only for remove_event with - EventPredicate - and for reply_action() that the order may matter. -

- reply_action() Reply to a caller. next_event - Insert the given EventType + + Insert the given EventType and EventContent as the next to process. This will bypass any events in the process mailbox as well as any other queued events. An event of type internal - should be used when you want to reliably distinguish + + should be used when you want to reliably distinguish an event inserted this way from any external event. + If there are multiple next_event actions + in the containing list they are buffered and all are + inserted so the first in the list will be the + first to process. remove_event - Remove the oldest queued event + + Remove the oldest queued event that matches equal to EventType and EventContent or for which - EventPredicate returns true. + EventPredicate + returns true. cancel_timer - Cancel the timer by calling + + Cancel the timer by calling erlang:cancel_timer/2 - with TimerRef, + + with TimerRef, clean the process message queue from any late timeout message, and removes any late timeout message from the gen_statem event queue using @@ -523,16 +590,20 @@ erlang:'!' -----> Module:StateName/4 the primitives mentioned above. demonitor - Like cancel_timer above but for + + Like cancel_timer above but for demonitor/2 - with MonitorRef. + + with MonitorRef. unlink - Like {cancel_timer,_} above but for + + Like {cancel_timer,_} above but for unlink/1 - with Id. + + with Id.
@@ -574,19 +645,19 @@ erlang:'!' -----> Module:StateName/4 NewState (which may be the same as the current state), set NewData - and execute all Ops + and execute all Actions keep_state The gen_statem will keep the current state, or do a state transition to the current state if you like, set NewData - and execute all Ops + and execute all Actions keep_state_and_data The gen_statem will keep the current state, or do a state transition to the current state if you like, keep the current server data, - and execute all Ops + and execute all Actions @@ -776,8 +847,8 @@ erlang:'!' -----> Module:StateName/4 state function returns with {reply,Caller,Reply} as one - - transition_op() + + action() , and that Reply becomes the return value of this function. @@ -870,20 +941,20 @@ erlang:'!' -----> Module:StateName/4 Enter the gen_statem receive loop -

If Server_or_Ops is a list() +

If Server_or_Actions is a list() the same as enter_loop/7 except that no server_name() must have been registered and - Ops = Server_or_Ops. + Actions = Server_or_Actions.

Otherwise the same as enter_loop/7 with - Server = Server_or_Ops and - Ops = []. + Server = Server_or_Actions and + Actions = [].

@@ -917,7 +988,7 @@ erlang:'!' -----> Module:StateName/4
name must have been registered accordingly before this function is called.

CallbackMode, State, - Data and Ops + Data and Actions have the same meanings as in the return value of Module:init/1. Also, the callback module Module @@ -952,7 +1023,7 @@ erlang:'!' -----> Module:StateName/4 Args = term() Result = {CallbackMode,State,Data} -  | {CallbackMode,State,Data,Ops} +  | {CallbackMode,State,Data,Actions}  | {stop,Reason} | ignore CallbackMode = callback_mode() @@ -960,8 +1031,9 @@ erlang:'!' -----> Module:StateName/4 State = state() Data = data() - Ops = - [transition_op()] + Actions = + [action()] | + action() Reason = term() @@ -978,7 +1050,7 @@ erlang:'!' -----> Module:StateName/4 function.

If the initialization is successful, the function should return {CallbackMode,State,Data} or - {CallbackMode,State,Data,Ops}. + {CallbackMode,State,Data,Actions}. CallbackMode selects the callback_mode(). of the gen_statem. @@ -986,7 +1058,7 @@ erlang:'!' -----> Module:StateName/4 of the gen_statem and Data the server data()

-

The Ops +

The Actions are executed when entering the first state just as for a state function. @@ -1047,7 +1119,7 @@ erlang:'!' -----> Module:StateName/4 from this or from any other state function by returning with {reply,Caller,Reply} in - Ops, in + Actions, in Replies or by calling @@ -1066,7 +1138,7 @@ erlang:'!' -----> Module:StateName/4 all postponed events will be retried in the new state.

See - transition_op() + action() for options that can be set and actions that can be done by gen_statem after returning from this function.

@@ -1102,7 +1174,7 @@ erlang:'!' -----> Module:StateName/4 is terminating. If it is because another callback function has returned a stop tuple {stop,Reason} in - Ops, + Actions, Reason will have the value specified in that tuple. If it is due to a failure, Reason is the error reason.

diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index fe84a428f6..7fbc1e0f0d 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -45,7 +45,7 @@ [wakeup_from_hibernate/3]). %% Fix problem for doc build --export_type([state_callback_result/0]). +-export_type([transition_option/0,state_callback_result/0]). %%%========================================================================== %%% Interface functions. @@ -53,47 +53,77 @@ -type caller() :: {To :: pid(), Tag :: term()}. % Reply-to specifier for call + -type state() :: state_name() | % For state callback function StateName/5 term(). % For state callback function handle_event/5 + -type state_name() :: atom(). + -type data() :: term(). + -type event_type() :: {'call',Caller :: caller()} | 'cast' | 'info' | 'timeout' | 'internal'. + -type event_predicate() :: % Return true for the event in question fun((event_type(), term()) -> boolean()). + -type callback_mode() :: 'state_functions' | 'handle_event_function'. --type transition_op() :: - %% First NewState and NewData are set, - %% then all transition_action()s are executed in order of - %% apperance. Postponing the current event is performed - %% (iff transition_option() 'postpone' is 'true'). - %% Lastly pending events are processed or if there are - %% no pending events the server goes into receive - %% or hibernate (iff transition_option() 'hibernate' is 'true') - transition_option() | transition_action(). + -type transition_option() :: - %% The last of each kind in the transition_op() - %% list takes precedence - 'postpone' | % Postpone the current event to a different (=/=) state - {'postpone', Postpone :: boolean()} | - 'hibernate' | % Hibernate the server instead of going into receive - {'hibernate', Hibernate :: boolean()} | - (Timeout :: timeout()) | % {timeout,Timeout} - {'timeout', % Generate a ('timeout', Msg, ...) event after Time - Time :: timeout(), Msg :: term()}. --type transition_action() :: - %% These can occur multiple times and are executed in order - %% of appearence in the transition_op() list + postpone() | hibernate() | state_timeout(). +-type postpone() :: + %% If 'true' postpone the current event + %% and retry it when the state changes (=/=) + boolean(). +-type hibernate() :: + %% If 'true' hibernate the server instead of going into receive + boolean(). +-type state_timeout() :: + %% Generate a ('timeout', Msg, ...) event after Time + %% unless some other event is delivered + Time :: timeout(). + +-type action() :: + %% During a state change: + %% * NewState and NewData are set. + %% * All action()s are executed in order of apperance. + %% * Postponing the current event is performed + %% iff 'postpone' is 'true'. + %% * A state timer is started iff 'timeout' is set. + %% * Pending events are processed or if there are + %% no pending events the server goes into receive + %% or hibernate (iff 'hibernate' is 'true') + %% + %% These action()s are executed in order of appearence + %% in the containing list. The ones that set options + %% will override any previous so the last of each kind wins. + %% + 'postpone' | % Set the postpone option + {'postpone', Postpone :: postpone()} | + %% + 'hibernate' | % Set the hibernate option + {'hibernate', Hibernate :: hibernate()} | + %% + (Timeout :: state_timeout()) | % {timeout,Timeout} + {'timeout', % Set the timeout option + Time :: state_timeout(), Msg :: term()} | + %% reply_action() | + %% + %% All 'next_event' events are kept in a list and then + %% inserted at state changes so the first in the + %% action() list is the first to be delivered. {'next_event', % Insert event as the next to handle EventType :: event_type(), EventContent :: term()} | + %% {'remove_event', % Remove the oldest matching (=:=) event EventType :: event_type(), EventContent :: term()} | {'remove_event', % Remove the oldest event satisfying predicate EventPredicate :: event_predicate()} | + %% {'cancel_timer', % Cancel timer and clean up mess(ages) TimerRef :: reference()} | {'demonitor', % Demonitor and clean up mess(ages) @@ -103,6 +133,7 @@ -type reply_action() :: {'reply', % Reply to a caller Caller :: caller(), Reply :: term()}. + -type state_callback_result() :: {'stop', % Stop the server Reason :: term()} | @@ -122,15 +153,16 @@ {'next_state', % State transition, maybe to the same state NewState :: state(), NewData :: data(), - Ops :: [transition_op()] | transition_op()} | + Actions :: [action()] | action()} | {'keep_state', % {keep_state,NewData,[]} NewData :: data()} | {'keep_state', NewData :: data(), - Ops :: [transition_op()] | transition_op()} | + Actions :: [action()] | action()} | {'keep_state_and_data'} | % {keep_state_and_data,[]} {'keep_state_and_data', - Ops :: [transition_op()] | transition_op()}. + Actions :: [action()] | action()}. + %% The state machine init function. It is called only once and %% the server is not running until this function has returned @@ -138,7 +170,7 @@ %% for all events to this server. -callback init(Args :: term()) -> {callback_mode(), state(), data()} | - {callback_mode(), state(), data(), [transition_op()]} | + {callback_mode(), state(), data(), [action()] | action()} | 'ignore' | {'stop', Reason :: term()}. @@ -434,19 +466,19 @@ enter_loop(Module, Opts, CallbackMode, State, Data) -> Module :: module(), Opts :: [debug_opt()], CallbackMode :: callback_mode(), State :: state(), Data :: data(), - Server_or_Ops :: - server_name() | pid() | [transition_op()]) -> + Server_or_Actions :: + server_name() | pid() | [action()]) -> no_return(). -enter_loop(Module, Opts, CallbackMode, State, Data, Server_or_Ops) -> +enter_loop(Module, Opts, CallbackMode, State, Data, Server_or_Actions) -> if - is_list(Server_or_Ops) -> + is_list(Server_or_Actions) -> enter_loop( Module, Opts, CallbackMode, State, Data, - self(), Server_or_Ops); + self(), Server_or_Actions); true -> enter_loop( Module, Opts, CallbackMode, State, Data, - Server_or_Ops, []) + Server_or_Actions, []) end. %% -spec enter_loop( @@ -454,11 +486,11 @@ enter_loop(Module, Opts, CallbackMode, State, Data, Server_or_Ops) -> CallbackMode :: callback_mode(), State :: state(), Data :: data(), Server :: server_name() | pid(), - Ops :: [transition_op()]) -> + Actions :: [action()] | action()) -> no_return(). -enter_loop(Module, Opts, CallbackMode, State, Data, Server, Ops) -> +enter_loop(Module, Opts, CallbackMode, State, Data, Server, Actions) -> Parent = gen:get_parent(), - enter(Module, Opts, CallbackMode, State, Data, Server, Ops, Parent). + enter(Module, Opts, CallbackMode, State, Data, Server, Actions, Parent). %%--------------------------------------------------------------------------- %% API helpers @@ -480,32 +512,38 @@ do_send(Proc, Msg) -> end. %% Here init_it/6 and enter_loop/5,6,7 functions converge -enter(Module, Opts, CallbackMode, State, Data, Server, Ops, Parent) +enter(Module, Opts, CallbackMode, State, Data, Server, Actions, Parent) when is_atom(Module), is_pid(Parent) -> case callback_mode(CallbackMode) of true -> Name = gen:get_proc_name(Server), Debug = gen:debug_options(Name, Opts), PrevState = undefined, + NewActions = + if + is_list(Actions) -> + Actions ++ [{postpone,false}]; + true -> + [Actions,{postpone,false}] + end, S = #{ callback_mode => CallbackMode, module => Module, name => Name, prev_state => PrevState, - state => PrevState, % Discarded by loop_event_transition_ops + state => PrevState, % Discarded by loop_event_actions data => Data, timer => undefined, postponed => [], hibernate => false}, - loop_event_transition_ops( + loop_event_actions( Parent, Debug, S, [], {event,undefined}, % Discarded due to {postpone,false} - PrevState, State, Data, - Ops++[{postpone,false}]); + PrevState, State, Data, NewActions); false -> erlang:error( badarg, - [Module,Opts,CallbackMode,State,Data,Server,Ops,Parent]) + [Module,Opts,CallbackMode,State,Data,Server,Actions,Parent]) end. %%%========================================================================== @@ -536,11 +574,11 @@ init_result(Starter, Parent, ServerRef, Module, Result, Opts) -> enter( Module, Opts, CallbackMode, State, Data, ServerRef, [], Parent); - {CallbackMode,State,Data,Ops} -> + {CallbackMode,State,Data,Actions} -> proc_lib:init_ack(Starter, {ok,self()}), enter( Module, Opts, CallbackMode, State, Data, - ServerRef, Ops, Parent); + ServerRef, Actions, Parent); {stop,Reason} -> gen:unregister_name(ServerRef), proc_lib:init_ack(Starter, {error,Reason}), @@ -814,183 +852,209 @@ loop_event_result( %% Since we got back here Replies was bad terminate(Class, NewReason, Stacktrace, NewDebug, NewS, Q); {next_state,NewState,NewData} -> - loop_event_transition_ops( + loop_event_actions( Parent, Debug, S, Events, Event, State, NewState, NewData, []); - {next_state,NewState,NewData,Ops} - when is_list(Ops) -> - loop_event_transition_ops( + {next_state,NewState,NewData,Actions} + when is_list(Actions) -> + loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, Ops); + State, NewState, NewData, Actions); {keep_state,NewData} -> - loop_event_transition_ops( + loop_event_actions( Parent, Debug, S, Events, Event, State, State, NewData, []); - {keep_state,NewData,Ops} -> - loop_event_transition_ops( + {keep_state,NewData,Actions} -> + loop_event_actions( Parent, Debug, S, Events, Event, - State, State, NewData, Ops); + State, State, NewData, Actions); {keep_state_and_data} -> - loop_event_transition_ops( + loop_event_actions( Parent, Debug, S, Events, Event, State, State, Data, []); - {keep_state_and_data,Ops} -> - loop_event_transition_ops( + {keep_state_and_data,Actions} -> + loop_event_actions( Parent, Debug, S, Events, Event, - State, State, Data, Ops); + State, State, Data, Actions); _ -> ?TERMINATE( error, {bad_return_value,Result}, Debug, S, [Event|Events]) end. -loop_event_transition_ops( - Parent, Debug0, #{postponed := P0} = S, Events, Event, - State, NewState, NewData, Ops) -> - case collect_transition_options(Ops) of - {Postpone,Hibernate,Timeout,Actions} -> - P1 = % Move current event to postponed if Postpone - case Postpone of - true -> - [Event|P0]; - false -> - P0 - end, - {Q2,P2} = % Move all postponed events to queue if state change - if - NewState =:= State -> - {Events,P1}; - true -> - {lists:reverse(P1, Events),[]} - end, - %% - case process_transition_actions( - Actions, Debug0, S, Q2, P2) of - {Debug,Q3,P} -> - NewDebug = - sys_debug( - Debug, S, - case Postpone of - true -> - {postpone,Event,NewState}; - false -> - {consume,Event,NewState} - end), - {Timer,Q} = - case Timeout of - undefined -> - {undefined,Q3}; - {timeout,0,Msg} -> - %% Pretend the timeout has just been received - {undefined,Q3 ++ [{timeout,Msg}]}; - {timeout,Time,Msg} -> - {erlang:start_timer(Time, self(), Msg), - Q3} - end, - loop_events( - Parent, NewDebug, - S#{ - prev_state := State, - state := NewState, - data := NewData, - timer := Timer, - hibernate := Hibernate, - postponed := P}, - Q, Timer); - [Class,Reason,Stacktrace,Debug] -> - terminate( - Class, Reason, Stacktrace, Debug, S, [Event|Events]) - end; - %% - [Class,Reason,Stacktrace] -> - terminate( - Class, Reason, Stacktrace, Debug0, S, [Event|Events]) - end. - -%%--------------------------------------------------------------------------- -%% Server helpers - -collect_transition_options(Ops) -> - if - is_list(Ops) -> - collect_transition_options( - Ops, false, false, undefined, []); - true -> - collect_transition_options( - [Ops], false, false, undefined, []) - end. -%% Keep the last of each kind -collect_transition_options( - [], Postpone, Hibernate, Timeout, Actions) -> - {Postpone,Hibernate,Timeout,lists:reverse(Actions)}; -collect_transition_options( - [Op|Ops] = AllOps, Postpone, Hibernate, Timeout, Actions) -> - case Op of +loop_event_actions( + Parent, Debug, S, Events, Event, State, NewState, NewData, Actions) -> + loop_event_actions( + Parent, Debug, S, Events, Event, State, NewState, NewData, + false, false, undefined, [], Actions). +%% +loop_event_actions( + Parent, Debug, #{postponed := P0} = S, Events, Event, + State, NewState, NewData, + Postpone, Hibernate, Timeout, NextEvents, []) -> + P1 = % Move current event to postponed if Postpone + case Postpone of + true -> + [Event|P0]; + false -> + P0 + end, + {Timer,Q1} = + case Timeout of + undefined -> + {undefined,Events}; + {timeout,0,Msg} -> + %% Pretend the timeout has just been received + {undefined,Events ++ [{timeout,Msg}]}; + {timeout,Time,Msg} -> + {erlang:start_timer(Time, self(), Msg), + Events} + end, + {Q2,P} = % Move all postponed events to queue if state change + if + NewState =:= State -> + {Q1,P1}; + true -> + {lists:reverse(P1, Q1),[]} + end, + %% Place next events first in queue + Q = lists:reverse(NextEvents, Q2), + %% + NewDebug = + sys_debug( + Debug, S, + case Postpone of + true -> + {postpone,Event,NewState}; + false -> + {consume,Event,NewState} + end), + %% Loop to top; process next event + loop_events( + Parent, NewDebug, + S#{ + prev_state := State, + state := NewState, + data := NewData, + timer := Timer, + hibernate := Hibernate, + postponed := P}, + Q, Timer); +loop_event_actions( + Parent, Debug, S, Events, Event, State, NewState, NewData, + Postpone, Hibernate, Timeout, NextEvents, [Action|Actions]) -> + case Action of + %% Set options postpone -> - collect_transition_options( - Ops, true, Hibernate, Timeout, Actions); + loop_event_actions( + Parent, Debug, S, Events, Event, + State, NewState, NewData, + true, Hibernate, Timeout, NextEvents, Actions); {postpone,NewPostpone} when is_boolean(NewPostpone) -> - collect_transition_options( - Ops, NewPostpone, Hibernate, Timeout, Actions); + loop_event_actions( + Parent, Debug, S, Events, Event, + State, NewState, NewData, + NewPostpone, Hibernate, Timeout, NextEvents, Actions); {postpone,_} -> - [error,{bad_ops,AllOps},?STACKTRACE()]; + ?TERMINATE( + error, {bad_action,Action}, Debug, S, [Event|Events]); hibernate -> - collect_transition_options( - Ops, Postpone, true, Timeout, Actions); + loop_event_actions( + Parent, Debug, S, Events, Event, + State, NewState, NewData, + Postpone, true, Timeout, NextEvents, Actions); {hibernate,NewHibernate} when is_boolean(NewHibernate) -> - collect_transition_options( - Ops, Postpone, NewHibernate, Timeout, Actions); + loop_event_actions( + Parent, Debug, S, Events, Event, + State, NewState, NewData, + Postpone, NewHibernate, Timeout, NextEvents, Actions); {hibernate,_} -> - [error,{bad_ops,AllOps},?STACKTRACE()]; - {timeout,infinity,_} -> % Ignore since it will never time out - collect_transition_options( - Ops, Postpone, Hibernate, undefined, Actions); + ?TERMINATE( + error, {bad_action,Action}, Debug, S, [Event|Events]); + {timeout,infinity,_} -> % Clear timer - it will never trigger + loop_event_actions( + Parent, Debug, S, Events, Event, + State, NewState, NewData, + Postpone, Hibernate, undefined, NextEvents, Actions); {timeout,Time,_} = NewTimeout when is_integer(Time), Time >= 0 -> - collect_transition_options( - Ops, Postpone, Hibernate, NewTimeout, Actions); + loop_event_actions( + Parent, Debug, S, Events, Event, + State, NewState, NewData, + Postpone, Hibernate, NewTimeout, NextEvents, Actions); {timeout,_,_} -> - [error,{bad_ops,AllOps},?STACKTRACE()]; - _ -> % Collect others as actions - collect_transition_options( - Ops, Postpone, Hibernate, Timeout, [Op|Actions]) - end. - -process_transition_actions([], Debug, _S, Q, P) -> - {Debug,Q,P}; -process_transition_actions( - [Action|Actions] = AllActions, Debug, S, Q, P) -> - case Action of - {reply,{_To,_Tag}=Caller,Reply} -> - NewDebug = do_reply(Debug, S, Caller, Reply), - process_transition_actions(Actions, NewDebug, S, Q, P); + ?TERMINATE( + error, {bad_action,Action}, Debug, S, [Event|Events]); + %% Actual actions + {reply,Caller,Reply} -> + case caller(Caller) of + true -> + NewDebug = do_reply(Debug, S, Caller, Reply), + loop_event_actions( + Parent, NewDebug, S, Events, Event, + State, NewState, NewData, + Postpone, Hibernate, Timeout, NextEvents, Actions); + false -> + ?TERMINATE( + error, {bad_action,Action}, Debug, S, [Event|Events]) + end; {next_event,Type,Content} -> case event_type(Type) of true -> - process_transition_actions( - Actions, Debug, S, [{Type,Content}|Q], P); + loop_event_actions( + Parent, Debug, S, Events, Event, + State, NewState, NewData, + Postpone, Hibernate, Timeout, + [{Type,Content}|NextEvents], Actions); false -> - [error,{bad_ops,AllActions},?STACKTRACE(),Debug] + ?TERMINATE( + error, {bad_action,Action}, Debug, S, [Event|Events]) end; _ -> %% All others are remove actions case remove_fun(Action) of false -> - process_transition_actions( - Actions, Debug, S, Q, P); + loop_event_actions( + Parent, Debug, S, Events, Event, + State, NewState, NewData, + Postpone, Hibernate, Timeout, NextEvents, Actions); undefined -> - [error,{bad_ops,AllActions},?STACKTRACE(),Debug]; + ?TERMINATE( + error, {bad_action,Action}, Debug, S, [Event|Events]); RemoveFun when is_function(RemoveFun, 2) -> - case remove_event(RemoveFun, Q, P) of - {NewQ,NewP} -> - process_transition_actions( - Actions, Debug, S, NewQ, NewP); - Error -> - Error ++ [Debug] + #{postponed := P} = S, + case remove_event(RemoveFun, Events, P) of + false -> + loop_event_actions( + Parent, Debug, S, Events, Event, + State, NewState, NewData, + Postpone, Hibernate, Timeout, NextEvents, + Actions); + {NewEvents,false} -> + loop_event_actions( + Parent, Debug, S, NewEvents, Event, + State, NewState, NewData, + Postpone, Hibernate, Timeout, NextEvents, + Actions); + {false,NewP} -> + NewS = S#{postponed := NewP}, + loop_event_actions( + Parent, Debug, NewS, Events, Event, + State, NewState, NewData, + Postpone, Hibernate, Timeout, NextEvents, + Actions); + [Class,Reason,Stacktrace] -> + terminate( + Class, Reason, Stacktrace, + Debug, S, [Event|Events]) end; - Error -> - Error ++ [Debug] + [Class,Reason,Stacktrace] -> + terminate( + Class, Reason, Stacktrace, Debug, S, [Event|Events]) end end. +%%--------------------------------------------------------------------------- +%% Server helpers + reply_then_terminate(Class, Reason, Stacktrace, Debug, S, Q, Replies) -> if is_list(Replies) -> @@ -1004,14 +1068,14 @@ reply_then_terminate(Class, Reason, Stacktrace, Debug, S, Q, Replies) -> do_reply_then_terminate(Class, Reason, Stacktrace, Debug, S, Q, []) -> terminate(Class, Reason, Stacktrace, Debug, S, Q); do_reply_then_terminate( - Class, Reason, Stacktrace, Debug, S, Q, [R|Rs] = Replies) -> + Class, Reason, Stacktrace, Debug, S, Q, [R|Rs]) -> case R of {reply,{_To,_Tag}=Caller,Reply} -> NewDebug = do_reply(Debug, S, Caller, Reply), do_reply_then_terminate( Class, Reason, Stacktrace, NewDebug, S, Q, Rs); _ -> - [error,{bad_replies,Replies},?STACKTRACE(),Debug] + [error,{bad_action,R},?STACKTRACE(),Debug] end. do_reply(Debug, S, Caller, Reply) -> @@ -1026,20 +1090,19 @@ remove_event(RemoveFun, Q, P) -> false -> case remove_head_event(RemoveFun, Q) of false -> - {P,Q}; + false; NewQ -> - {P,NewQ} + {false,NewQ} end; NewP -> - {NewP,Q} + {NewP,false} end catch Class:Reason -> [Class,Reason,erlang:get_stacktrace()] end. -%% Do the given transition action and create -%% an event removal predicate fun() +%% Do the given action and create an event removal predicate fun() remove_fun({remove_event,Type,Content}) -> fun (T, C) when T =:= Type, C =:= Content -> true; (_, _) -> false -- cgit v1.2.3 From 14028aee428c135211e33df77c1bee2fdc128f6e Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Wed, 24 Feb 2016 16:36:24 +0100 Subject: Ditch PrevState StateName/4 -> StateName/3 handle_event/5 -> handle_event/4 --- lib/stdlib/doc/src/gen_statem.xml | 70 +++++++++++-------------------- lib/stdlib/src/gen_statem.erl | 23 ++++------ lib/stdlib/test/gen_statem_SUITE.erl | 81 ++++++++++++++++++------------------ 3 files changed, 73 insertions(+), 101 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 1e41d616e9..f21610d14c 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -62,12 +62,12 @@ gen_statem:stop -----> Module:terminate/2 gen_statem:call gen_statem:cast erlang:send -erlang:'!' -----> Module:StateName/4 - Module:handle_event/5 +erlang:'!' -----> Module:StateName/3 + Module:handle_event/4 - -----> Module:terminate/3 -- -----> Module:code_change/3 +- -----> Module:code_change/4

Events are of different types so the callback functions can know the origin of an event @@ -91,28 +91,22 @@ erlang:'!' -----> Module:StateName/4 is state_functions the state has to be an atom and is used as the state function name. See - - Module:StateName/4 + + Module:StateName/3 . This gathers all code for a specific state in one function and hence dispatches on state first. Note that in this mode the callback function - - Module:code_change/4 - makes the state name code_change - unusable. Actually you might get away with using it - as a state name but you will have to find a way to separate - the two callback roles e.g by observing that you can force - argument 3 into having different types for the different roles. - This is a slippery slope, though. It is easier - to simply not use code_change as a state name. + + Module:terminate/3 + makes the state name terminate unusable.

When callback_mode is handle_event_function the state can be any term and the state function name is - - Module:handle_event/5 + + Module:handle_event/4 . This makes it easy to dispatch on state or on event as you like but you will have to implement it. @@ -120,12 +114,6 @@ erlang:'!' -----> Module:StateName/4 states so you do not accidentally postpone one event forever creating an infinite busy loop.

-

There is a small gotcha in both callback modes regarding - the state undefined that is used as the previous state - when the first state function is called. So either do not - use undefined as a state, or figure out - the implications yourself. -

The gen_statem enqueues incoming events in order of arrival and presents these to the state function @@ -378,14 +366,14 @@ erlang:'!' -----> Module:StateName/4 The state has to be of type state_name() and one callback function per state that is - - Module:StateName/4 - is used. This is the default. + + Module:StateName/3 + is used. handle_event_function The state can be any term and the callback function - - Module:handle_event/5 + + Module:handle_event/4 is used for all states. @@ -1054,9 +1042,10 @@ erlang:'!' -----> Module:StateName/4 CallbackMode selects the callback_mode(). of the gen_statem. - State is the state() - of the gen_statem and - Data the server data() + State is the initial + state() + and Data the initial server + data()

The Actions are executed when entering the first @@ -1076,11 +1065,10 @@ erlang:'!' -----> Module:StateName/4 - Module:StateName(EventType, EventContent, - PrevStateName, Data) -> Result + Module:StateName(EventType, EventContent, Data) -> Result Module:handle_event(EventType, EventContent, - PrevState, State, Data) -> Result + State, Data) -> Result Handle an event @@ -1088,10 +1076,7 @@ erlang:'!' -----> Module:StateName/4 event_type() EventContent = term() - PrevStateName = - state_name() - - PrevState = State = + State = NewState = state() Data = NewData = @@ -1109,9 +1094,9 @@ erlang:'!' -----> Module:StateName/4 cast/2 or as a normal process message one of these functions is called. If callback_mode - is state_functions then Module:StateName/4 is called, + is state_functions then Module:StateName/3 is called, and if it is handle_event_function - then Module:handle_event/5 is called. + then Module:handle_event/4 is called.

If EventType is {call,Caller} @@ -1126,13 +1111,6 @@ erlang:'!' -----> Module:StateName/4 reply(Caller, Reply) .

-

PrevStateName and PrevState are useful - in some odd cases for example when you want to do something - only at the first event in a state, or when you need to - handle events differently depending on the previous state. - Note that when gen_statem enters its first state - this is set to undefined. -

If this function returns with a new state that does not match equal (=/=) to the current state all postponed events will be retried in the new state. diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 7fbc1e0f0d..c844b76653 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -184,7 +184,6 @@ -callback state_name( event_type(), EventContent :: term(), - PrevStateName :: state_name(), % Previous state name Data :: data()) -> state_callback_result(). %% @@ -195,7 +194,6 @@ -callback handle_event( event_type(), EventContent :: term(), - PrevState :: state(), % Previous state State :: state(), % Current state Data :: data()) -> state_callback_result(). @@ -234,10 +232,10 @@ [init/1, % One may use enter_loop/5,6,7 instead format_status/2, % Has got a default implementation %% - state_name/4, % Example for callback_mode =:= state_functions: + state_name/3, % Example for callback_mode =:= state_functions: %% there has to be a StateName/5 callback function for every StateName. %% - handle_event/5]). % For callback_mode =:= handle_event_function + handle_event/4]). % For callback_mode =:= handle_event_function %% Type validation functions callback_mode(CallbackMode) -> @@ -518,7 +516,7 @@ enter(Module, Opts, CallbackMode, State, Data, Server, Actions, Parent) true -> Name = gen:get_proc_name(Server), Debug = gen:debug_options(Name, Opts), - PrevState = undefined, + OldState = make_ref(), NewActions = if is_list(Actions) -> @@ -530,8 +528,7 @@ enter(Module, Opts, CallbackMode, State, Data, Server, Actions, Parent) callback_mode => CallbackMode, module => Module, name => Name, - prev_state => PrevState, - state => PrevState, % Discarded by loop_event_actions + state => OldState, % Discarded by loop_event_actions data => Data, timer => undefined, postponed => [], @@ -539,7 +536,7 @@ enter(Module, Opts, CallbackMode, State, Data, Server, Actions, Parent) loop_event_actions( Parent, Debug, S, [], {event,undefined}, % Discarded due to {postpone,false} - PrevState, State, Data, NewActions); + OldState, State, Data, NewActions); false -> erlang:error( badarg, @@ -771,7 +768,6 @@ loop_events( Parent, Debug, #{callback_mode := CallbackMode, module := Module, - prev_state := PrevState, state := State, data := Data} = S, [{Type,Content} = Event|Events] = Q, Timer) -> @@ -780,9 +776,9 @@ loop_events( try case CallbackMode of state_functions -> - Module:State(Type, Content, PrevState, Data); + Module:State(Type, Content, Data); handle_event_function -> - Module:handle_event(Type, Content, PrevState, State, Data) + Module:handle_event(Type, Content, State, Data) end of Result -> loop_event_result( @@ -796,7 +792,7 @@ loop_events( %% of calling a nonexistent state function case erlang:get_stacktrace() of [{Module,State, - [Type,Content,PrevState,Data]=Args, + [Type,Content,Data]=Args, _} |Stacktrace] when CallbackMode =:= state_functions -> @@ -806,7 +802,7 @@ loop_events( Stacktrace, Debug, S, Q); [{Module,handle_event, - [Type,Content,PrevState,State,Data]=Args, + [Type,Content,State,Data]=Args, _} |Stacktrace] when CallbackMode =:= handle_event_function -> @@ -932,7 +928,6 @@ loop_event_actions( loop_events( Parent, NewDebug, S#{ - prev_state := State, state := NewState, data := NewData, timer := Timer, diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 3302171329..27b042c0d8 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -1160,26 +1160,26 @@ terminate(_Reason, _State, _Data) -> %% State functions -idle(cast, {connect,Pid}, _, Data) -> +idle(cast, {connect,Pid}, Data) -> Pid ! accept, {next_state,wfor_conf,Data}; -idle({call,From}, connect, _, Data) -> +idle({call,From}, connect, Data) -> gen_statem:reply(From, accept), {next_state,wfor_conf,Data}; -idle(cast, badreturn, _, _Data) -> +idle(cast, badreturn, _Data) -> badreturn; -idle({call,_From}, badreturn, _, _Data) -> +idle({call,_From}, badreturn, _Data) -> badreturn; -idle({call,From}, {delayed_answer,T}, _, Data) -> +idle({call,From}, {delayed_answer,T}, Data) -> receive after T -> gen_statem:reply({reply,From,delayed}), throw({keep_state,Data}) end; -idle({call,From}, {timeout,Time}, _, _Data) -> +idle({call,From}, {timeout,Time}, _Data) -> {next_state,timeout,{From,Time}, [{timeout,Time,idle}]}; -idle(Type, Content, PrevState, Data) -> +idle(Type, Content, Data) -> case handle_common_events(Type, Content, idle, Data) of undefined -> case Type of @@ -1187,45 +1187,45 @@ idle(Type, Content, PrevState, Data) -> throw({keep_state,Data,[{reply,From,'eh?'}]}); _ -> throw( - {stop,{unexpected,idle,Type,Content,PrevState}}) + {stop,{unexpected,idle,Type,Content}}) end; Result -> Result end. -timeout(timeout, idle, idle, {From,Time}) -> +timeout(timeout, idle, {From,Time}) -> TRef2 = erlang:start_timer(Time, self(), ok), TRefC1 = erlang:start_timer(Time, self(), cancel1), TRefC2 = erlang:start_timer(Time, self(), cancel2), {next_state,timeout2,{From,Time,TRef2}, [{cancel_timer, TRefC1}, {next_event,internal,{cancel_timer,TRefC2}}]}; -timeout(_, _, _, Data) -> +timeout(_, _, Data) -> {keep_state,Data}. timeout2( - internal, {cancel_timer,TRefC2}, timeout, {From,Time,TRef2}) -> + internal, {cancel_timer,TRefC2}, {From,Time,TRef2}) -> Time4 = Time * 4, receive after Time4 -> ok end, {next_state,timeout3,{From,TRef2}, [{cancel_timer,TRefC2}]}; -timeout2(_, _, _, Data) -> +timeout2(_, _, Data) -> {keep_state,Data}. -timeout3(info, {timeout,TRef2,Result}, _, {From,TRef2}) -> +timeout3(info, {timeout,TRef2,Result}, {From,TRef2}) -> gen_statem:reply([{reply,From,Result}]), {next_state,idle,state}; -timeout3(_, _, _, Data) -> +timeout3(_, _, Data) -> {keep_state,Data}. -wfor_conf({call,From}, confirm, _, Data) -> +wfor_conf({call,From}, confirm, Data) -> {next_state,connected,Data, [{reply,From,yes}]}; -wfor_conf(cast, {ping,_,_}, _, _) -> +wfor_conf(cast, {ping,_,_}, _) -> {keep_state_and_data,[postpone]}; -wfor_conf(cast, confirm, _, Data) -> +wfor_conf(cast, confirm, Data) -> {next_state,connected,Data}; -wfor_conf(Type, Content, _, Data) -> +wfor_conf(Type, Content, Data) -> case handle_common_events(Type, Content, wfor_conf, Data) of undefined -> case Type of @@ -1239,21 +1239,21 @@ wfor_conf(Type, Content, _, Data) -> Result end. -connected({call,From}, {msg,Ref}, _, Data) -> +connected({call,From}, {msg,Ref}, Data) -> {keep_state,Data, [{reply,From,{ack,Ref}}]}; -connected(cast, {msg,From,Ref}, _, Data) -> +connected(cast, {msg,From,Ref}, Data) -> From ! {ack,Ref}, {keep_state,Data}; -connected({call,From}, disconnect, _, Data) -> +connected({call,From}, disconnect, Data) -> {next_state,idle,Data, [{reply,From,yes}]}; -connected(cast, disconnect, _, Data) -> +connected(cast, disconnect, Data) -> {next_state,idle,Data}; -connected(cast, {ping,Pid,Tag}, _, Data) -> +connected(cast, {ping,Pid,Tag}, Data) -> Pid ! {pong,Tag}, {keep_state,Data}; -connected(Type, Content, _, Data) -> +connected(Type, Content, Data) -> case handle_common_events(Type, Content, connected, Data) of undefined -> case Type of @@ -1267,9 +1267,9 @@ connected(Type, Content, _, Data) -> Result end. -state0({call,From}, stop, _, Data) -> +state0({call,From}, stop, Data) -> {stop_and_reply,normal,[{reply,From,stopped}],Data}; -state0(Type, Content, _, Data) -> +state0(Type, Content, Data) -> case handle_common_events(Type, Content, state0, Data) of undefined -> {keep_state,Data}; @@ -1277,26 +1277,26 @@ state0(Type, Content, _, Data) -> Result end. -hiber_idle({call,From}, 'alive?', _, Data) -> +hiber_idle({call,From}, 'alive?', Data) -> {keep_state,Data, [{reply,From,'alive!'}]}; -hiber_idle({call,From}, hibernate_sync, _, Data) -> +hiber_idle({call,From}, hibernate_sync, Data) -> {next_state,hiber_wakeup,Data, [{reply,From,hibernating}, hibernate]}; -hiber_idle(info, hibernate_later, _, _) -> +hiber_idle(info, hibernate_later, _) -> Tref = erlang:start_timer(1000, self(), hibernate), {keep_state,Tref}; -hiber_idle(info, hibernate_now, _, Data) -> +hiber_idle(info, hibernate_now, Data) -> {keep_state,Data, [hibernate]}; -hiber_idle(info, {timeout,Tref,hibernate}, _, Tref) -> +hiber_idle(info, {timeout,Tref,hibernate}, Tref) -> {keep_state,[], [hibernate]}; -hiber_idle(cast, hibernate_async, _, Data) -> +hiber_idle(cast, hibernate_async, Data) -> {next_state,hiber_wakeup,Data, [hibernate]}; -hiber_idle(Type, Content, _, Data) -> +hiber_idle(Type, Content, Data) -> case handle_common_events(Type, Content, hiber_idle, Data) of undefined -> {keep_state,Data}; @@ -1304,19 +1304,19 @@ hiber_idle(Type, Content, _, Data) -> Result end. -hiber_wakeup({call,From}, wakeup_sync, _, Data) -> +hiber_wakeup({call,From}, wakeup_sync, Data) -> {next_state,hiber_idle,Data, [{reply,From,good_morning}]}; -hiber_wakeup({call,From}, snooze_sync, _, Data) -> +hiber_wakeup({call,From}, snooze_sync, Data) -> {keep_state,Data, [{reply,From,please_just_five_more}, hibernate]}; -hiber_wakeup(cast, wakeup_async, _, Data) -> +hiber_wakeup(cast, wakeup_async, Data) -> {next_state,hiber_idle,Data}; -hiber_wakeup(cast, snooze_async, _, Data) -> +hiber_wakeup(cast, snooze_async, Data) -> {keep_state,Data, [hibernate]}; -hiber_wakeup(Type, Content, _, Data) -> +hiber_wakeup(Type, Content, Data) -> case handle_common_events(Type, Content, hiber_wakeup, Data) of undefined -> {keep_state,Data}; @@ -1353,10 +1353,9 @@ handle_common_events(_, _, _, _) -> %% Wrap the state in a 1 element list just to test non-atom %% states. Note that the state from init/1 is not wrapped %% so both atom and non-atom states are tested. -handle_event(Type, Event, PrevState, State, Data) -> - PrevStateName = unwrap_state(PrevState), +handle_event(Type, Event, State, Data) -> StateName = unwrap_state(State), - try ?MODULE:StateName(Type, Event, PrevStateName, Data) of + try ?MODULE:StateName(Type, Event, Data) of Result -> wrap_result(Result) catch -- cgit v1.2.3 From 7a731535a3c4eebac00bd56ae7862c2dac25284c Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Wed, 24 Feb 2016 16:52:01 +0100 Subject: Unify whitespace in XML doc --- lib/stdlib/doc/src/gen_statem.xml | 534 +++++++++++++++++++++++--------------- 1 file changed, 330 insertions(+), 204 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index f21610d14c..da88bedf6b 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -31,7 +31,8 @@ gen_statem Generic State Machine Behaviour -

A behaviour module for implementing a state machine. +

+ A behaviour module for implementing a state machine. Two callback modes are supported. One for a finite state machine like gen_fsm that require the state to be an atom and use that state as @@ -45,9 +46,12 @@ and include functionality for tracing and error reporting. It will also fit into an OTP supervision tree. Refer to - OTP Design Principles for more information. + OTP Design Principles + + for more information.

-

A gen_statem assumes all specific parts to be located in a +

+ A gen_statem assumes all specific parts to be located in a callback module exporting a pre-defined set of functions. The relationship between the behaviour functions and the callback functions can be illustrated as follows:

@@ -68,29 +72,32 @@ erlang:'!' -----> Module:StateName/3 - -----> Module:terminate/3 - -----> Module:code_change/4 -

Events are of different +

+ Events are of different types so the callback functions can know the origin of an event and how to respond.

-

If a callback function fails or returns a bad value, +

+ If a callback function fails or returns a bad value, the gen_statem will terminate. An exception of class throw, however, is not regarded as an error but as a valid return.

-

The "state function" for a specific +

+ The "state function" for a specific state in a gen_statem is the callback function that is called for all events in this state, and is selected depending on callback_mode that the implementation specifies during gen_statem init.

-

When +

+ When callback_mode is state_functions the state has to be an atom and - is used as the state function name. - See + is used as the state function name. See Module:StateName/3 . @@ -101,8 +108,8 @@ erlang:'!' -----> Module:StateName/3 Module:terminate/3 makes the state name terminate unusable.

-

When - callback_mode +

+ When callback_mode is handle_event_function the state can be any term and the state function name is @@ -114,7 +121,8 @@ erlang:'!' -----> Module:StateName/3 states so you do not accidentally postpone one event forever creating an infinite busy loop.

-

The gen_statem enqueues incoming events in order of arrival +

+ The gen_statem enqueues incoming events in order of arrival and presents these to the state function in that order. The state function can postpone an event @@ -122,14 +130,15 @@ erlang:'!' -----> Module:StateName/3 After a state change all enqueued events (including postponed) are again presented to the state function.

-

The gen_statem event queue model is sufficient +

+ The gen_statem event queue model is sufficient to emulate the normal process message queue and selective receive with postponing an event corresponding to not matching it in a receive statement and changing states corresponding to entering a new receive statement.

-

The - state function +

+ The state function can insert events using the action() next_event @@ -143,7 +152,8 @@ erlang:'!' -----> Module:StateName/3 that can be used for such events making it impossible to mistake for an external event.

-

Inserting an event replaces the trick of calling your own +

+ Inserting an event replaces the trick of calling your own state handling functions that you often would have to resort to in e.g gen_fsm to force processing an inserted event before others. @@ -159,20 +169,24 @@ erlang:'!' -----> Module:StateName/3 for the details of a state transition.

-

A gen_statem handles system messages as documented in - sys. - The sys module - can be used for debugging a gen_statem. +

+ A gen_statem handles system messages as documented in + sys. + The sys module + can be used for debugging a gen_statem.

-

Note that a gen_statem does not trap exit signals +

+ Note that a gen_statem does not trap exit signals automatically, this must be explicitly initiated by the callback module.

-

Unless otherwise stated, all functions in this module fail if +

+ Unless otherwise stated, all functions in this module fail if the specified gen_statem does not exist or if bad arguments are given.

-

The gen_statem process can go into hibernation (see +

+ The gen_statem process can go into hibernation (see erlang:hibernate/3 ) if a @@ -192,11 +206,13 @@ erlang:'!' -----> Module:StateName/3 -

Name specification to use when starting - a gen_statem server. - See +

+ Name specification to use when starting + a gen_statem server. See + start_link/3 - and + + and server_ref() below. @@ -206,12 +222,14 @@ erlang:'!' -----> Module:StateName/3 -

Server specification to use when addressing +

+ Server specification to use when addressing a gen_statem server. See call/2 and server_name() - above. + + above.

It can be:

@@ -219,15 +237,18 @@ erlang:'!' -----> Module:StateName/3 LocalName The gen_statem is locally registered. Name, Node - The gen_statem is locally registered + + The gen_statem is locally registered on another node. GlobalName - The gen_statem is globally registered + + The gen_statem is globally registered in global. RegMod, ViaName - The gen_statem is registered through + + The gen_statem is registered through an alternative process registry. The registry callback module RegMod should export the functions @@ -244,11 +265,13 @@ erlang:'!' -----> Module:StateName/3 -

Debug option that can be used when starting +

+ Debug option that can be used when starting a gen_statem server through for example enter_loop/4.

-

For every entry in Dbgs +

+ For every entry in Dbgs the corresponding function in sys will be called.

@@ -257,7 +280,8 @@ erlang:'!' -----> Module:StateName/3 -

Options that can be used when starting +

+ Options that can be used when starting a gen_statem server through for example start_link/3.

@@ -266,7 +290,8 @@ erlang:'!' -----> Module:StateName/3 -

Return value from the start functions for_example +

+ Return value from the start functions for_example start_link/3.

@@ -275,7 +300,8 @@ erlang:'!' -----> Module:StateName/3 -

Destination to use when replying through for example the +

+ Destination to use when replying through for example the action() {reply,Caller,Reply} @@ -287,7 +313,8 @@ erlang:'!' -----> Module:StateName/3 -

After a state change (NewState =/= State) +

+ After a state change (NewState =/= State) all postponed events are retried.

@@ -295,10 +322,12 @@ erlang:'!' -----> Module:StateName/3 -

If +

+ If callback_mode - is state_functions, which is the default, + + is state_functions, which is the default, the state has to be of this type.

@@ -306,7 +335,8 @@ erlang:'!' -----> Module:StateName/3 -

A term in which the state machine implementation +

+ A term in which the state machine implementation should store any server data it needs. The difference between this and the state() itself is that a change in this data does not cause @@ -320,7 +350,8 @@ erlang:'!' -----> Module:StateName/3 -

External events are of 3 different type: +

+ External events are of 3 different type: {call,Caller}, cast or info. Calls (synchronous) and casts (asynchronous) originate from the corresponding API functions. @@ -337,7 +368,8 @@ erlang:'!' -----> Module:StateName/3 -

A fun() of arity 2 that takes an event +

+ A fun() of arity 2 that takes an event and returns a boolean. When used in the action() @@ -363,18 +395,22 @@ erlang:'!' -----> Module:StateName/3

state_functions - The state has to be of type + + The state has to be of type state_name() and one callback function per state that is Module:StateName/3 - is used. + + is used. handle_event_function - The state can be any term and the callback function + + The state can be any term and the callback function Module:handle_event/4 - is used for all states. + + is used for all states.
@@ -388,9 +424,10 @@ erlang:'!' -----> Module:StateName/3 and they modify how the state transition is done:

- All - actions - are processed in order of appearance. + + All + actions + are processed in order of appearance. If the @@ -481,14 +518,16 @@ erlang:'!' -----> Module:StateName/3 -

These state transition actions may be invoked by +

+ These state transition actions may be invoked by returning them from the state function, from Module:init/1 or by giving them to enter_loop/6,7.

-

Actions are executed in the containing list order. +

+ Actions are executed in the containing list order. The order matters for some actions such as next_event and reply_action(). The order can in peculiar cases matter for remove_event with @@ -599,12 +638,14 @@ erlang:'!' -----> Module:StateName/3 -

Reply to a caller waiting for a reply in +

+ Reply to a caller waiting for a reply in call/2. Caller must be the term from the {call,Caller} - argument to the + + argument to the state function.

@@ -614,35 +655,42 @@ erlang:'!' -----> Module:StateName/3 stop - Terminate the gen_statem by calling + + Terminate the gen_statem by calling Module:terminate/3 - with Reason and + + with Reason and NewData, if given. stop_and_reply - Send all Replies + + Send all Replies then terminate the gen_statem by calling Module:terminate/3 - with Reason and + + with Reason and NewData, if given. next_state - The gen_statem will do a state transition to + + The gen_statem will do a state transition to NewState (which may be the same as the current state), set NewData and execute all Actions keep_state - The gen_statem will keep the current state, or + + The gen_statem will keep the current state, or do a state transition to the current state if you like, set NewData and execute all Actions keep_state_and_data - The gen_statem will keep the current state, or + + The gen_statem will keep the current state, or do a state transition to the current state if you like, keep the current server data, and execute all Actions @@ -659,7 +707,8 @@ erlang:'!' -----> Module:StateName/3 Create a linked gen_statem process -

Creates a gen_statem process according +

+ Creates a gen_statem process according to OTP design principles (using proc_lib @@ -668,82 +717,88 @@ erlang:'!' -----> Module:StateName/3 This is essential when the gen_statem shall be part of a supervision tree so it gets linked to its supervisor.

-

The gen_statem process calls +

+ The gen_statem process calls Module:init/1 to initialize the server. To ensure a synchronized start-up procedure, start_link/3,4 does not return until Module:init/1 has returned.

-

ServerName specifies the +

+ ServerName specifies the server_name() - to register for the gen_statem. + + to register for the gen_statem. If the gen_statem is started with start_link/3 no ServerName is provided and the gen_statem is not registered.

Module is the name of the callback module.

-

Args is an arbitrary term which is passed as +

+ Args is an arbitrary term which is passed as the argument to - Module:init/1 - . + Module:init/1.

-

If the option {timeout,Time} is present in +

+ If the option {timeout,Time} is present in Opts, the gen_statem is allowed to spend Time milliseconds initializing or it will be terminated and the start function will return - - {error,timeout} - . + {error,timeout}.

-

If the option +

+ If the option {debug,Dbgs} is present in Opts, debugging through sys is activated.

-

If the option {spawn_opt,SpawnOpts} is present in +

+ If the option {spawn_opt,SpawnOpts} is present in Opts, SpawnOpts will be passed as option list to spawn_opt/2 which is used to spawn the gen_statem process.

-

Using the spawn option monitor is currently not +

+ Using the spawn option monitor is currently not allowed, but will cause this function to fail with reason - badarg.

+ badarg. +

-

If the gen_statem is successfully created +

+ If the gen_statem is successfully created and initialized this function returns {ok,Pid}, - where Pid is the pid() + + where Pid is the pid() of the gen_statem. If there already exists a process with the specified ServerName this function returns - - {error,{already_started,Pid}} - , where Pid is the pid() of that process. + {error,{already_started,Pid}}, + where Pid is the pid() of that process.

-

If Module:init/1 fails with Reason, +

+ If Module:init/1 fails with Reason, this function returns - - {error,Reason} - . If Module:init/1 returns + {error,Reason}. + If Module:init/1 returns {stop,Reason} or - - ignore - , the process is terminated and this function + ignore, + the process is terminated and this function returns {error,Reason} - or - - ignore - , respectively. + + or + ignore, + respectively.

@@ -754,7 +809,8 @@ erlang:'!' -----> Module:StateName/3 Create a stand-alone gen_statem process -

Creates a stand-alone gen_statem process according to +

+ Creates a stand-alone gen_statem process according to OTP design principles (using proc_lib primitives). @@ -762,7 +818,8 @@ erlang:'!' -----> Module:StateName/3 this start function can not be used by a supervisor to start a child.

-

See start_link/3,4 +

+ See start_link/3,4 for a description of arguments and return values.

@@ -772,10 +829,9 @@ erlang:'!' -----> Module:StateName/3 Synchronously stop a generic server -

The same as - - stop(ServerRef, normal, infinity) - . +

+ The same as + stop(ServerRef, normal, infinity).

@@ -783,33 +839,37 @@ erlang:'!' -----> Module:StateName/3 Synchronously stop a generic server -

Orders the gen_statem +

+ Orders the gen_statem ServerRef - to exit with the given Reason + + to exit with the given Reason and waits for it to terminate. The gen_statem will call Module:terminate/3 - before exiting. + + before exiting.

-

This function returns ok if the server terminates +

+ This function returns ok if the server terminates with the expected reason. Any other reason than normal, shutdown, or {shutdown,Term} will cause an error report to be issued through - - error_logger:format/2 - . + error_logger:format/2. The default Reason is normal.

-

Timeout is an integer greater than zero +

+ Timeout is an integer greater than zero which specifies how many milliseconds to wait for the server to terminate, or the atom infinity to wait indefinitely. The default value is infinity. If the server has not terminated within the specified time, a timeout exception is raised.

-

If the process does not exist, a noproc exception +

+ If the process does not exist, a noproc exception is raised.

@@ -820,10 +880,12 @@ erlang:'!' -----> Module:StateName/3 Make a synchronous call to a gen_statem -

Makes a synchronous call to the gen_statem +

+ Makes a synchronous call to the gen_statem ServerRef - by sending a request + + by sending a request and waiting until its reply arrives. The gen_statem will call the state function with @@ -831,23 +893,24 @@ erlang:'!' -----> Module:StateName/3 {call,Caller} and event content Request.

-

A Reply is generated when a +

+ A Reply is generated when a state function returns with {reply,Caller,Reply} as one - - action() - , + action(), and that Reply becomes the return value of this function.

-

Timeout is an integer greater than zero +

+ Timeout is an integer greater than zero which specifies how many milliseconds to wait for a reply, or the atom infinity to wait indefinitely, which is the default. If no reply is received within the specified time, the function call fails. -

To avoid getting a late reply in the caller's +

+ To avoid getting a late reply in the caller's inbox this function spawns a proxy process that does the call. A late reply gets delivered to the dead proxy process hence gets discarded. This is @@ -856,7 +919,8 @@ erlang:'!' -----> Module:StateName/3

-

The call may fail for example if the gen_statem dies +

+ The call may fail for example if the gen_statem dies before or during this function call.

@@ -866,10 +930,12 @@ erlang:'!' -----> Module:StateName/3 Send an asynchronous event to a gen_statem -

Sends an asynchronous event to the gen_statem +

+ Sends an asynchronous event to the gen_statem ServerRef - and returns ok immediately, + + and returns ok immediately, ignoring if the destination node or gen_statem does not exist. The gen_statem will call the @@ -886,26 +952,31 @@ erlang:'!' -----> Module:StateName/3 Reply to a caller -

This function can be used by a gen_statem +

+ This function can be used by a gen_statem to explicitly send a reply to a process that waits in call/2 when the reply cannot be defined in the return value of a state function.

-

Caller must be the term from the +

+ Caller must be the term from the {call,Caller} - argument to the + + argument to the state function. Caller and Reply can also be specified using a reply_action() - and multiple replies with a list of them. + + and multiple replies with a list of them.

-

A reply sent with this function will not be visible +

+ A reply sent with this function will not be visible in sys debug output.

@@ -916,12 +987,14 @@ erlang:'!' -----> Module:StateName/3 Enter the gen_statem receive loop -

The same as +

+ The same as enter_loop/7 except that no server_name() - must have been registered. + + must have been registered.

@@ -929,16 +1002,19 @@ erlang:'!' -----> Module:StateName/3 Enter the gen_statem receive loop -

If Server_or_Actions is a list() +

+ If Server_or_Actions is a list() the same as enter_loop/7 except that no server_name() - must have been registered and + + must have been registered and Actions = Server_or_Actions.

-

Otherwise the same as +

+ Otherwise the same as enter_loop/7 with Server = Server_or_Actions and @@ -950,7 +1026,8 @@ erlang:'!' -----> Module:StateName/3 Enter the gen_statem receive loop -

Makes an the calling process become a gen_statem. +

+ Makes an the calling process become a gen_statem. Does not return, instead the calling process will enter the gen_statem receive loop and become a gen_statem server. @@ -960,35 +1037,36 @@ erlang:'!' -----> Module:StateName/3 The user is responsible for any initialization of the process, including registering a name for it.

-

This function is useful when a more complex initialization +

+ This function is useful when a more complex initialization procedure is needed than the gen_statem behaviour provides.

-

Module, Opts and +

+ Module, Opts and Server have the same meanings as when calling - - gen_statem:start[_link]/3,4 - . + gen_statem:start[_link]/3,4. However, the server_name() - name must have been registered accordingly + + name must have been registered accordingly before this function is called.

-

CallbackMode, State, +

+ CallbackMode, State, Data and Actions have the same meanings as in the return value of Module:init/1. Also, the callback module Module does not need to export an init/1 function.

-

Failure: If the calling process was not started by a +

+ Failure: If the calling process was not started by a proc_lib start function, or if it is not registered according to - - server_name() - . + server_name().

@@ -999,7 +1077,8 @@ erlang:'!' -----> Module:StateName/3
CALLBACK FUNCTIONS -

The following functions should be exported from a +

+ The following functions should be exported from a gen_statem callback module.

@@ -1013,13 +1092,16 @@ erlang:'!' -----> Module:StateName/3 Result = {CallbackMode,State,Data}  | {CallbackMode,State,Data,Actions}  | {stop,Reason} | ignore - CallbackMode = + + CallbackMode = callback_mode() State = state() - Data = data() + + Data = data() - Actions = + + Actions = [action()] | action() @@ -1027,16 +1109,20 @@ erlang:'!' -----> Module:StateName/3 -

Whenever a gen_statem is started using +

+ Whenever a gen_statem is started using start_link/3,4 or start/3,4, this function is called by the new process to initialize the implementation state and server data.

-

Args is the Args argument provided to the start - function.

-

If the initialization is successful, the function should +

+ Args is the Args argument provided to the start + function. +

+

+ If the initialization is successful, the function should return {CallbackMode,State,Data} or {CallbackMode,State,Data,Actions}. CallbackMode selects the @@ -1045,19 +1131,22 @@ erlang:'!' -----> Module:StateName/3 State is the initial state() and Data the initial server - data() + data().

-

The Actions +

+ The Actions are executed when entering the first state just as for a state function.

-

If something goes wrong during the initialization +

+ If something goes wrong during the initialization the function should return {stop,Reason} or ignore. See start_link/3,4.

-

This function may use +

+ This function may use throw to return Result.

@@ -1072,24 +1161,29 @@ erlang:'!' -----> Module:StateName/3
Handle an event - EventType = + + EventType = event_type() EventContent = term() - State = NewState = + + State = NewState = state() - Data = NewData = + + Data = NewData = data() - Result = + + Result = state_callback_result() -

Whenever a gen_statem receives an event from +

+ Whenever a gen_statem receives an event from call/2, cast/2 or as a normal process message one of these functions is called. @@ -1098,7 +1192,8 @@ erlang:'!' -----> Module:StateName/3 and if it is handle_event_function then Module:handle_event/4 is called.

-

If EventType is +

+ If EventType is {call,Caller} the caller is waiting for a reply. The reply can be sent from this or from any other @@ -1107,20 +1202,20 @@ erlang:'!' -----> Module:StateName/3 Actions, in Replies or by calling - - reply(Caller, Reply) - . + reply(Caller, Reply).

-

If this function returns with a new state that +

+ If this function returns with a new state that does not match equal (=/=) to the current state all postponed events will be retried in the new state.

-

See - action() +

+ See action() for options that can be set and actions that can be done by gen_statem after returning from this function.

-

These functions may use +

+ These functions may use throw, to return Result.

@@ -1133,22 +1228,24 @@ erlang:'!' -----> Module:StateName/3 Reason = normal | shutdown | {shutdown,term()} | term() State = state() - Data = data() - + Data = data() Ignored = term() -

This function is called by a gen_statem +

+ This function is called by a gen_statem when it is about to terminate. It should be the opposite of Module:init/1 and do any necessary cleaning up. When it returns, the gen_statem terminates with Reason. The return value is ignored.

-

Reason is a term denoting the stop reason and +

+ Reason is a term denoting the stop reason and State is the internal state of the gen_statem.

-

Reason depends on why the gen_statem +

+ Reason depends on why the gen_statem is terminating. If it is because another callback function has returned a stop tuple {stop,Reason} in @@ -1156,36 +1253,41 @@ erlang:'!' -----> Module:StateName/3 Reason will have the value specified in that tuple. If it is due to a failure, Reason is the error reason.

-

If the gen_statem is part of a supervision tree and is +

+ If the gen_statem is part of a supervision tree and is ordered by its supervisor to terminate, this function will be called with Reason = shutdown if the following conditions apply:

- the gen_statem has been set + + the gen_statem has been set to trap exit signals, and - the shutdown strategy as defined in the supervisor's + + the shutdown strategy as defined in the supervisor's child specification is an integer timeout value, not brutal_kill. -

Even if the gen_statem is not +

+ Even if the gen_statem is not part of a supervision tree, this function will be called if it receives an 'EXIT' message from its parent. Reason will be the same as in the 'EXIT' message.

-

Otherwise, the gen_statem will be immediately terminated. +

+ Otherwise, the gen_statem will be immediately terminated.

-

Note that for any other reason than normal, +

+ Note that for any other reason than normal, shutdown, or {shutdown,Term} the gen_statem is assumed to terminate due to an error and an error report is issued using - - error_logger:format/2 - . + error_logger:format/2.

-

This function may use +

+ This function may use throw to return Ignored, which is ignored anyway.

@@ -1203,16 +1305,19 @@ erlang:'!' -----> Module:StateName/3 OldState = NewState = term() Extra = term() Result = {ok,{NewState,NewData}} | Reason - OldState = NewState = + + OldState = NewState = state() - OldData = NewData = + + OldData = NewData = data() Reason = term() -

This function is called by a gen_statem when it should +

+ This function is called by a gen_statem when it should update its internal state during a release upgrade/downgrade, i.e. when the instruction {update,Module,Change,...} where Change={advanced,Extra} is given in @@ -1222,26 +1327,32 @@ erlang:'!' -----> Module:StateName/3 for more information.

-

In the case of an upgrade, OldVsn is Vsn, and +

+ In the case of an upgrade, OldVsn is Vsn, and in the case of a downgrade, OldVsn is {down,Vsn}. Vsn is defined by the vsn attribute(s) of the old version of the callback module Module. If no such attribute is defined, the version is the checksum of the BEAM file.

-

OldState and OldData is the internal state +

+ OldState and OldData is the internal state of the gen_statem.

-

Extra is passed as-is from the {advanced,Extra} +

+ Extra is passed as-is from the {advanced,Extra} part of the update instruction.

-

If successful, the function shall return the updated +

+ If successful, the function shall return the updated internal state in an {ok,{NewState,NewData}} tuple.

-

If the function returns Reason, the ongoing +

+ If the function returns Reason, the ongoing upgrade will fail and roll back to the old release.

-

This function may use +

+ This function may use throw to return Result or Reason.

@@ -1257,10 +1368,12 @@ erlang:'!' -----> Module:StateName/3 Opt = normal | terminate PDict = [{Key, Value}] - State = + + State = state() - Data = + + Data = data() Key = term() @@ -1269,7 +1382,8 @@ erlang:'!' -----> Module:StateName/3 -

This callback is optional, so a callback module need not +

+ This callback is optional, so a callback module need not export it. The gen_statem module provides a default implementation of this function that returns {State,Data}. If this callback fails the default @@ -1280,43 +1394,52 @@ erlang:'!' -----> Module:StateName/3

This function is called by a gen_statem process when:

- One of + + One of sys:get_status/1,2 is invoked to get the gen_statem status. Opt is set to the atom normal for this case. - The gen_statem terminates abnormally and logs an error. + + The gen_statem terminates abnormally and logs an error. Opt is set to the atom terminate for this case. -

This function is useful for customising the form and +

+ This function is useful for customising the form and appearance of the gen_statem status for these cases. A callback module wishing to customise the sys:get_status/1,2 - return value as well as how + + return value as well as how its status appears in termination error logs exports an instance of format_status/2 that returns a term describing the current status of the gen_statem.

-

PDict is the current value of the gen_statem's +

+ PDict is the current value of the gen_statem's process dictionary.

-

State +

+ State is the internal state of the gen_statem.

-

Data +

+ Data is the internal server data of the gen_statem.

-

The function should return Status, a term that +

+ The function should return Status, a term that customises the details of the current state and status of the gen_statem. There are no restrictions on the form Status can take, but for the sys:get_status/1,2 - case (when Opt + + case (when Opt is normal), the recommended form for the Status value is [{data, [{"State", Term}]}] where Term provides relevant details of @@ -1325,14 +1448,17 @@ erlang:'!' -----> Module:StateName/3 consistent with the rest of the sys:get_status/1,2 - return value. + + return value.

-

One use for this function is to return compact alternative +

+ One use for this function is to return compact alternative state representations to avoid having large state terms printed in logfiles. Another is to hide sensitive data from being written to the error log.

-

This function may use +

+ This function may use throw to return Status.

-- cgit v1.2.3 From e1bbf6f1a081ff41d3287a468450bef5f65ea4ad Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Wed, 24 Feb 2016 17:04:30 +0100 Subject: Type check in API --- lib/stdlib/src/gen_statem.erl | 104 ++++++++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 44 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index c844b76653..dd980daddb 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -487,8 +487,17 @@ enter_loop(Module, Opts, CallbackMode, State, Data, Server_or_Actions) -> Actions :: [action()] | action()) -> no_return(). enter_loop(Module, Opts, CallbackMode, State, Data, Server, Actions) -> - Parent = gen:get_parent(), - enter(Module, Opts, CallbackMode, State, Data, Server, Actions, Parent). + case is_atom(Module) andalso callback_mode(CallbackMode) of + true -> + Parent = gen:get_parent(), + enter( + Module, Opts, CallbackMode, State, Data, Server, + Actions, Parent); + false -> + error( + badarg, + [Module,Opts,CallbackMode,State,Data,Server,Actions]) + end. %%--------------------------------------------------------------------------- %% API helpers @@ -510,38 +519,31 @@ do_send(Proc, Msg) -> end. %% Here init_it/6 and enter_loop/5,6,7 functions converge -enter(Module, Opts, CallbackMode, State, Data, Server, Actions, Parent) - when is_atom(Module), is_pid(Parent) -> - case callback_mode(CallbackMode) of - true -> - Name = gen:get_proc_name(Server), - Debug = gen:debug_options(Name, Opts), - OldState = make_ref(), - NewActions = - if - is_list(Actions) -> - Actions ++ [{postpone,false}]; - true -> - [Actions,{postpone,false}] - end, - S = #{ - callback_mode => CallbackMode, - module => Module, - name => Name, - state => OldState, % Discarded by loop_event_actions - data => Data, - timer => undefined, - postponed => [], - hibernate => false}, - loop_event_actions( - Parent, Debug, S, [], - {event,undefined}, % Discarded due to {postpone,false} - OldState, State, Data, NewActions); - false -> - erlang:error( - badarg, - [Module,Opts,CallbackMode,State,Data,Server,Actions,Parent]) - end. +enter(Module, Opts, CallbackMode, State, Data, Server, Actions, Parent) -> + %% The values should already have been type checked + Name = gen:get_proc_name(Server), + Debug = gen:debug_options(Name, Opts), + OldState = make_ref(), % Will be discarded by loop_event_actions/9 + NewActions = + if + is_list(Actions) -> + Actions ++ [{postpone,false}]; + true -> + [Actions,{postpone,false}] + end, + S = #{ + callback_mode => CallbackMode, + module => Module, + name => Name, + state => OldState, + data => Data, + timer => undefined, + postponed => [], + hibernate => false}, + loop_event_actions( + Parent, Debug, S, [], + {event,undefined}, % Will be discarded thanks to {postpone,false} + OldState, State, Data, NewActions). %%%========================================================================== %%% gen callbacks @@ -567,15 +569,29 @@ init_it(Starter, Parent, ServerRef, Module, Args, Opts) -> init_result(Starter, Parent, ServerRef, Module, Result, Opts) -> case Result of {CallbackMode,State,Data} -> - proc_lib:init_ack(Starter, {ok,self()}), - enter( - Module, Opts, CallbackMode, State, Data, - ServerRef, [], Parent); + case callback_mode(CallbackMode) of + true -> + proc_lib:init_ack(Starter, {ok,self()}), + enter( + Module, Opts, CallbackMode, State, Data, + ServerRef, [], Parent); + false -> + Error = {bad_return_value,Result}, + proc_lib:init_ack(Starter, {error,Error}), + exit(Error) + end; {CallbackMode,State,Data,Actions} -> - proc_lib:init_ack(Starter, {ok,self()}), - enter( - Module, Opts, CallbackMode, State, Data, - ServerRef, Actions, Parent); + case callback_mode(CallbackMode) of + true -> + proc_lib:init_ack(Starter, {ok,self()}), + enter( + Module, Opts, CallbackMode, State, Data, + ServerRef, Actions, Parent); + false -> + Error = {bad_return_value,Result}, + proc_lib:init_ack(Starter, {error,Error}), + exit(Error) + end; {stop,Reason} -> gen:unregister_name(ServerRef), proc_lib:init_ack(Starter, {error,Reason}), @@ -584,8 +600,8 @@ init_result(Starter, Parent, ServerRef, Module, Result, Opts) -> gen:unregister_name(ServerRef), proc_lib:init_ack(Starter, ignore), exit(normal); - Other -> - Error = {bad_return_value,Other}, + _ -> + Error = {bad_return_value,Result}, proc_lib:init_ack(Starter, {error,Error}), exit(Error) end. -- cgit v1.2.3 From edc94441562b255467773ec27b11835910a708fd Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 25 Feb 2016 15:12:48 +0100 Subject: Remove {keep_state_and_data} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correct typo reported by Luïc Hoguin. --- lib/stdlib/doc/src/gen_statem.xml | 2 +- lib/stdlib/src/gen_statem.erl | 9 ++------- lib/stdlib/test/gen_statem_SUITE.erl | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index da88bedf6b..35b842c173 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -61,7 +61,7 @@ gen_statem module Callback module gen_statem:start gen_statem:start_link -----> Module:init/1 -gen_statem:stop -----> Module:terminate/2 +gen_statem:stop -----> Module:terminate/3 gen_statem:call gen_statem:cast diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index dd980daddb..e6ad7ab0be 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -156,11 +156,10 @@ Actions :: [action()] | action()} | {'keep_state', % {keep_state,NewData,[]} NewData :: data()} | - {'keep_state', + {'keep_state', % Keep state, change data NewData :: data(), Actions :: [action()] | action()} | - {'keep_state_and_data'} | % {keep_state_and_data,[]} - {'keep_state_and_data', + {'keep_state_and_data', % Keep state and data -> only actions Actions :: [action()] | action()}. @@ -880,10 +879,6 @@ loop_event_result( loop_event_actions( Parent, Debug, S, Events, Event, State, State, NewData, Actions); - {keep_state_and_data} -> - loop_event_actions( - Parent, Debug, S, Events, Event, - State, State, Data, []); {keep_state_and_data,Actions} -> loop_event_actions( Parent, Debug, S, Events, Event, diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 27b042c0d8..f5cbdd9a40 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -1233,7 +1233,7 @@ wfor_conf(Type, Content, Data) -> {next_state,idle,Data, [{reply,From,'eh?'}]}; _ -> - throw({keep_state_and_data}) + throw({keep_state_and_data,[]}) end; Result -> Result -- cgit v1.2.3 From 63b291070d5522b5b116c8fe158497ee55149656 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 25 Feb 2016 15:45:32 +0100 Subject: Clarify documentation after Torben Hoffman's comments --- lib/stdlib/doc/src/gen_statem.xml | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 35b842c173..4af6732e7c 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -374,7 +374,7 @@ erlang:'!' -----> Module:StateName/3 When used in the action() {remove_event,RemoveEventPredicate}, - the event for which the predicate returns true + the oldest event for which the predicate returns true will be removed.

@@ -441,6 +441,14 @@ erlang:'!' -----> Module:StateName/3 If the state changes the queue of incoming events is reset to start with the oldest postponed. + + All events inserted with + + action() next_event + + are inserted in the queue to be processed before + all other events. + If the @@ -450,7 +458,8 @@ erlang:'!' -----> Module:StateName/3 timeout is set a state timer may be started or a timeout zero event - may be enqueued as the newest incoming. + may be enqueued as the newest incoming that is the last + to process before going into receive for new events. The (possibly new) @@ -582,16 +591,16 @@ erlang:'!' -----> Module:StateName/3 and EventContent as the next to process. This will bypass any events in the process mailbox as well as any other queued events. + All next_event actions + in the containing list are buffered and inserted + after the actions have been done + so the first in the list will be the first to process. An event of type internal should be used when you want to reliably distinguish an event inserted this way from any external event. - If there are multiple next_event actions - in the containing list they are buffered and all are - inserted so the first in the list will be the - first to process. remove_event @@ -599,7 +608,11 @@ erlang:'!' -----> Module:StateName/3 that matches equal to EventType and EventContent or for which EventPredicate - returns true. + returns true. Note that next_event + and postpone events in the same actions list + does not get into the event queue until after all actions + has been done so you can not remove an event that you insert + in the same actions list. Make up your mind! cancel_timer -- cgit v1.2.3 From cea77ca09be156669c657592bebf6efc9d5cfaee Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 25 Feb 2016 16:02:59 +0100 Subject: Write a simple example in the reference manual --- lib/stdlib/doc/src/gen_statem.xml | 188 +++++++++++++++++++++++++++++--------- 1 file changed, 144 insertions(+), 44 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 4af6732e7c..d50b88c561 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -91,7 +91,7 @@ erlang:'!' -----> Module:StateName/3 in a gen_statem is the callback function that is called for all events in this state, and is selected depending on callback_mode - that the implementation specifies during gen_statem init. + that the implementation specifies when the the server starts.

When @@ -103,7 +103,8 @@ erlang:'!' -----> Module:StateName/3 . This gathers all code for a specific state in one function and hence dispatches on state first. - Note that in this mode the callback function + Note that in this mode the fact that there is + a mandatory callback function Module:terminate/3 makes the state name terminate unusable. @@ -115,9 +116,8 @@ erlang:'!' -----> Module:StateName/3 Module:handle_event/4 . - This makes it easy to dispatch on state or on event as - you like but you will have to implement it. - Also be careful about which events you handle in which + This makes it easy to dispatch on state or on event as you desire. + Be careful about which events you handle in which states so you do not accidentally postpone one event forever creating an infinite busy loop.

@@ -132,9 +132,9 @@ erlang:'!' -----> Module:StateName/3

The gen_statem event queue model is sufficient - to emulate the normal process message queue and selective receive - with postponing an event corresponding to not matching - it in a receive statement and changing states corresponding + to emulate the normal process message queue with selective receive. + Postponing an event corresponds to not matching it + in a receive statement and changing states corresponds to entering a new receive statement.

@@ -149,8 +149,8 @@ erlang:'!' -----> Module:StateName/3 event_type() internal - that can be used for such events making it impossible - to mistake for an external event. + that can be used for such events making them impossible + to mistake for external events.

Inserting an event replaces the trick of calling your own @@ -165,7 +165,7 @@ erlang:'!' -----> Module:StateName/3

See the type - transition_option(). + transition_option() for the details of a state transition.

@@ -192,7 +192,7 @@ erlang:'!' -----> Module:StateName/3 ) if a state function or Module:init/1 - specifies 'hibernate' in the returned + specifies hibernate in the returned Actions list. This might be useful if the server is expected to be idle for a long time. However use this feature with care @@ -202,6 +202,99 @@ erlang:'!' -----> Module:StateName/3

+
+ EXAMPLE +

+ This example shows a simple pushbutton model + for a toggling pushbutton implemented with + + callback_mode() + + state_functions. + You can push the button and it replies if it went on or off, + and you can ask for a count of how many times it has been + pushed to on. +

+

This is the complete callback module file pushbutton.erl:

+ +-module(pushbutton). +-behaviour(gen_statem). + +-export([start/0,push/0,get_count/0,stop/0]). +-export([terminate/3,code_change/4,init/1]). +-export([on/3,off/3]). + +name() -> pushbutton_statem. % The registered server name + +%% API. This example uses a registered name name() +%% and does not link to the caller. +start() -> + gen_statem:start({local,name()}, ?MODULE, [], []). +push() -> + gen_statem:call(name(), push). +get_count() -> + gen_statem:call(name(), get_count). +stop() -> + gen_statem:stop(name()). + +%% Mandatory callback functions +terminate(_Reason, _State, _Data) -> + void. +code_change(_Vsn, State, Data, _Extra) -> + {ok,{State,Data}}. +init([]) -> + %% Set the callback mode and initial state + data. + %% Data is used only as a counter. + State = off, Data = 0, + {state_functions,State,Data}. + + +%%% State functions + +off({call,Caller}, push, Data) -> + %% Go to 'on', increment count and reply + %% that the resulting status is 'on' + {next_state,on,Data+1,[{reply,Caller,on}]}; +off(EventType, EventContent, Data) -> + handle_event(EventType, EventContent, Data). + +on({call,Caller}, push, Data) -> + %% Go to 'off' and reply that the resulting status is 'off' + {next_state,off,Data,[{reply,Caller,off}]}; +on(EventType, EventContent, Data) -> + handle_event(EventType, EventContent, Data). + +%% Handle events common to all states +handle_event({call,Caller}, get_count, Data) -> + %% Reply with the current count + {keep_state,Data,[{reply,Caller,Data}]}; +handle_event(_, _, Data) -> + %% Ignore all other events + {keep_state,Data}. + +

And this is a shell session when running it:

+
+1> pushbutton:start().
+{ok,<0.36.0>}
+2> pushbutton:get_count().
+0
+3> pushbutton:push().
+on
+4> pushbutton:get_count().
+1
+5> pushbutton:push().
+off
+6> pushbutton:get_count().
+1
+7> pushbutton:stop().
+ok
+8> pushbutton:push().
+** exception exit: {noproc,{gen_statem,call,[pushbutton_statem,push,infinity]}}
+     in function  gen:do_for_proc/2 (gen.erl, line 261)
+     in call from gen_statem:call/3 (gen_statem.erl, line 386)
+    
+
+ @@ -327,7 +420,7 @@ erlang:'!' -----> Module:StateName/3 callback_mode - is state_functions, which is the default, + is state_functions, the state has to be of this type.

@@ -353,11 +446,14 @@ erlang:'!' -----> Module:StateName/3

External events are of 3 different type: {call,Caller}, cast or info. - Calls (synchronous) and casts (asynchronous) + Calls + (synchronous) and + casts originate from the corresponding API functions. For calls the event contain whom to reply to. - Type info originates from normal messages sent - to the gen_statem process. + Type info originates from + regular process messages sent + to the gen_statem. It is also possible for the state machine implementation to insert events to itself, in particular of types @@ -421,7 +517,8 @@ erlang:'!' -----> Module:StateName/3

Transition options may be set by actions - and they modify how the state transition is done: + and they modify some details below in how + the state transition is done:

@@ -430,12 +527,12 @@ erlang:'!' -----> Module:StateName/3 are processed in order of appearance. - If the + If - transition_option() + postpone() - postpone - is true the current event is postponed. + is true + the current event is postponed. If the state changes the queue of incoming events @@ -450,15 +547,16 @@ erlang:'!' -----> Module:StateName/3 all other events. - If the - - transition_option() - + If a - timeout + state_timeout() + + is set through + + action() timeout - is set a state timer may be started or a timeout zero event - may be enqueued as the newest incoming that is the last + a state timer may be started or a timeout zero event + may be enqueued as the newest incoming, that is the last to process before going into receive for new events. @@ -466,7 +564,12 @@ erlang:'!' -----> Module:StateName/3 state function is called with the oldest enqueued event if there is any, otherwise the gen_statem goes into receive - or hibernation (if the option hibernate is true) + or hibernation + (if + + hibernate() + + is true) to wait for the next message. In hibernation the next non-system event awakens the gen_statem, or rather the next incoming message awakens the gen_statem @@ -518,7 +621,7 @@ erlang:'!' -----> Module:StateName/3 Also note that it is not possible nor needed to cancel this timeout using the - action() cancel_timer. + action() cancel_timer since this timeout is cancelled automatically by any other event.

@@ -538,25 +641,22 @@ erlang:'!' -----> Module:StateName/3

Actions are executed in the containing list order. The order matters for some actions such as next_event - and reply_action(). The order can in peculiar cases - matter for remove_event with - EventPredicate versus other - event removal actions. + and reply_action().

- The order also matters for actions that set + Actions that set transition options - since setting an option overrides any previous - of the same kind, so the last in the containing list wins. + overrides any previous of the same kind, + so the last in the containing list wins.

postpone Set the - transition_option() postpone + transition_option() postpone() for this state transition. This action is ignored when returned from @@ -569,7 +669,7 @@ erlang:'!' -----> Module:StateName/3 Set the - transition_option() hibernate + transition_option() hibernate() for this state transition. @@ -577,7 +677,7 @@ erlang:'!' -----> Module:StateName/3 Set the - transition_option() timeout + transition_option() state_timeout() to Time with the EventContent as Msg @@ -594,7 +694,7 @@ erlang:'!' -----> Module:StateName/3 All next_event actions in the containing list are buffered and inserted after the actions have been done - so the first in the list will be the first to process. + so that the first in the list will be the first to process. An event of type internal @@ -612,7 +712,7 @@ erlang:'!' -----> Module:StateName/3 and postpone events in the same actions list does not get into the event queue until after all actions has been done so you can not remove an event that you insert - in the same actions list. Make up your mind! + with the same actions list. Make up your mind! cancel_timer @@ -622,7 +722,7 @@ erlang:'!' -----> Module:StateName/3 with TimerRef, clean the process message queue from any late timeout message, - and removes any late timeout message + and remove any late timeout message from the gen_statem event queue using {remove_event,EventPredicate} above. This is a convenience function that saves quite some @@ -639,7 +739,7 @@ erlang:'!' -----> Module:StateName/3 unlink - Like {cancel_timer,_} above but for + Like cancel_timer above but for unlink/1 -- cgit v1.2.3 From 1e4831762b5ab4bd2210fc9e0a204e00dfe81b39 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 26 Feb 2016 10:35:16 +0100 Subject: Implement 'keep_state_and_data' and 'stop' --- lib/stdlib/doc/src/gen_statem.xml | 7 ++++- lib/stdlib/src/gen_statem.erl | 60 ++++++++++++++++++++---------------- lib/stdlib/test/gen_statem_SUITE.erl | 4 +-- 3 files changed, 42 insertions(+), 29 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index d50b88c561..bda3ef081d 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -803,12 +803,17 @@ ok keep_state_and_data - The gen_statem will keep the current state, or + The gen_statem will keep the current state or do a state transition to the current state if you like, keep the current server data, and execute all Actions +

+ All these terms are tuples or atoms and this property + will hold in any future version of gen_statem, + just in case you need such a promise. +

diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index e6ad7ab0be..16c296794f 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -135,32 +135,34 @@ Caller :: caller(), Reply :: term()}. -type state_callback_result() :: - {'stop', % Stop the server - Reason :: term()} | - {'stop', % Stop the server - Reason :: term(), - NewData :: data()} | - {'stop_and_reply', % Reply then stop the server - Reason :: term(), - Replies :: [reply_action()] | reply_action()} | - {'stop_and_reply', % Reply then stop the server - Reason :: term(), - Replies :: [reply_action()] | reply_action(), - NewData :: data()} | - {'next_state', % {next_state,NewState,NewData,[]} - NewState :: state(), - NewData :: data()} | - {'next_state', % State transition, maybe to the same state - NewState :: state(), - NewData :: data(), - Actions :: [action()] | action()} | - {'keep_state', % {keep_state,NewData,[]} - NewData :: data()} | - {'keep_state', % Keep state, change data - NewData :: data(), - Actions :: [action()] | action()} | - {'keep_state_and_data', % Keep state and data -> only actions - Actions :: [action()] | action()}. + 'stop' | % {stop,normal} + {'stop', % Stop the server + Reason :: term()} | + {'stop', % Stop the server + Reason :: term(), + NewData :: data()} | + {'stop_and_reply', % Reply then stop the server + Reason :: term(), + Replies :: [reply_action()] | reply_action()} | + {'stop_and_reply', % Reply then stop the server + Reason :: term(), + Replies :: [reply_action()] | reply_action(), + NewData :: data()} | + {'next_state', % {next_state,NewState,NewData,[]} + NewState :: state(), + NewData :: data()} | + {'next_state', % State transition, maybe to the same state + NewState :: state(), + NewData :: data(), + Actions :: [action()] | action()} | + {'keep_state', % {keep_state,NewData,[]} + NewData :: data()} | + {'keep_state', % Keep state, change data + NewData :: data(), + Actions :: [action()] | action()} | + 'keep_state_and_data' | % {keep_state_and_data,[]} + {'keep_state_and_data', % Keep state and data -> only actions + Actions :: [action()] | action()}. %% The state machine init function. It is called only once and @@ -841,6 +843,8 @@ loop_event_result( #{state := State, data := Data} = S, Events, Event, Result) -> case Result of + stop -> + ?TERMINATE(exit, normal, Debug, S, [Event|Events]); {stop,Reason} -> ?TERMINATE(exit, Reason, Debug, S, [Event|Events]); {stop,Reason,NewData} -> @@ -879,6 +883,10 @@ loop_event_result( loop_event_actions( Parent, Debug, S, Events, Event, State, State, NewData, Actions); + keep_state_and_data -> + loop_event_actions( + Parent, Debug, S, Events, Event, + State, State, Data, []); {keep_state_and_data,Actions} -> loop_event_actions( Parent, Debug, S, Events, Event, diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index f5cbdd9a40..09b309c36f 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -1233,7 +1233,7 @@ wfor_conf(Type, Content, Data) -> {next_state,idle,Data, [{reply,From,'eh?'}]}; _ -> - throw({keep_state_and_data,[]}) + throw(keep_state_and_data) end; Result -> Result @@ -1334,7 +1334,7 @@ handle_common_events(cast, {get,Pid}, State, Data) -> handle_common_events({call,From}, stop, _, Data) -> {stop_and_reply,normal,[{reply,From,stopped}],Data}; handle_common_events(cast, stop, _, _) -> - {stop,normal}; + stop; handle_common_events({call,From}, {stop,Reason}, _, Data) -> {stop_and_reply,Reason,{reply,From,stopped},Data}; handle_common_events(cast, {stop,Reason}, _, _) -> -- cgit v1.2.3 From efda33e83f2bbc666f6ec261d54ad95c38acbc36 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 26 Feb 2016 10:51:53 +0100 Subject: Allow actions without containing list Type check atom state as early as possible --- lib/stdlib/src/gen_statem.erl | 21 ++++++++++++++++----- lib/stdlib/test/gen_statem_SUITE.erl | 6 +++--- 2 files changed, 19 insertions(+), 8 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 16c296794f..83de4114a7 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -571,7 +571,9 @@ init_result(Starter, Parent, ServerRef, Module, Result, Opts) -> case Result of {CallbackMode,State,Data} -> case callback_mode(CallbackMode) of - true -> + true + when CallbackMode =:= state_functions, is_atom(State); + CallbackMode =/= state_functions -> proc_lib:init_ack(Starter, {ok,self()}), enter( Module, Opts, CallbackMode, State, Data, @@ -840,7 +842,7 @@ loop_events( %% Interpret all callback return value variants loop_event_result( Parent, Debug, - #{state := State, data := Data} = S, + #{callback_mode := CallbackMode, state := State, data := Data} = S, Events, Event, Result) -> case Result of stop -> @@ -866,12 +868,15 @@ loop_event_result( exit, Reason, ?STACKTRACE(), Debug, NewS, Q, Replies), %% Since we got back here Replies was bad terminate(Class, NewReason, Stacktrace, NewDebug, NewS, Q); - {next_state,NewState,NewData} -> + {next_state,NewState,NewData} + when CallbackMode =:= state_functions, is_atom(NewState); + CallbackMode =/= state_functions -> loop_event_actions( Parent, Debug, S, Events, Event, State, NewState, NewData, []); {next_state,NewState,NewData,Actions} - when is_list(Actions) -> + when CallbackMode =:= state_functions, is_atom(NewState); + CallbackMode =/= state_functions -> loop_event_actions( Parent, Debug, S, Events, Event, State, NewState, NewData, Actions); @@ -900,7 +905,13 @@ loop_event_actions( Parent, Debug, S, Events, Event, State, NewState, NewData, Actions) -> loop_event_actions( Parent, Debug, S, Events, Event, State, NewState, NewData, - false, false, undefined, [], Actions). + false, false, undefined, [], + if + is_list(Actions) -> + Actions; + true -> + [Actions] + end). %% loop_event_actions( Parent, Debug, #{postponed := P0} = S, Events, Event, diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 09b309c36f..0429c0e5f4 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -1178,7 +1178,7 @@ idle({call,From}, {delayed_answer,T}, Data) -> end; idle({call,From}, {timeout,Time}, _Data) -> {next_state,timeout,{From,Time}, - [{timeout,Time,idle}]}; + {timeout,Time,idle}}; idle(Type, Content, Data) -> case handle_common_events(Type, Content, idle, Data) of undefined -> @@ -1220,7 +1220,7 @@ timeout3(_, _, Data) -> wfor_conf({call,From}, confirm, Data) -> {next_state,connected,Data, - [{reply,From,yes}]}; + {reply,From,yes}}; wfor_conf(cast, {ping,_,_}, _) -> {keep_state_and_data,[postpone]}; wfor_conf(cast, confirm, Data) -> @@ -1241,7 +1241,7 @@ wfor_conf(Type, Content, Data) -> connected({call,From}, {msg,Ref}, Data) -> {keep_state,Data, - [{reply,From,{ack,Ref}}]}; + {reply,From,{ack,Ref}}}; connected(cast, {msg,From,Ref}, Data) -> From ! {ack,Ref}, {keep_state,Data}; -- cgit v1.2.3 From 07790dfcd4c5706878e8562365e2418021e67056 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 26 Feb 2016 11:58:47 +0100 Subject: Optimize postponed handling --- lib/stdlib/src/gen_statem.erl | 187 +++++++++++++++++++++++------------------- 1 file changed, 101 insertions(+), 86 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 83de4114a7..8f058cc3a8 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -903,108 +903,63 @@ loop_event_result( loop_event_actions( Parent, Debug, S, Events, Event, State, NewState, NewData, Actions) -> + Postpone = false, % Shall we postpone this event, true or false + Hibernate = false, + Timeout = undefined, + NextEvents = [], + P = false, % The postponed list or false if unchanged loop_event_actions( Parent, Debug, S, Events, Event, State, NewState, NewData, - false, false, undefined, [], if is_list(Actions) -> Actions; true -> [Actions] - end). + end, + Postpone, Hibernate, Timeout, NextEvents, P). %% loop_event_actions( - Parent, Debug, #{postponed := P0} = S, Events, Event, - State, NewState, NewData, - Postpone, Hibernate, Timeout, NextEvents, []) -> - P1 = % Move current event to postponed if Postpone - case Postpone of - true -> - [Event|P0]; - false -> - P0 - end, - {Timer,Q1} = - case Timeout of - undefined -> - {undefined,Events}; - {timeout,0,Msg} -> - %% Pretend the timeout has just been received - {undefined,Events ++ [{timeout,Msg}]}; - {timeout,Time,Msg} -> - {erlang:start_timer(Time, self(), Msg), - Events} - end, - {Q2,P} = % Move all postponed events to queue if state change - if - NewState =:= State -> - {Q1,P1}; - true -> - {lists:reverse(P1, Q1),[]} - end, - %% Place next events first in queue - Q = lists:reverse(NextEvents, Q2), - %% - NewDebug = - sys_debug( - Debug, S, - case Postpone of - true -> - {postpone,Event,NewState}; - false -> - {consume,Event,NewState} - end), - %% Loop to top; process next event - loop_events( - Parent, NewDebug, - S#{ - state := NewState, - data := NewData, - timer := Timer, - hibernate := Hibernate, - postponed := P}, - Q, Timer); -loop_event_actions( - Parent, Debug, S, Events, Event, State, NewState, NewData, - Postpone, Hibernate, Timeout, NextEvents, [Action|Actions]) -> + Parent, Debug, S, Events, Event, + State, NewState, NewData, [Action|Actions], + Postpone, Hibernate, Timeout, NextEvents, P) -> case Action of %% Set options postpone -> loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, - true, Hibernate, Timeout, NextEvents, Actions); + State, NewState, NewData, Actions, + true, Hibernate, Timeout, NextEvents, P); {postpone,NewPostpone} when is_boolean(NewPostpone) -> loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, - NewPostpone, Hibernate, Timeout, NextEvents, Actions); + State, NewState, NewData, Actions, + NewPostpone, Hibernate, Timeout, NextEvents, P); {postpone,_} -> ?TERMINATE( error, {bad_action,Action}, Debug, S, [Event|Events]); hibernate -> loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, - Postpone, true, Timeout, NextEvents, Actions); + State, NewState, NewData, Actions, + Postpone, true, Timeout, NextEvents, P); {hibernate,NewHibernate} when is_boolean(NewHibernate) -> loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, - Postpone, NewHibernate, Timeout, NextEvents, Actions); + State, NewState, NewData, Actions, + Postpone, NewHibernate, Timeout, NextEvents, P); {hibernate,_} -> ?TERMINATE( error, {bad_action,Action}, Debug, S, [Event|Events]); {timeout,infinity,_} -> % Clear timer - it will never trigger loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, - Postpone, Hibernate, undefined, NextEvents, Actions); + State, NewState, NewData, Actions, + Postpone, Hibernate, undefined, NextEvents, P); {timeout,Time,_} = NewTimeout when is_integer(Time), Time >= 0 -> loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, - Postpone, Hibernate, NewTimeout, NextEvents, Actions); + State, NewState, NewData, Actions, + Postpone, Hibernate, NewTimeout, NextEvents, P); {timeout,_,_} -> ?TERMINATE( error, {bad_action,Action}, Debug, S, [Event|Events]); @@ -1015,8 +970,8 @@ loop_event_actions( NewDebug = do_reply(Debug, S, Caller, Reply), loop_event_actions( Parent, NewDebug, S, Events, Event, - State, NewState, NewData, - Postpone, Hibernate, Timeout, NextEvents, Actions); + State, NewState, NewData, Actions, + Postpone, Hibernate, Timeout, NextEvents, P); false -> ?TERMINATE( error, {bad_action,Action}, Debug, S, [Event|Events]) @@ -1026,9 +981,9 @@ loop_event_actions( true -> loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, + State, NewState, NewData, Actions, Postpone, Hibernate, Timeout, - [{Type,Content}|NextEvents], Actions); + [{Type,Content}|NextEvents], P); false -> ?TERMINATE( error, {bad_action,Action}, Debug, S, [Event|Events]) @@ -1039,33 +994,36 @@ loop_event_actions( false -> loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, - Postpone, Hibernate, Timeout, NextEvents, Actions); + State, NewState, NewData, Actions, + Postpone, Hibernate, Timeout, NextEvents, P); undefined -> ?TERMINATE( error, {bad_action,Action}, Debug, S, [Event|Events]); RemoveFun when is_function(RemoveFun, 2) -> - #{postponed := P} = S, - case remove_event(RemoveFun, Events, P) of + P0 = + case P of + false -> + maps:get(postponed, S); + _ -> + P + end, + case remove_event(RemoveFun, Events, P0) of false -> loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, - Postpone, Hibernate, Timeout, NextEvents, - Actions); + State, NewState, NewData, Actions, + Postpone, Hibernate, Timeout, NextEvents, P); {NewEvents,false} -> loop_event_actions( Parent, Debug, S, NewEvents, Event, - State, NewState, NewData, - Postpone, Hibernate, Timeout, NextEvents, - Actions); + State, NewState, NewData, Actions, + Postpone, Hibernate, Timeout, NextEvents, P); {false,NewP} -> - NewS = S#{postponed := NewP}, loop_event_actions( - Parent, Debug, NewS, Events, Event, - State, NewState, NewData, + Parent, Debug, S, Events, Event, + State, NewState, NewData, Actions, Postpone, Hibernate, Timeout, NextEvents, - Actions); + NewP); [Class,Reason,Stacktrace] -> terminate( Class, Reason, Stacktrace, @@ -1075,7 +1033,64 @@ loop_event_actions( terminate( Class, Reason, Stacktrace, Debug, S, [Event|Events]) end - end. + end; +loop_event_actions( + Parent, Debug, S, Events, Event, State, NewState, NewData, [], + Postpone, Hibernate, Timeout, NextEvents, P) -> + P0 = + case P of + false -> + maps:get(postponed, S); + _ -> + P + end, + P1 = % Move current event to postponed if Postpone + case Postpone of + true -> + [Event|P0]; + false -> + P0 + end, + {Timer,Q1} = + case Timeout of + undefined -> + {undefined,Events}; + {timeout,0,Msg} -> + %% Pretend the timeout has just been received + {undefined,Events ++ [{timeout,Msg}]}; + {timeout,Time,Msg} -> + {erlang:start_timer(Time, self(), Msg), + Events} + end, + {Q2,P2} = % Move all postponed events to queue if state change + if + NewState =:= State -> + {Q1,P1}; + true -> + {lists:reverse(P1, Q1),[]} + end, + %% Place next events first in queue + Q3 = lists:reverse(NextEvents, Q2), + %% + NewDebug = + sys_debug( + Debug, S, + case Postpone of + true -> + {postpone,Event,NewState}; + false -> + {consume,Event,NewState} + end), + %% Loop to top; process next event + loop_events( + Parent, NewDebug, + S#{ + state := NewState, + data := NewData, + timer := Timer, + hibernate := Hibernate, + postponed := P2}, + Q3, Timer). %%--------------------------------------------------------------------------- %% Server helpers -- cgit v1.2.3 From ded8d9e3ec0ba6481d80e28f6057e6ccf5a6d575 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 26 Feb 2016 14:35:25 +0100 Subject: ct:ify test suite --- lib/stdlib/test/gen_statem_SUITE.erl | 165 ++++++++++++++++++----------------- 1 file changed, 86 insertions(+), 79 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 0429c0e5f4..321573b721 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -26,7 +26,9 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -suite() -> [{ct_hooks,[ts_install_cth]}]. +suite() -> + [{ct_hooks,[ts_install_cth]}, + {timetrap,{minutes,1}}]. all() -> [{group, start}, @@ -82,27 +84,24 @@ end_per_group(_GroupName, Config) -> Config. init_per_testcase(_CaseName, Config) -> - ?t:messages_get(), - Dog = ?t:timetrap(?t:minutes(1)), + flush(), %%% dbg:tracer(), %%% dbg:p(all, c), %%% dbg:tpl(gen_statem, cx), %%% dbg:tpl(proc_lib, cx), %%% dbg:tpl(gen, cx), %%% dbg:tpl(sys, cx), - [{watchdog, Dog} | Config]. + Config. end_per_testcase(_CaseName, Config) -> %%% dbg:stop(), - Dog = ?config(watchdog, Config), - test_server:timetrap_cancel(Dog), Config. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -define(EXPECT_FAILURE(Code, Reason), try begin Code end of - _ -> - ?t:fail() + Reason -> + ct:fail({unexpected,Reason}) catch error:Reason -> Reason; exit:Reason -> Reason @@ -110,7 +109,7 @@ end_per_testcase(_CaseName, Config) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% anonymous -start1(Config) when is_list(Config) -> +start1(Config) -> %%OldFl = process_flag(trap_exit, true), {ok,Pid0} = gen_statem:start_link(?MODULE, start_arg(Config, []), []), @@ -125,7 +124,7 @@ start1(Config) when is_list(Config) -> ok = verify_empty_msgq(). %% anonymous w. shutdown -start2(Config) when is_list(Config) -> +start2(Config) -> %% Dont link when shutdown {ok,Pid0} = gen_statem:start(?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid0), @@ -135,7 +134,7 @@ start2(Config) when is_list(Config) -> ok = verify_empty_msgq(). %% anonymous with timeout -start3(Config) when is_list(Config) -> +start3(Config) -> %%OldFl = process_flag(trap_exit, true), {ok,Pid0} = @@ -152,7 +151,7 @@ start3(Config) when is_list(Config) -> ok = verify_empty_msgq(). %% anonymous with ignore -start4(Config) when is_list(Config) -> +start4(Config) -> OldFl = process_flag(trap_exit, true), ignore = gen_statem:start(?MODULE, start_arg(Config, ignore), []), @@ -161,8 +160,7 @@ start4(Config) when is_list(Config) -> ok = verify_empty_msgq(). %% anonymous with stop -start5(suite) -> []; -start5(Config) when is_list(Config) -> +start5(Config) -> OldFl = process_flag(trap_exit, true), {error,stopped} = gen_statem:start(?MODULE, start_arg(Config, stop), []), @@ -171,7 +169,7 @@ start5(Config) when is_list(Config) -> ok = verify_empty_msgq(). %% anonymous linked -start6(Config) when is_list(Config) -> +start6(Config) -> {ok,Pid} = gen_statem:start_link(?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid), ok = do_sync_func_test(Pid), @@ -180,7 +178,7 @@ start6(Config) when is_list(Config) -> ok = verify_empty_msgq(). %% global register linked -start7(Config) when is_list(Config) -> +start7(Config) -> STM = {global,my_stm}, {ok,Pid} = @@ -200,7 +198,7 @@ start7(Config) when is_list(Config) -> %% local register -start8(Config) when is_list(Config) -> +start8(Config) -> %%OldFl = process_flag(trap_exit, true), Name = my_stm, STM = {local,Name}, @@ -220,7 +218,7 @@ start8(Config) when is_list(Config) -> ok = verify_empty_msgq(). %% local register linked -start9(Config) when is_list(Config) -> +start9(Config) -> %%OldFl = process_flag(trap_exit, true), Name = my_stm, STM = {local,Name}, @@ -240,7 +238,7 @@ start9(Config) when is_list(Config) -> ok = verify_empty_msgq(). %% global register -start10(Config) when is_list(Config) -> +start10(Config) -> STM = {global,my_stm}, {ok,Pid} = @@ -259,7 +257,7 @@ start10(Config) when is_list(Config) -> ok = verify_empty_msgq(). %% Stop registered processes -start11(Config) when is_list(Config) -> +start11(Config) -> Name = my_stm, LocalSTM = {local,Name}, GlobalSTM = {global,Name}, @@ -278,14 +276,14 @@ start11(Config) when is_list(Config) -> receive after 1 -> true end, Result = gen_statem:start(GlobalSTM, ?MODULE, start_arg(Config, []), []), - io:format("Result = ~p~n",[Result]), + ct:log("Result = ~p~n",[Result]), {ok,_Pid3} = Result, stop_it(GlobalSTM), ok = verify_empty_msgq(). %% Via register linked -start12(Config) when is_list(Config) -> +start12(Config) -> dummy_via:reset(), VIA = {via,dummy_via,my_stm}, @@ -378,19 +376,20 @@ stop7(Config) -> %% Anonymous on remote node stop8(Config) -> - {ok,Node} = ?t:start_node(gen_statem_stop8, slave, []), + Node = gen_statem_stop8, + {ok,NodeName} = ct_slave:start(Node), Dir = filename:dirname(code:which(?MODULE)), - rpc:call(Node, code, add_path, [Dir]), + rpc:call(NodeName, code, add_path, [Dir]), {ok,Pid} = rpc:call( - Node, gen_statem,start, + NodeName, gen_statem,start, [?MODULE,start_arg(Config, []),[]]), ok = gen_statem:stop(Pid), - false = rpc:call(Node, erlang, is_process_alive, [Pid]), + false = rpc:call(NodeName, erlang, is_process_alive, [Pid]), noproc = ?EXPECT_FAILURE(gen_statem:stop(Pid), Reason1), - true = ?t:stop_node(Node), - {{nodedown,Node},{sys,terminate,_}} = + {ok,NodeName} = ct_slave:stop(Node), + {{nodedown,NodeName},{sys,terminate,_}} = ?EXPECT_FAILURE(gen_statem:stop(Pid), Reason2), ok. @@ -398,46 +397,48 @@ stop8(Config) -> stop9(Config) -> Name = to_stop, LocalSTM = {local,Name}, - {ok,Node} = ?t:start_node(gen_statem__stop9, slave, []), - STM = {Name,Node}, + Node = gen_statem__stop9, + {ok,NodeName} = ct_slave:start(Node), + STM = {Name,NodeName}, Dir = filename:dirname(code:which(?MODULE)), - rpc:call(Node, code, add_path, [Dir]), + rpc:call(NodeName, code, add_path, [Dir]), {ok,Pid} = rpc:call( - Node, gen_statem, start, + NodeName, gen_statem, start, [LocalSTM,?MODULE,start_arg(Config, []),[]]), ok = gen_statem:stop(STM), - undefined = rpc:call(Node,erlang,whereis,[Name]), - false = rpc:call(Node,erlang,is_process_alive,[Pid]), + undefined = rpc:call(NodeName,erlang,whereis,[Name]), + false = rpc:call(NodeName,erlang,is_process_alive,[Pid]), noproc = ?EXPECT_FAILURE(gen_statem:stop(STM), Reason1), - true = ?t:stop_node(Node), - {{nodedown,Node},{sys,terminate,_}} = + {ok,NodeName} = ct_slave:stop(Node), + {{nodedown,NodeName},{sys,terminate,_}} = ?EXPECT_FAILURE(gen_statem:stop(STM), Reason2), ok. %% Globally registered name on remote node stop10(Config) -> + Node = gen_statem_stop10, STM = {global,to_stop}, - {ok,Node} = ?t:start_node(gen_statem_stop10, slave, []), + {ok,NodeName} = ct_slave:start(Node), Dir = filename:dirname(code:which(?MODULE)), - rpc:call(Node,code,add_path,[Dir]), + rpc:call(NodeName,code,add_path,[Dir]), {ok,Pid} = rpc:call( - Node, gen_statem, start, + NodeName, gen_statem, start, [STM,?MODULE,start_arg(Config, []),[]]), global:sync(), ok = gen_statem:stop(STM), - false = rpc:call(Node, erlang, is_process_alive, [Pid]), + false = rpc:call(NodeName, erlang, is_process_alive, [Pid]), noproc = ?EXPECT_FAILURE(gen_statem:stop(STM), Reason1), - true = ?t:stop_node(Node), + {ok,NodeName} = ct_slave:stop(Node), noproc = ?EXPECT_FAILURE(gen_statem:stop(STM), Reason2), ok. %% Check that time outs in calls work -abnormal1(Config) when is_list(Config) -> +abnormal1(Config) -> Name = abnormal1, LocalSTM = {local,Name}, @@ -455,7 +456,7 @@ abnormal1(Config) when is_list(Config) -> %% Check that bad return values makes the stm crash. Note that we must %% trap exit since we must link to get the real bad_return_ error -abnormal2(Config) when is_list(Config) -> +abnormal2(Config) -> OldFl = process_flag(trap_exit, true), {ok,Pid} = gen_statem:start_link(?MODULE, start_arg(Config, []), []), @@ -465,13 +466,13 @@ abnormal2(Config) when is_list(Config) -> receive {'EXIT',Pid,{bad_return_value,badreturn}} -> ok after 5000 -> - ?t:fail(gen_statem_did_not_die) + ct:fail(gen_statem_did_not_die) end, process_flag(trap_exit, OldFl), ok = verify_empty_msgq(). -shutdown(Config) when is_list(Config) -> +shutdown(Config) -> process_flag(trap_exit, true), {ok,Pid0} = gen_statem:start_link(?MODULE, start_arg(Config, []), []), @@ -486,15 +487,15 @@ shutdown(Config) when is_list(Config) -> receive Any -> - io:format("Unexpected: ~p", [Any]), - ?t:fail() + ct:log("Unexpected: ~p", [Any]), + ct:fail({unexpected,Any}) after 500 -> ok end. -sys1(Config) when is_list(Config) -> +sys1(Config) -> {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), {status, Pid, {module,gen_statem}, _} = sys:get_status(Pid), sys:suspend(Pid), @@ -507,7 +508,7 @@ sys1(Config) when is_list(Config) -> end), receive {Tag,_} -> - ?t:fail() + ct:fail(should_be_suspended) after 3000 -> exit(Caller, ok) end, @@ -517,7 +518,7 @@ sys1(Config) when is_list(Config) -> sys:resume(Pid), stop_it(Pid). -call_format_status(Config) when is_list(Config) -> +call_format_status(Config) -> {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), Status = sys:get_status(Pid), {status,Pid,_Mod,[_PDict,running,_,_, Data]} = Status, @@ -573,7 +574,7 @@ call_format_status(Config) when is_list(Config) -> -error_format_status(Config) when is_list(Config) -> +error_format_status(Config) -> error_logger_forwarder:register(), OldFl = process_flag(trap_exit, true), Data = "called format_status", @@ -593,10 +594,10 @@ error_format_status(Config) when is_list(Config) -> ok; Other when is_tuple(Other), element(1, Other) =:= error -> error_logger_forwarder:unregister(), - ?t:fail({unexpected,Other}) + ct:fail({unexpected,Other}) after 1000 -> error_logger_forwarder:unregister(), - ?t:fail() + ct:fail(timeout) end, process_flag(trap_exit, OldFl), error_logger_forwarder:unregister(), @@ -609,7 +610,7 @@ error_format_status(Config) when is_list(Config) -> end, ok = verify_empty_msgq(). -terminate_crash_format(Config) when is_list(Config) -> +terminate_crash_format(Config) -> error_logger_forwarder:register(), OldFl = process_flag(trap_exit, true), Data = crash_terminate, @@ -629,10 +630,10 @@ terminate_crash_format(Config) when is_list(Config) -> ok; Other when is_tuple(Other), element(1, Other) =:= error -> error_logger_forwarder:unregister(), - ?t:fail({unexpected,Other}) + ct:fail({unexpected,Other}) after 1000 -> error_logger_forwarder:unregister(), - ?t:fail() + ct:fail(timeout) end, process_flag(trap_exit, OldFl), error_logger_forwarder:unregister(), @@ -646,7 +647,7 @@ terminate_crash_format(Config) when is_list(Config) -> ok = verify_empty_msgq(). -get_state(Config) when is_list(Config) -> +get_state(Config) -> State = self(), {ok,Pid} = gen_statem:start( @@ -675,7 +676,7 @@ get_state(Config) when is_list(Config) -> stop_it(Pid3), ok = verify_empty_msgq(). -replace_state(Config) when is_list(Config) -> +replace_state(Config) -> State = self(), {ok, Pid} = gen_statem:start( @@ -707,7 +708,7 @@ replace_state(Config) when is_list(Config) -> ok = verify_empty_msgq(). %% Hibernation -hibernate(Config) when is_list(Config) -> +hibernate(Config) -> OldFl = process_flag(trap_exit, true), {ok,Pid0} = @@ -718,7 +719,7 @@ hibernate(Config) when is_list(Config) -> receive {'EXIT',Pid0,normal} -> ok after 5000 -> - ?t:fail(gen_statem_did_not_die) + ct:fail(gen_statem_did_not_die) end, {ok,Pid} = @@ -801,7 +802,7 @@ hibernate(Config) when is_list(Config) -> receive {'EXIT',Pid,normal} -> ok after 5000 -> - ?t:fail(gen_statem_did_not_die) + ct:fail(gen_statem_did_not_die) end, ok = verify_empty_msgq(). @@ -810,8 +811,8 @@ is_in_erlang_hibernate(Pid) -> is_in_erlang_hibernate_1(200, Pid). is_in_erlang_hibernate_1(0, Pid) -> - io:format("~p\n", [erlang:process_info(Pid, current_function)]), - ?t:fail(not_in_erlang_hibernate_3); + ct:log("~p\n", [erlang:process_info(Pid, current_function)]), + ct:fail(not_in_erlang_hibernate_3); is_in_erlang_hibernate_1(N, Pid) -> {current_function,MFA} = erlang:process_info(Pid, current_function), case MFA of @@ -827,8 +828,8 @@ is_not_in_erlang_hibernate(Pid) -> is_not_in_erlang_hibernate_1(200, Pid). is_not_in_erlang_hibernate_1(0, Pid) -> - io:format("~p\n", [erlang:process_info(Pid, current_function)]), - ?t:fail(not_in_erlang_hibernate_3); + ct:log("~p\n", [erlang:process_info(Pid, current_function)]), + ct:fail(not_in_erlang_hibernate_3); is_not_in_erlang_hibernate_1(N, Pid) -> {current_function,MFA} = erlang:process_info(Pid, current_function), case MFA of @@ -839,10 +840,8 @@ is_not_in_erlang_hibernate_1(N, Pid) -> ok end. -%%sys1(suite) -> []; -%%sys1(_) -> -enter_loop(Config) when is_list(Config) -> +enter_loop(_Config) -> OldFlag = process_flag(trap_exit, true), dummy_via:reset(), @@ -856,7 +855,7 @@ enter_loop(Config) when is_list(Config) -> {'EXIT',Pid1a,normal} -> ok after 5000 -> - ?t:fail(gen_statem_did_not_die) + ct:fail(gen_statem_did_not_die) end, %% Unregistered process + {local,Name} @@ -866,7 +865,7 @@ enter_loop(Config) when is_list(Config) -> {'EXIT',Pid1b,process_not_registered} -> ok after 5000 -> - ?t:fail(gen_statem_did_not_die) + ct:fail(gen_statem_did_not_die) end, %% Globally registered process + {global,Name} @@ -878,7 +877,7 @@ enter_loop(Config) when is_list(Config) -> {'EXIT',Pid2a,normal} -> ok after 5000 -> - ?t:fail(gen_statem_did_not_die) + ct:fail(gen_statem_did_not_die) end, %% Unregistered process + {global,Name} @@ -888,7 +887,7 @@ enter_loop(Config) when is_list(Config) -> {'EXIT',Pid2b,process_not_registered_globally} -> ok after 5000 -> - ?t:fail(gen_statem_did_not_die) + ct:fail(gen_statem_did_not_die) end, %% Unregistered process + no name @@ -900,7 +899,7 @@ enter_loop(Config) when is_list(Config) -> {'EXIT',Pid3,normal} -> ok after 5000 -> - ?t:fail(gen_statem_did_not_die) + ct:fail(gen_statem_did_not_die) end, %% Process not started using proc_lib @@ -913,7 +912,7 @@ enter_loop(Config) when is_list(Config) -> {'EXIT',Pid4,process_was_not_started_by_proc_lib} -> ok after 5000 -> - ?t:fail(gen_statem_did_not_die) + ct:fail(gen_statem_did_not_die) end, %% Make sure I am the parent, ie that ordering a shutdown will @@ -926,7 +925,7 @@ enter_loop(Config) when is_list(Config) -> {'EXIT',Pid5,shutdown} -> ok after 5000 -> - ?t:fail(gen_statem_did_not_die) + ct:fail(gen_statem_did_not_die) end, %% Make sure gen_statem:enter_loop does not accept {local,Name} @@ -939,7 +938,7 @@ enter_loop(Config) when is_list(Config) -> {'EXIT',Pid6a,process_not_registered} -> ok after 1000 -> - ?t:fail(gen_statem_started) + ct:fail(gen_statem_started) end, unregister(armitage), @@ -953,7 +952,7 @@ enter_loop(Config) when is_list(Config) -> {'EXIT',Pid6b,process_not_registered_globally} -> ok after 1000 -> - ?t:fail(gen_statem_started) + ct:fail(gen_statem_started) end, global:unregister_name(armitage), @@ -964,7 +963,7 @@ enter_loop(Config) when is_list(Config) -> {'EXIT',Pid6c,{process_not_registered_via,dummy_via}} -> ok after 1000 -> - ?t:fail( + ct:fail( {gen_statem_started, process_info(self(), messages)}) end, @@ -1105,7 +1104,7 @@ do_sync_disconnect(STM) -> verify_empty_msgq() -> receive after 500 -> ok end, - [] = ?t:messages_get(), + [] = flush(), ok. start_arg(Config, Arg) -> @@ -1388,3 +1387,11 @@ format_status(terminate, [_Pdict,State,Data]) -> {formatted,State,Data}; format_status(normal, [_Pdict,_State,_Data]) -> [format_status_called]. + +flush() -> + receive + Msg -> + [Msg|flush()] + after 0 -> + [] + end. -- cgit v1.2.3 From df457111fbb82d160605f45c43552249cd6f8ac5 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 26 Feb 2016 14:52:24 +0100 Subject: Test the order for multiple next_event --- lib/stdlib/test/gen_statem_SUITE.erl | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 321573b721..268b45a0e7 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -44,10 +44,10 @@ all() -> groups() -> [{start, [], [start1, start2, start3, start4, start5, start6, start7, - start8, start9, start10, start11, start12]}, + start8, start9, start10, start11, start12, next_events]}, {start_handle_event, [], [start1, start2, start3, start4, start5, start6, start7, - start8, start9, start10, start11, start12]}, + start8, start9, start10, start11, start12, next_events]}, {stop, [], [stop1, stop2, stop3, stop4, stop5, stop6, stop7, stop8, stop9, stop10]}, {stop_handle_event, [], @@ -998,6 +998,18 @@ enter_loop(Reg1, Reg2) -> ?MODULE, [], CallbackMode, state0, []) end. + +%% Test the order for multiple {next_event,T,C} +next_events(Config) -> + {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), + ok = gen_statem:cast(Pid, next_event), + {state,next_events,[]} = gen_statem:call(Pid, get), + ok = gen_statem:stop(Pid), + false = erlang:is_process_alive(Pid), + noproc = + ?EXPECT_FAILURE(gen_statem:stop(Pid), Reason). + + %% %% Functionality check %% @@ -1178,6 +1190,11 @@ idle({call,From}, {delayed_answer,T}, Data) -> idle({call,From}, {timeout,Time}, _Data) -> {next_state,timeout,{From,Time}, {timeout,Time,idle}}; +idle(cast, next_event, _Data) -> + {next_state,next_events,[a,b,c], + [{next_event,internal,a}, + {next_event,internal,b}, + {next_event,internal,c}]}; idle(Type, Content, Data) -> case handle_common_events(Type, Content, idle, Data) of undefined -> @@ -1323,6 +1340,16 @@ hiber_wakeup(Type, Content, Data) -> Result end. +next_events(internal, Msg, [Msg|Msgs]) -> + {keep_state,Msgs}; +next_events(Type, Content, Data) -> + case handle_common_events(Type, Content, next_events, Data) of + undefined -> + {keep_state,Data}; + Result -> + Result + end. + handle_common_events({call,From}, get, State, Data) -> {keep_state,Data, -- cgit v1.2.3 From c2b8e6cdbc884e93cabe78e9fc9dcc040eb828eb Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 26 Feb 2016 16:19:55 +0100 Subject: Relax caller() type check and cleanup --- lib/stdlib/src/gen_statem.erl | 56 ++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 27 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 8f058cc3a8..29848d13a3 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -249,7 +249,7 @@ callback_mode(CallbackMode) -> false end. %% -caller({Pid,Tag}) when is_pid(Pid), is_reference(Tag) -> +caller({Pid,_}) when is_pid(Pid) -> true; caller(_) -> false. @@ -279,7 +279,7 @@ event_type(Type) -> -define( TERMINATE(Class, Reason, Debug, S, Q), terminate( - Class, + begin Class end, begin Reason end, ?STACKTRACE(), begin Debug end, @@ -355,23 +355,23 @@ stop(ServerRef, Reason, Timeout) -> %% Send an event to a state machine that arrives with type 'event' -spec cast(ServerRef :: server_ref(), Msg :: term()) -> ok. cast({global,Name}, Msg) -> - try global:send(Name, cast(Msg)) of + try global:send(Name, wrap_cast(Msg)) of _ -> ok catch _:_ -> ok end; cast({via,RegMod,Name}, Msg) -> - try RegMod:send(Name, cast(Msg)) of + try RegMod:send(Name, wrap_cast(Msg)) of _ -> ok catch _:_ -> ok end; cast({Name,Node} = ServerRef, Msg) when is_atom(Name), is_atom(Node) -> - do_send(ServerRef, cast(Msg)); + send(ServerRef, wrap_cast(Msg)); cast(ServerRef, Msg) when is_atom(ServerRef) -> - do_send(ServerRef, cast(Msg)); + send(ServerRef, wrap_cast(Msg)); cast(ServerRef, Msg) when is_pid(ServerRef) -> - do_send(ServerRef, cast(Msg)). + send(ServerRef, wrap_cast(Msg)). %% Call a state machine (synchronous; a reply is expected) that %% arrives with type {call,Caller} @@ -425,7 +425,7 @@ call(ServerRef, Request, Timeout) -> {Reason,{?MODULE,call,[ServerRef,Request,Timeout]}}, Stacktrace); {'DOWN',Mref,_,_,Reason} -> - %% There is just a theoretical possibility that the + %% There is a theoretical possibility that the %% proxy process gets killed between try--of and ! %% so this clause is in case of that exit(Reason) @@ -433,14 +433,13 @@ call(ServerRef, Request, Timeout) -> %% Reply from a state machine callback to whom awaits in call/2 -spec reply([reply_action()] | reply_action()) -> ok. -reply({reply,{_To,_Tag}=Caller,Reply}) -> +reply({reply,Caller,Reply}) -> reply(Caller, Reply); reply(Replies) when is_list(Replies) -> - [reply(Reply) || Reply <- Replies], - ok. + replies(Replies). %% -spec reply(Caller :: caller(), Reply :: term()) -> ok. -reply({To,Tag}, Reply) -> +reply({To,Tag}, Reply) when is_pid(To) -> Msg = {Tag,Reply}, try To ! Msg of _ -> @@ -503,11 +502,17 @@ enter_loop(Module, Opts, CallbackMode, State, Data, Server, Actions) -> %%--------------------------------------------------------------------------- %% API helpers -cast(Event) -> +wrap_cast(Event) -> {'$gen_cast',Event}. +replies([{reply,Caller,Reply}|Replies]) -> + reply(Caller, Reply), + replies(Replies); +replies([]) -> + ok. + %% Might actually not send the message in case of caught exception -do_send(Proc, Msg) -> +send(Proc, Msg) -> try erlang:send(Proc, Msg, [noconnect]) of noconnect -> _ = spawn(erlang, send, [Proc,Msg]), @@ -723,20 +728,17 @@ wakeup_from_hibernate(Parent, Debug, S) -> loop(Parent, Debug, #{hibernate := Hib} = S) -> case Hib of true -> - loop_hibernate(Parent, Debug, S); + %% Does not return but restarts process at + %% wakeup_from_hibernate/3 that jumps to loop_receive/3 + proc_lib:hibernate( + ?MODULE, wakeup_from_hibernate, [Parent,Debug,S]), + error( + {should_not_have_arrived_here_but_instead_in, + {wakeup_from_hibernate,3}}); false -> loop_receive(Parent, Debug, S) end. -loop_hibernate(Parent, Debug, S) -> - %% Does not return but restarts process at - %% wakeup_from_hibernate/3 that jumps to loop_receive/3 - proc_lib:hibernate( - ?MODULE, wakeup_from_hibernate, [Parent,Debug,S]), - error( - {should_not_have_arrived_here_but_instead_in, - {wakeup_from_hibernate,3}}). - %% Entry point for wakeup_from_hibernate/3 loop_receive(Parent, Debug, #{timer := Timer} = S) -> receive @@ -754,7 +756,7 @@ loop_receive(Parent, Debug, #{timer := Timer} = S) -> %% but this will stand out in the crash report... ?TERMINATE(exit, Reason, Debug, S, [EXIT]); {timeout,Timer,Content} when Timer =/= undefined -> - loop_receive( + loop_event( Parent, Debug, S, {timeout,Content}, undefined); _ -> Event = @@ -766,11 +768,11 @@ loop_receive(Parent, Debug, #{timer := Timer} = S) -> _ -> {info,Msg} end, - loop_receive(Parent, Debug, S, Event, Timer) + loop_event(Parent, Debug, S, Event, Timer) end end. -loop_receive(Parent, Debug, S, Event, Timer) -> +loop_event(Parent, Debug, S, Event, Timer) -> NewDebug = sys_debug(Debug, S, {in,Event}), %% Here the queue of not yet processed events is created loop_events(Parent, NewDebug, S, [Event], Timer). -- cgit v1.2.3 From e660572b020da58c89149c7f052c7127cc0263cb Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Mon, 29 Feb 2016 11:36:27 +0100 Subject: Remove the remove_event action and all alike Removing events from the internal queues is not necessary with the choosen semantics of the event queue vs. hibernate. In an early implementation it was possible by combining hibernate with e.g. postpone to get an event in the queue that you would not see before processing the postponed event, and therefore should you decide to cancel a timer it was essential to be able to remove that unseen event from the queue. With the choosen semantics you will have to postpone or generate an event for it to be in the event queue, and if you e.g. postpone a timeout event and then cancel the timer it is your mistake. You have seen the event and should know better than to try to cancel the timer. So, the actions: remove_event, cancel_timer, demonitor and unlink are now removed. There have also been some cleanup of the timer handling code. --- lib/stdlib/doc/src/gen_statem.xml | 71 +------- lib/stdlib/src/gen_statem.erl | 326 +++++++++-------------------------- lib/stdlib/test/gen_statem_SUITE.erl | 35 ++-- 3 files changed, 104 insertions(+), 328 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index bda3ef081d..04b80d29ac 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -461,24 +461,6 @@ ok

- - - -

- A fun() of arity 2 that takes an event - and returns a boolean. - When used in the - action() - {remove_event,RemoveEventPredicate}, - the oldest event for which the predicate returns true - will be removed. -

-

- The predicate may not use a throw exception - to return its result. -

-
-
@@ -617,13 +599,11 @@ ok counts just like a new in this respect. If the value is infinity no timer is started. If it is 0 the timeout event - is immediately enqueued as the newest received. + is immediately enqueued as the newest received + (unless there are retried or inserted events to process). Also note that it is not possible nor needed - to cancel this timeout using the - - action() cancel_timer - - since this timeout is cancelled automatically by any other event. + to cancel this timeout since it is cancelled automatically + by any other event.

@@ -702,49 +682,6 @@ ok should be used when you want to reliably distinguish an event inserted this way from any external event.
- remove_event - - Remove the oldest queued event - that matches equal to EventType - and EventContent or for which - EventPredicate - returns true. Note that next_event - and postpone events in the same actions list - does not get into the event queue until after all actions - has been done so you can not remove an event that you insert - with the same actions list. Make up your mind! - - cancel_timer - - Cancel the timer by calling - - erlang:cancel_timer/2 - - with TimerRef, - clean the process message queue from any late timeout message, - and remove any late timeout message - from the gen_statem event queue using - {remove_event,EventPredicate} above. - This is a convenience function that saves quite some - lines of code and testing time over doing it from - the primitives mentioned above. - - demonitor - - Like cancel_timer above but for - - demonitor/2 - - with MonitorRef. - - unlink - - Like cancel_timer above but for - - unlink/1 - - with Id. -
diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 29848d13a3..26f1aede6f 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -66,9 +66,6 @@ {'call',Caller :: caller()} | 'cast' | 'info' | 'timeout' | 'internal'. --type event_predicate() :: % Return true for the event in question - fun((event_type(), term()) -> boolean()). - -type callback_mode() :: 'state_functions' | 'handle_event_function'. -type transition_option() :: @@ -117,19 +114,7 @@ %% action() list is the first to be delivered. {'next_event', % Insert event as the next to handle EventType :: event_type(), - EventContent :: term()} | - %% - {'remove_event', % Remove the oldest matching (=:=) event - EventType :: event_type(), EventContent :: term()} | - {'remove_event', % Remove the oldest event satisfying predicate - EventPredicate :: event_predicate()} | - %% - {'cancel_timer', % Cancel timer and clean up mess(ages) - TimerRef :: reference()} | - {'demonitor', % Demonitor and clean up mess(ages) - MonitorRef :: reference()} | - {'unlink', % Unlink and clean up mess(ages) - Id :: pid() | port()}. + EventContent :: term()}. -type reply_action() :: {'reply', % Reply to a caller Caller :: caller(), Reply :: term()}. @@ -745,11 +730,11 @@ loop_receive(Parent, Debug, #{timer := Timer} = S) -> Msg -> case Msg of {system,Pid,Req} -> + #{hibernate := Hibernate} = S, %% Does not return but tail recursively calls %% system_continue/3 that jumps to loop/3 sys:handle_system_msg( - Req, Pid, Parent, ?MODULE, Debug, S, - maps:get(hibernate, S)); + Req, Pid, Parent, ?MODULE, Debug, S, Hibernate); {'EXIT',Parent,Reason} = EXIT -> %% EXIT is not a 2-tuple and therefore %% not an event and has no event_type(), @@ -757,8 +742,25 @@ loop_receive(Parent, Debug, #{timer := Timer} = S) -> ?TERMINATE(exit, Reason, Debug, S, [EXIT]); {timeout,Timer,Content} when Timer =/= undefined -> loop_event( - Parent, Debug, S, {timeout,Content}, undefined); + Parent, Debug, S, {timeout,Content}); _ -> + %% Cancel Timer if running + case Timer of + undefined -> + ok; + _ -> + case erlang:cancel_timer(Timer) of + TimeLeft when is_integer(TimeLeft) -> + ok; + false -> + receive + {timeout,Timer,_} -> + ok + after 0 -> + ok + end + end + end, Event = case Msg of {'$gen_call',Caller,Request} -> @@ -768,22 +770,20 @@ loop_receive(Parent, Debug, #{timer := Timer} = S) -> _ -> {info,Msg} end, - loop_event(Parent, Debug, S, Event, Timer) + loop_event(Parent, Debug, S, Event) end end. -loop_event(Parent, Debug, S, Event, Timer) -> +loop_event(Parent, Debug, S, Event) -> + %% The timer field in S is now invalid and ignored + %% until we get back to loop/3 NewDebug = sys_debug(Debug, S, {in,Event}), %% Here the queue of not yet processed events is created - loop_events(Parent, NewDebug, S, [Event], Timer). + loop_events(Parent, NewDebug, S, [Event]). -%% Process first event in queue, or if there is none receive a new -%% -%% The loop_event* functions optimize S map handling by dismantling it, -%% passing the parts in arguments to avoid map lookups and construct the -%% new S map in one go on exit. Premature optimization, I know, but -%% there were quite some map lookups repeated in different functions. -loop_events(Parent, Debug, S, [], _Timer) -> +%% Process first the event queue, or if it is empty +%% loop back to receive a new event +loop_events(Parent, Debug, S, []) -> loop(Parent, Debug, S); loop_events( Parent, Debug, @@ -791,9 +791,7 @@ loop_events( module := Module, state := State, data := Data} = S, - [{Type,Content} = Event|Events] = Q, Timer) -> - _ = (Timer =/= undefined) andalso - cancel_timer(Timer), + [{Type,Content} = Event|Events] = Q) -> try case CallbackMode of state_functions -> @@ -841,7 +839,7 @@ loop_events( terminate(Class, Reason, Stacktrace, Debug, S, Q) end. -%% Interpret all callback return value variants +%% Interpret all callback return variants loop_event_result( Parent, Debug, #{callback_mode := CallbackMode, state := State, data := Data} = S, @@ -909,7 +907,6 @@ loop_event_actions( Hibernate = false, Timeout = undefined, NextEvents = [], - P = false, % The postponed list or false if unchanged loop_event_actions( Parent, Debug, S, Events, Event, State, NewState, NewData, if @@ -918,24 +915,25 @@ loop_event_actions( true -> [Actions] end, - Postpone, Hibernate, Timeout, NextEvents, P). + Postpone, Hibernate, Timeout, NextEvents). %% +%% Process all action()s loop_event_actions( Parent, Debug, S, Events, Event, State, NewState, NewData, [Action|Actions], - Postpone, Hibernate, Timeout, NextEvents, P) -> + Postpone, Hibernate, Timeout, NextEvents) -> case Action of - %% Set options + %% Actions that set options postpone -> loop_event_actions( Parent, Debug, S, Events, Event, State, NewState, NewData, Actions, - true, Hibernate, Timeout, NextEvents, P); + true, Hibernate, Timeout, NextEvents); {postpone,NewPostpone} when is_boolean(NewPostpone) -> loop_event_actions( Parent, Debug, S, Events, Event, State, NewState, NewData, Actions, - NewPostpone, Hibernate, Timeout, NextEvents, P); + NewPostpone, Hibernate, Timeout, NextEvents); {postpone,_} -> ?TERMINATE( error, {bad_action,Action}, Debug, S, [Event|Events]); @@ -943,12 +941,12 @@ loop_event_actions( loop_event_actions( Parent, Debug, S, Events, Event, State, NewState, NewData, Actions, - Postpone, true, Timeout, NextEvents, P); + Postpone, true, Timeout, NextEvents); {hibernate,NewHibernate} when is_boolean(NewHibernate) -> loop_event_actions( Parent, Debug, S, Events, Event, State, NewState, NewData, Actions, - Postpone, NewHibernate, Timeout, NextEvents, P); + Postpone, NewHibernate, Timeout, NextEvents); {hibernate,_} -> ?TERMINATE( error, {bad_action,Action}, Debug, S, [Event|Events]); @@ -956,12 +954,12 @@ loop_event_actions( loop_event_actions( Parent, Debug, S, Events, Event, State, NewState, NewData, Actions, - Postpone, Hibernate, undefined, NextEvents, P); + Postpone, Hibernate, undefined, NextEvents); {timeout,Time,_} = NewTimeout when is_integer(Time), Time >= 0 -> loop_event_actions( Parent, Debug, S, Events, Event, State, NewState, NewData, Actions, - Postpone, Hibernate, NewTimeout, NextEvents, P); + Postpone, Hibernate, NewTimeout, NextEvents); {timeout,_,_} -> ?TERMINATE( error, {bad_action,Action}, Debug, S, [Event|Events]); @@ -973,7 +971,7 @@ loop_event_actions( loop_event_actions( Parent, NewDebug, S, Events, Event, State, NewState, NewData, Actions, - Postpone, Hibernate, Timeout, NextEvents, P); + Postpone, Hibernate, Timeout, NextEvents); false -> ?TERMINATE( error, {bad_action,Action}, Debug, S, [Event|Events]) @@ -985,67 +983,25 @@ loop_event_actions( Parent, Debug, S, Events, Event, State, NewState, NewData, Actions, Postpone, Hibernate, Timeout, - [{Type,Content}|NextEvents], P); + [{Type,Content}|NextEvents]); false -> ?TERMINATE( error, {bad_action,Action}, Debug, S, [Event|Events]) end; _ -> - %% All others are remove actions - case remove_fun(Action) of - false -> - loop_event_actions( - Parent, Debug, S, Events, Event, - State, NewState, NewData, Actions, - Postpone, Hibernate, Timeout, NextEvents, P); - undefined -> - ?TERMINATE( - error, {bad_action,Action}, Debug, S, [Event|Events]); - RemoveFun when is_function(RemoveFun, 2) -> - P0 = - case P of - false -> - maps:get(postponed, S); - _ -> - P - end, - case remove_event(RemoveFun, Events, P0) of - false -> - loop_event_actions( - Parent, Debug, S, Events, Event, - State, NewState, NewData, Actions, - Postpone, Hibernate, Timeout, NextEvents, P); - {NewEvents,false} -> - loop_event_actions( - Parent, Debug, S, NewEvents, Event, - State, NewState, NewData, Actions, - Postpone, Hibernate, Timeout, NextEvents, P); - {false,NewP} -> - loop_event_actions( - Parent, Debug, S, Events, Event, - State, NewState, NewData, Actions, - Postpone, Hibernate, Timeout, NextEvents, - NewP); - [Class,Reason,Stacktrace] -> - terminate( - Class, Reason, Stacktrace, - Debug, S, [Event|Events]) - end; - [Class,Reason,Stacktrace] -> - terminate( - Class, Reason, Stacktrace, Debug, S, [Event|Events]) - end + ?TERMINATE( + error, {bad_action,Action}, Debug, S, [Event|Events]) end; +%% +%% End of actions list loop_event_actions( - Parent, Debug, S, Events, Event, State, NewState, NewData, [], - Postpone, Hibernate, Timeout, NextEvents, P) -> - P0 = - case P of - false -> - maps:get(postponed, S); - _ -> - P - end, + Parent, Debug, #{postponed := P0} = S, Events, Event, + State, NewState, NewData, [], + Postpone, Hibernate, Timeout, NextEvents) -> + %% + %% All options have been collected and next_events are buffered. + %% Do the actual state transition. + %% P1 = % Move current event to postponed if Postpone case Postpone of true -> @@ -1053,23 +1009,12 @@ loop_event_actions( false -> P0 end, - {Timer,Q1} = - case Timeout of - undefined -> - {undefined,Events}; - {timeout,0,Msg} -> - %% Pretend the timeout has just been received - {undefined,Events ++ [{timeout,Msg}]}; - {timeout,Time,Msg} -> - {erlang:start_timer(Time, self(), Msg), - Events} - end, - {Q2,P2} = % Move all postponed events to queue if state change + {Q2,P} = % Move all postponed events to queue if state change if NewState =:= State -> - {Q1,P1}; + {Events,P1}; true -> - {lists:reverse(P1, Q1),[]} + {lists:reverse(P1, Events),[]} end, %% Place next events first in queue Q3 = lists:reverse(NextEvents, Q2), @@ -1083,16 +1028,41 @@ loop_event_actions( false -> {consume,Event,NewState} end), - %% Loop to top; process next event + %% Have a peek on the event queue so we can avoid starting + %% the state timer unless we have to + {Q,Timer} = + case Timeout of + undefined -> + %% No state timeout has been requested + {Q3,undefined}; + {timeout,Time,Msg} -> + %% A state timeout has been requested + case Q3 of + [] when Time =:= 0 -> + %% Immediate timeout - simulate it + %% so we do not get the timeout message + %% after any received event + {[{timeout,Msg}],undefined}; + [] -> + %% Actually start a timer + {Q3,erlang:start_timer(Time, self(), Msg)}; + _ -> + %% Do not start a timer since any queued + %% event cancels the state timer so we pretend + %% that the timer has been started and cancelled + {Q3,undefined} + end + end, + %% Loop to top of event queue loop; process next event loop_events( Parent, NewDebug, S#{ state := NewState, data := NewData, timer := Timer, - hibernate := Hibernate, - postponed := P2}, - Q3, Timer). + postponed := P, + hibernate := Hibernate}, + Q). %%--------------------------------------------------------------------------- %% Server helpers @@ -1125,103 +1095,6 @@ do_reply(Debug, S, Caller, Reply) -> sys_debug(Debug, S, {out,Reply,Caller}). -%% Remove oldest matching event from the queue(s) -remove_event(RemoveFun, Q, P) -> - try - case remove_tail_event(RemoveFun, P) of - false -> - case remove_head_event(RemoveFun, Q) of - false -> - false; - NewQ -> - {false,NewQ} - end; - NewP -> - {NewP,false} - end - catch - Class:Reason -> - [Class,Reason,erlang:get_stacktrace()] - end. - -%% Do the given action and create an event removal predicate fun() -remove_fun({remove_event,Type,Content}) -> - fun (T, C) when T =:= Type, C =:= Content -> true; - (_, _) -> false - end; -remove_fun({remove_event,RemoveFun}) when is_function(RemoveFun, 2) -> - RemoveFun; -remove_fun({cancel_timer,TimerRef}) -> - try cancel_timer(TimerRef) of - false -> - false; - true -> - fun - (info, {timeout,TRef,_}) - when TRef =:= TimerRef -> - true; - (_, _) -> - false - end - catch - Class:Reason -> - [Class,Reason,erlang:get_stacktrace()] - end; -remove_fun({demonitor,MonitorRef}) -> - try erlang:demonitor(MonitorRef, [flush,info]) of - false -> - false; - true -> - fun (info, {'DOWN',MRef,_,_,_}) - when MRef =:= MonitorRef-> - true; - (_, _) -> - false - end - catch - Class:Reason -> - [Class,Reason,erlang:get_stacktrace()] - end; -remove_fun({unlink,Id}) -> - try unlink(Id) of - true -> - receive - {'EXIT',Id,_} -> - ok - after 0 -> - ok - end, - fun (info, {'EXIT',I,_}) - when I =:= Id -> - true; - (_, _) -> - false - end - catch - Class:Reason -> - [Class,Reason,erlang:get_stacktrace()] - end; -remove_fun(_) -> - undefined. - - -%% Cancel a timer and clense the process mailbox returning -%% false if no such timer message can arrive after this or -%% true otherwise -cancel_timer(TimerRef) -> - case erlang:cancel_timer(TimerRef) of - TimeLeft when is_integer(TimeLeft) -> - false; - false -> - receive - {timeout,TimerRef,_} -> - false - after 0 -> - true - end - end. - - terminate( Class, Reason, Stacktrace, Debug, #{module := Module, @@ -1350,30 +1223,3 @@ format_status_default(Opt, State, Data) -> _ -> [{data,[{"State",SSD}]}] end. - -%%--------------------------------------------------------------------------- -%% Farily general helpers - -%% Return the modified list where the first element that satisfies -%% the RemoveFun predicate is removed, or false if no such element exists. -remove_head_event(_RemoveFun, []) -> - false; -remove_head_event(RemoveFun, [{Tag,Content}|Events]) -> - case RemoveFun(Tag, Content) of - false -> - remove_head_event(RemoveFun, Events); - true -> - Events - end. - -%% Return the modified list where the last element that satisfies -%% the RemoveFun predicate is removed, or false if no such element exists. -remove_tail_event(_RemoveFun, []) -> - false; -remove_tail_event(RemoveFun, [{Tag,Content} = Event|Events]) -> - case remove_tail_event(RemoveFun, Events) of - false -> - RemoveFun(Tag, Content) andalso Events; - NewEvents -> - [Event|NewEvents] - end. diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 268b45a0e7..38aab752b8 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -1210,29 +1210,22 @@ idle(Type, Content, Data) -> end. timeout(timeout, idle, {From,Time}) -> - TRef2 = erlang:start_timer(Time, self(), ok), - TRefC1 = erlang:start_timer(Time, self(), cancel1), - TRefC2 = erlang:start_timer(Time, self(), cancel2), - {next_state,timeout2,{From,Time,TRef2}, - [{cancel_timer, TRefC1}, - {next_event,internal,{cancel_timer,TRefC2}}]}; -timeout(_, _, Data) -> - {keep_state,Data}. - -timeout2( - internal, {cancel_timer,TRefC2}, {From,Time,TRef2}) -> - Time4 = Time * 4, - receive after Time4 -> ok end, - {next_state,timeout3,{From,TRef2}, - [{cancel_timer,TRefC2}]}; -timeout2(_, _, Data) -> - {keep_state,Data}. - -timeout3(info, {timeout,TRef2,Result}, {From,TRef2}) -> + TRef = erlang:start_timer(Time, self(), ok), + {next_state,timeout2,{From,TRef}, + [{timeout,1,should_be_cancelled}, + postpone]}; % Should cancel state timeout +timeout(_, _, _) -> + keep_state_and_data. + +timeout2(timeout, idle, _) -> + keep_state_and_data; +timeout2(timeout, Reason, _) -> + {stop,Reason}; +timeout2(info, {timeout,TRef,Result}, {From,TRef}) -> gen_statem:reply([{reply,From,Result}]), {next_state,idle,state}; -timeout3(_, _, Data) -> - {keep_state,Data}. +timeout2(_, _, _) -> + {keep_state_and_data,[]}. wfor_conf({call,From}, confirm, Data) -> {next_state,connected,Data, -- cgit v1.2.3 From 79d75b981274f6841e1d4c09c125f92e1731ab4b Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Mon, 29 Feb 2016 16:10:00 +0100 Subject: Sharpen test suite --- lib/stdlib/src/gen_statem.erl | 2 +- lib/stdlib/test/gen_statem_SUITE.erl | 141 ++++++++++++++++++++++++++++++++++- 2 files changed, 139 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 26f1aede6f..17d361efc2 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -1169,7 +1169,7 @@ error_info( "** Reason for termination = ~w:~p~n" ++ "** State = ~p~n" ++ "** Callback mode = ~p~n" ++ - "** Queued/Posponed = ~w/~w~n" ++ + "** Queued/Postponed = ~w/~w~n" ++ case FixedStacktrace of [] -> ""; diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 38aab752b8..e62255035d 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -37,7 +37,7 @@ all() -> {group, stop_handle_event}, {group, abnormal}, {group, abnormal_handle_event}, - shutdown, + shutdown, stop_and_reply, postpone_and_next_event, {group, sys}, hibernate, enter_loop]. @@ -495,6 +495,113 @@ shutdown(Config) -> +stop_and_reply(_Config) -> + process_flag(trap_exit, true), + + Machine = + %% Abusing the internal format of From... + #{init => + fun (cast, {echo,From1,Reply1}, _) -> + {next_state,wait,{reply,From1,Reply1}} + end, + wait => + fun (cast, {stop_and_reply,Reason,From2,Reply2},R1) -> + {stop_and_reply,Reason, + [R1,{reply,From2,Reply2}]} + end}, + {ok,STM} = + gen_statem:start_link( + ?MODULE, + {map_statem,Machine,init,undefined,[]}, + []), + + Self = self(), + Tag1 = make_ref(), + gen_statem:cast(STM, {echo,{Self,Tag1},reply1}), + Tag2 = make_ref(), + gen_statem:cast(STM, {stop_and_reply,reason,{Self,Tag2},reply2}), + case flush() of + [{Tag1,reply1},{Tag2,reply2},{'EXIT',STM,reason}] -> + ok; + Other1 -> + ct:fail({unexpected,Other1}) + end, + + {noproc,_} = + ?EXPECT_FAILURE(gen_statem:call(STM, hej), Reason), + case flush() of + [] -> + ok; + Other2 -> + ct:fail({unexpected,Other2}) + end. + + + +postpone_and_next_event(_Config) -> + process_flag(trap_exit, true), + + Machine = + %% Abusing the internal format of From... + #{init => + fun (cast, _, _) -> + {keep_state_and_data,postpone}; + ({call,From}, {buffer,Pid,[Tag3,Tag4]}, _) -> + {next_state,buffer,[], + [{next_event,internal,{reply,{Pid,Tag3},ok3}}, + {next_event,internal,{reply,{Pid,Tag4},ok4}}, + {reply,From,ok}]} + end, + buffer => + fun (internal, Reply, Replies) -> + {keep_state,[Reply|Replies]}; + (cast, Reply, Replies) -> + {keep_state,[Reply|Replies]}; + ({call,From}, {stop,Reason}, Replies) -> + {next_state,stop,Replies, + lists:reverse( + Replies, + [{reply,From,ok}, + {next_event,internal,{stop,Reason}}])} + end, + stop => + fun (internal, Result, _) -> + Result + end}, + + {ok,STM} = + gen_statem:start_link( + ?MODULE, + {map_statem,Machine,init,undefined,[]}, + []), + Self = self(), + Tag1 = make_ref(), + gen_statem:cast(STM, {reply,{Self,Tag1},ok1}), + Tag2 = make_ref(), + gen_statem:cast(STM, {reply,{Self,Tag2},ok2}), + Tag3 = make_ref(), + Tag4 = make_ref(), + ok = gen_statem:call(STM, {buffer,Self,[Tag3,Tag4]}), + ok = gen_statem:call(STM, {stop,reason}), + case flush() of + [{Tag3,ok3},{Tag4,ok4},{Tag1,ok1},{Tag2,ok2}, + {'EXIT',STM,reason}] -> + ok; + Other1 -> + ct:fail({unexpected,Other1}) + end, + + {noproc,_} = + ?EXPECT_FAILURE(gen_statem:call(STM, hej), Reason), + case flush() of + [] -> + ok; + Other2 -> + ct:fail({unexpected,Other2}) + end. + + + sys1(Config) -> {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), {status, Pid, {module,gen_statem}, _} = sys:get_status(Pid), @@ -1115,7 +1222,6 @@ do_sync_disconnect(STM) -> verify_empty_msgq() -> - receive after 500 -> ok end, [] = flush(), ok. @@ -1157,6 +1263,8 @@ init({callback_mode,CallbackMode,Arg}) -> Other -> Other end; +init({map_statem,Machine,State,Data,Ops}) when is_map(Machine) -> + {handle_event_function,State,[Data|Machine],Ops}; init([]) -> {state_functions,idle,data}. @@ -1367,6 +1475,33 @@ handle_common_events(cast, {'alive?',Pid}, _, Data) -> handle_common_events(_, _, _, _) -> undefined. +%% Wrapper state machine that uses a map state machine spec +handle_event( + Type, Event, State, [Data|Machine]) + when is_map(Machine) -> + #{State := HandleEvent} = Machine, + case + try HandleEvent(Type, Event, Data) of + Result -> + Result + catch + Result -> + Result + end of + {stop,Reason,NewData} -> + {stop,Reason,[NewData|Machine]}; + {next_state,NewState,NewData} -> + {next_state,NewState,[NewData|Machine]}; + {next_state,NewState,NewData,Ops} -> + {next_state,NewState,[NewData|Machine],Ops}; + {keep_state,NewData} -> + {keep_state,[NewData|Machine]}; + {keep_state,NewData,Ops} -> + {keep_state,[NewData|Machine],Ops}; + Other -> + Other + end; +%% %% Dispatcher to test callback_mode handle_event_function %% %% Wrap the state in a 1 element list just to test non-atom @@ -1412,6 +1547,6 @@ flush() -> receive Msg -> [Msg|flush()] - after 0 -> + after 500 -> [] end. -- cgit v1.2.3 From a0c69303c971b4c65e5cd450dbe7de680aa58181 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 1 Mar 2016 09:08:30 +0100 Subject: Fixup sharpened test suite --- lib/stdlib/test/gen_statem_SUITE.erl | 55 +++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 20 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index e62255035d..f605f0c7b9 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -37,7 +37,7 @@ all() -> {group, stop_handle_event}, {group, abnormal}, {group, abnormal_handle_event}, - shutdown, stop_and_reply, postpone_and_next_event, + shutdown, stop_and_reply, event_order, {group, sys}, hibernate, enter_loop]. @@ -501,7 +501,11 @@ stop_and_reply(_Config) -> Machine = %% Abusing the internal format of From... #{init => - fun (cast, {echo,From1,Reply1}, _) -> + fun () -> + {ok,start,undefined} + end, + start => + fun (cast, {echo,From1,Reply1}, undefined) -> {next_state,wait,{reply,From1,Reply1}} end, wait => @@ -509,11 +513,7 @@ stop_and_reply(_Config) -> {stop_and_reply,Reason, [R1,{reply,From2,Reply2}]} end}, - {ok,STM} = - gen_statem:start_link( - ?MODULE, - {map_statem,Machine,init,undefined,[]}, - []), + {ok,STM} = gen_statem:start_link(?MODULE, {map_statem,Machine}, []), Self = self(), Tag1 = make_ref(), @@ -538,42 +538,49 @@ stop_and_reply(_Config) -> -postpone_and_next_event(_Config) -> +event_order(_Config) -> process_flag(trap_exit, true), Machine = %% Abusing the internal format of From... #{init => + fun () -> + {ok,start,undefined} + end, + start => fun (cast, _, _) -> - {keep_state_and_data,postpone}; - ({call,From}, {buffer,Pid,[Tag3,Tag4]}, _) -> + {keep_state_and_data,postpone}; %% Handled in 'buffer' + ({call,From}, {buffer,Pid,[Tag3,Tag4,Tag5]}, + undefined) -> {next_state,buffer,[], [{next_event,internal,{reply,{Pid,Tag3},ok3}}, {next_event,internal,{reply,{Pid,Tag4},ok4}}, + {timeout,0,{reply,{Pid,Tag5},ok5}}, + %% The timeout should not happen since there + %% are events that cancel it i.e next_event + %% and postponed {reply,From,ok}]} end, buffer => fun (internal, Reply, Replies) -> {keep_state,[Reply|Replies]}; + (timeout, Reply, Replies) -> + {keep_state,[Reply|Replies]}; (cast, Reply, Replies) -> {keep_state,[Reply|Replies]}; ({call,From}, {stop,Reason}, Replies) -> - {next_state,stop,Replies, + {next_state,stop,undefined, lists:reverse( Replies, [{reply,From,ok}, {next_event,internal,{stop,Reason}}])} end, stop => - fun (internal, Result, _) -> + fun (internal, Result, undefined) -> Result end}, - {ok,STM} = - gen_statem:start_link( - ?MODULE, - {map_statem,Machine,init,undefined,[]}, - []), + {ok,STM} = gen_statem:start_link(?MODULE, {map_statem,Machine}, []), Self = self(), Tag1 = make_ref(), gen_statem:cast(STM, {reply,{Self,Tag1},ok1}), @@ -581,7 +588,8 @@ postpone_and_next_event(_Config) -> gen_statem:cast(STM, {reply,{Self,Tag2},ok2}), Tag3 = make_ref(), Tag4 = make_ref(), - ok = gen_statem:call(STM, {buffer,Self,[Tag3,Tag4]}), + Tag5 = make_ref(), + ok = gen_statem:call(STM, {buffer,Self,[Tag3,Tag4,Tag5]}), ok = gen_statem:call(STM, {stop,reason}), case flush() of [{Tag3,ok3},{Tag4,ok4},{Tag1,ok1},{Tag2,ok2}, @@ -1263,8 +1271,15 @@ init({callback_mode,CallbackMode,Arg}) -> Other -> Other end; -init({map_statem,Machine,State,Data,Ops}) when is_map(Machine) -> - {handle_event_function,State,[Data|Machine],Ops}; +init({map_statem,#{init := Init}=Machine}) -> + case Init() of + {ok,State,Data,Ops} -> + {handle_event_function,State,[Data|Machine],Ops}; + {ok,State,Data} -> + {handle_event_function,State,[Data|Machine]}; + Other -> + Other + end; init([]) -> {state_functions,idle,data}. -- cgit v1.2.3 From 9dfb4e6cf574870ac6e6a5a2a507c989c64e7525 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 1 Mar 2016 08:31:58 +0100 Subject: Change code_change/4 to {ok,State,Data} --- lib/stdlib/doc/src/gen_statem.xml | 4 ++-- lib/stdlib/src/gen_statem.erl | 4 ++-- lib/stdlib/test/gen_statem_SUITE.erl | 15 ++++++++++++--- 3 files changed, 16 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 04b80d29ac..01be3099c1 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -241,7 +241,7 @@ stop() -> terminate(_Reason, _State, _Data) -> void. code_change(_Vsn, State, Data, _Extra) -> - {ok,{State,Data}}. + {ok,State,Data}. init([]) -> %% Set the callback mode and initial state + data. %% Data is used only as a counter. @@ -1359,7 +1359,7 @@ ok   Vsn = term() OldState = NewState = term() Extra = term() - Result = {ok,{NewState,NewData}} | Reason + Result = {ok,NewState,NewData} | Reason OldState = NewState = state() diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 17d361efc2..cc09efc140 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -200,7 +200,7 @@ OldState :: state(), OldData :: data(), Extra :: term()) -> - {ok, {NewState :: state(), NewData :: data()}}. + {ok, NewState :: state(), NewData :: data()}. %% Format the callback module state in some sensible that is %% often condensed way. For StatusOption =:= 'normal' the perferred @@ -619,7 +619,7 @@ system_code_change( Result -> Result end of - {ok,{NewState,NewData}} -> + {ok,NewState,NewData} -> {ok, S#{ state := NewState, diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index f605f0c7b9..573a64bbb7 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -55,7 +55,7 @@ groups() -> {abnormal, [], [abnormal1, abnormal2]}, {abnormal_handle_event, [], [abnormal1, abnormal2]}, {sys, [], - [sys1, + [sys1, code_change, call_format_status, error_format_status, terminate_crash_format, get_state, replace_state]}, @@ -633,6 +633,15 @@ sys1(Config) -> sys:resume(Pid), stop_it(Pid). +code_change(Config) -> + {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), + {idle,data} = sys:get_state(Pid), + sys:suspend(Pid), + sys:change_code(Pid, ?MODULE, old_vsn, extra), + sys:resume(Pid), + {idle,{old_vsn,data,extra}} = sys:get_state(Pid), + stop_it(Pid). + call_format_status(Config) -> {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), Status = sys:get_status(Pid), @@ -1550,8 +1559,8 @@ wrap_result(Result) -> -code_change(_OldVsn, State, Data, _Extra) -> - {ok,State,Data}. +code_change(OldVsn, State, Data, Extra) -> + {ok,State,{OldVsn,Data,Extra}}. format_status(terminate, [_Pdict,State,Data]) -> {formatted,State,Data}; -- cgit v1.2.3 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 --- lib/sasl/doc/src/appup.xml | 5 +- lib/sasl/doc/src/error_logging.xml | 7 ++- lib/stdlib/doc/src/gen_statem.xml | 87 ++++++++++++++++++++------- lib/stdlib/doc/src/proc_lib.xml | 8 +-- lib/stdlib/doc/src/supervisor.xml | 10 ++-- lib/stdlib/doc/src/sys.xml | 31 ++++++---- lib/stdlib/src/gen_statem.erl | 93 ++++++++++++++++------------ lib/tools/doc/src/erlang_mode.xml | 3 +- lib/tools/emacs/erlang-skels.el | 120 ++++++++++++++++++++++++++++++++++++- 9 files changed, 281 insertions(+), 83 deletions(-) (limited to 'lib') diff --git a/lib/sasl/doc/src/appup.xml b/lib/sasl/doc/src/appup.xml index b54d2adb19..6fbdcb9f5b 100644 --- a/lib/sasl/doc/src/appup.xml +++ b/lib/sasl/doc/src/appup.xml @@ -4,7 +4,7 @@
- 19972014 + 19972016 Ericsson AB. All Rights Reserved. @@ -137,7 +137,8 @@ code change. If it is set to {advanced,Extra}, implemented processes using gen_server, - gen_fsm, or + gen_fsm, + gen_statem, or gen_event transform their internal state by calling the callback function code_change. Special processes call the callback diff --git a/lib/sasl/doc/src/error_logging.xml b/lib/sasl/doc/src/error_logging.xml index 46b12f3872..8464a41ff9 100644 --- a/lib/sasl/doc/src/error_logging.xml +++ b/lib/sasl/doc/src/error_logging.xml @@ -4,7 +4,7 @@
- 19972013 + 19972016 Ericsson AB. All Rights Reserved. @@ -90,8 +90,9 @@ a process terminates with an unexpected reason, which is any reason other than normal, shutdown, or {shutdown,Term}. Processes using behaviors - gen_server or - gen_fsm + gen_server, + gen_fsm or + gen_statem are examples of such processes. A crash report contains the following items:

Crasher diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 01be3099c1..db6a4e03ea 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -407,7 +407,7 @@ ok

- After a state change (NewState =/= State) + After a state change (NextState =/= State) all postponed events are retried.

@@ -566,7 +566,7 @@ ok

If true postpone the current event and retry it when the state changes that is: - NewState =/= State. + NextState =/= State.

@@ -701,7 +701,47 @@ ok - + + + + next_state + + The gen_statem will do a state transition to + NextStateName + (which may be the same as the current state), + set NewData + and execute all Actions + + +

+ All these terms are tuples or atoms and this property + will hold in any future version of gen_statem, + just in case you need such a promise. +

+
+
+ + + + + next_state + + The gen_statem will do a state transition to + NextState + (which may be the same as the current state), + set NewData + and execute all Actions + + +

+ All these terms are tuples or atoms and this property + will hold in any future version of gen_statem, + just in case you need such a promise. +

+
+
+ + stop @@ -723,14 +763,6 @@ ok with Reason and NewData, if given. - next_state - - The gen_statem will do a state transition to - NewState - (which may be the same as the current state), - set NewData - and execute all Actions - keep_state The gen_statem will keep the current state, or @@ -1209,10 +1241,11 @@ ok - Module:StateName(EventType, EventContent, Data) -> Result + Module:StateName(EventType, EventContent, Data) -> + StateFunctionResult Module:handle_event(EventType, EventContent, - State, Data) -> Result + State, Data) -> HandleEventResult Handle an event @@ -1222,7 +1255,7 @@ ok EventContent = term() - State = NewState = + State = state() @@ -1230,9 +1263,15 @@ ok data() - Result = - - state_callback_result() + StateFunctionResult = + + state_function_result() + + + + HandleEventResult = + + handle_event_result() @@ -1260,9 +1299,15 @@ ok reply(Caller, Reply).

- If this function returns with a new state that + If this function returns with a next state that does not match equal (=/=) to the current state - all postponed events will be retried in the new state. + all postponed events will be retried in the next state. +

+

+ The only difference between StateFunctionResult and + HandleEventResult is that for StateFunctionResult + the next state has to be an atom but for HandleEventResult + there is no restriction on the next state.

See action() @@ -1272,7 +1317,7 @@ ok

These functions may use throw, - to return Result. + to return the result.

@@ -1401,7 +1446,7 @@ ok

If successful, the function shall return the updated internal state in an - {ok,{NewState,NewData}} tuple. + {ok,NewState,NewData} tuple.

If the function returns Reason, the ongoing diff --git a/lib/stdlib/doc/src/proc_lib.xml b/lib/stdlib/doc/src/proc_lib.xml index 85f0c0c908..245580b1ba 100644 --- a/lib/stdlib/doc/src/proc_lib.xml +++ b/lib/stdlib/doc/src/proc_lib.xml @@ -4,7 +4,7 @@

- 19962014 + 19962016 Ericsson AB. All Rights Reserved. @@ -34,9 +34,9 @@

This module is used to start processes adhering to the OTP Design Principles. Specifically, the functions in this module are used by the OTP standard behaviors (gen_server, - gen_fsm, ...) when starting new processes. The functions - can also be used to start special processes, user - defined processes which comply to the OTP design principles. See + gen_fsm, gen_statem, ...) when starting new processes. + The functions can also be used to start special processes, + user defined processes which comply to the OTP design principles. See Sys and Proc_Lib in OTP Design Principles for an example.

Some useful information is initialized when a process starts. The registered names, or the process identifiers, of the parent diff --git a/lib/stdlib/doc/src/supervisor.xml b/lib/stdlib/doc/src/supervisor.xml index 24ff251ce3..9d81fb0db7 100644 --- a/lib/stdlib/doc/src/supervisor.xml +++ b/lib/stdlib/doc/src/supervisor.xml @@ -4,7 +4,7 @@

- 19962014 + 19962016 Ericsson AB. All Rights Reserved. @@ -34,8 +34,8 @@

A behaviour module for implementing a supervisor, a process which supervises other processes called child processes. A child process can either be another supervisor or a worker process. - Worker processes are normally implemented using one of - the gen_event, gen_fsm, or gen_server + Worker processes are normally implemented using one of the + gen_event, gen_fsm, gen_statem or gen_server behaviours. A supervisor implemented using this module will have a standard set of interface functions and include functionality for tracing and error reporting. Supervisors are used to build a @@ -221,7 +221,8 @@

modules is used by the release handler during code replacement to determine which processes are using a certain module. As a rule of thumb, if the child process is a - supervisor, gen_server, or gen_fsm, + supervisor, gen_server, + gen_fsm or gen_statem this should be a list with one element [Module], where Module is the callback module. If the child process is an event manager (gen_event) with a @@ -633,6 +634,7 @@ SEE ALSO

gen_event(3), gen_fsm(3), + gen_statem(3), gen_server(3), sys(3)

diff --git a/lib/stdlib/doc/src/sys.xml b/lib/stdlib/doc/src/sys.xml index d400f72e1d..2255395f46 100644 --- a/lib/stdlib/doc/src/sys.xml +++ b/lib/stdlib/doc/src/sys.xml @@ -4,7 +4,7 @@
- 19962014 + 19962016 Ericsson AB. All Rights Reserved. @@ -217,14 +217,18 @@ processes. For example, a gen_server process returns the callback module's state, a gen_fsm process returns information such as its current state name and state data, - and a gen_event process returns information about each of its + a gen_statem process returns information about + its current state and data, and a gen_event process + returns information about each of its registered handlers. Callback modules for gen_server, - gen_fsm, and gen_event can also customise the value + gen_fsm, gen_statem and gen_event + can also customise the value of Misc by exporting a format_status/2 function that contributes module-specific information; - see gen_server:format_status/2, - gen_fsm:format_status/2, and - gen_event:format_status/2 + see gen_server format_status/2, + gen_fsm format_status/2, + gen_statem format_status/2, and + gen_event format_status/2 for more details.

@@ -245,6 +249,8 @@ processes. For a gen_server process, the returned State is simply the callback module's state. For a gen_fsm process, State is the tuple {CurrentStateName, CurrentStateData}. + For a gen_statem process State is + the tuple {CurrentState,CurrentData}. For a gen_event process, State a list of tuples, where each tuple corresponds to an event handler registered in the process and contains {Module, Id, HandlerState}, where Module is the event handler's module name, @@ -263,8 +269,9 @@ details of the exception.

The system_get_state/1 function is primarily useful for user-defined behaviours and modules that implement OTP special - processes. The gen_server, gen_fsm, and gen_event OTP - behaviour modules export this function, and so callback modules for those behaviours + processes. The gen_server, gen_fsm, + gen_statem and gen_event OTP + behaviour modules export this function, so callback modules for those behaviours need not supply their own.

To obtain more information about a process, including its state, see get_status/1 and @@ -290,6 +297,8 @@ gen_fsm process, State is the tuple {CurrentStateName, CurrentStateData}, and NewState is a similar tuple that may contain a new state name, new state data, or both. + The same applies for a gen_statem process but + it names the tuple fields {CurrentState,CurrentData}. For a gen_event process, State is the tuple {Module, Id, HandlerState} where Module is the event handler's module name, Id is the handler's ID (which is the value false if it was registered without @@ -304,7 +313,8 @@ state, then regardless of process type, it may simply return its State argument.

If a StateFun function crashes or throws an exception, then - for gen_server and gen_fsm processes, the original state of the process is + for gen_server, gen_fsm or gen_statem processes, + the original state of the process is unchanged. For gen_event processes, a crashing or failing StateFun function means that only the state of the particular event handler it was working on when it failed or crashed is unchanged; it can still succeed in changing the states of other event @@ -329,7 +339,8 @@ {callback_failed, StateFun, {Class, Reason}}.

The system_replace_state/2 function is primarily useful for user-defined behaviours and modules that implement OTP special processes. The - gen_server, gen_fsm, and gen_event OTP behaviour modules export this function, + gen_server, gen_fsm, gen_statem and + gen_event OTP behaviour modules export this function, and so callback modules for those behaviours need not supply their own.

diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index cc09efc140..438c366f8e 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -44,8 +44,16 @@ -export( [wakeup_from_hibernate/3]). +%% Type exports for templates +-export_type( + [event_type/0, + callback_mode/0, + state_function_result/0, + handle_event_result/0, + action/0]). + %% Fix problem for doc build --export_type([transition_option/0,state_callback_result/0]). +-export_type([transition_option/0]). %%%========================================================================== %%% Interface functions. @@ -84,7 +92,7 @@ -type action() :: %% During a state change: - %% * NewState and NewData are set. + %% * NextState and NewData are set. %% * All action()s are executed in order of apperance. %% * Postponing the current event is performed %% iff 'postpone' is 'true'. @@ -119,7 +127,25 @@ {'reply', % Reply to a caller Caller :: caller(), Reply :: term()}. --type state_callback_result() :: +-type state_function_result() :: + {'next_state', % {next_state,NextStateName,NewData,[]} + NextStateName :: state_name(), + NewData :: data()} | + {'next_state', % State transition, maybe to the same state + NextStateName :: state_name(), + NewData :: data(), + Actions :: [action()] | action()} | + common_state_callback_result(). +-type handle_event_result() :: + {'next_state', % {next_state,NextState,NewData,[]} + NextState :: state(), + NewData :: data()} | + {'next_state', % State transition, maybe to the same state + NextState :: state(), + NewData :: data(), + Actions :: [action()] | action()} | + common_state_callback_result(). +-type common_state_callback_result() :: 'stop' | % {stop,normal} {'stop', % Stop the server Reason :: term()} | @@ -133,13 +159,6 @@ Reason :: term(), Replies :: [reply_action()] | reply_action(), NewData :: data()} | - {'next_state', % {next_state,NewState,NewData,[]} - NewState :: state(), - NewData :: data()} | - {'next_state', % State transition, maybe to the same state - NewState :: state(), - NewData :: data(), - Actions :: [action()] | action()} | {'keep_state', % {keep_state,NewData,[]} NewData :: data()} | {'keep_state', % Keep state, change data @@ -171,7 +190,7 @@ event_type(), EventContent :: term(), Data :: data()) -> - state_callback_result(). + state_function_result(). %% %% State callback for callback_mode() =:= handle_event_function. %% @@ -182,7 +201,7 @@ EventContent :: term(), State :: state(), % Current state Data :: data()) -> - state_callback_result(). + handle_event_result(). %% Clean up before the server terminates. -callback terminate( @@ -514,7 +533,7 @@ enter(Module, Opts, CallbackMode, State, Data, Server, Actions, Parent) -> %% The values should already have been type checked Name = gen:get_proc_name(Server), Debug = gen:debug_options(Name, Opts), - OldState = make_ref(), % Will be discarded by loop_event_actions/9 + PrevState = make_ref(), % Will be discarded by loop_event_actions/9 NewActions = if is_list(Actions) -> @@ -526,7 +545,7 @@ enter(Module, Opts, CallbackMode, State, Data, Server, Actions, Parent) -> callback_mode => CallbackMode, module => Module, name => Name, - state => OldState, + state => PrevState, data => Data, timer => undefined, postponed => [], @@ -534,7 +553,7 @@ enter(Module, Opts, CallbackMode, State, Data, Server, Actions, Parent) -> loop_event_actions( Parent, Debug, S, [], {event,undefined}, % Will be discarded thanks to {postpone,false} - OldState, State, Data, NewActions). + PrevState, State, Data, NewActions). %%%========================================================================== %%% gen callbacks @@ -868,18 +887,18 @@ loop_event_result( exit, Reason, ?STACKTRACE(), Debug, NewS, Q, Replies), %% Since we got back here Replies was bad terminate(Class, NewReason, Stacktrace, NewDebug, NewS, Q); - {next_state,NewState,NewData} - when CallbackMode =:= state_functions, is_atom(NewState); + {next_state,NextState,NewData} + when CallbackMode =:= state_functions, is_atom(NextState); CallbackMode =/= state_functions -> loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, []); - {next_state,NewState,NewData,Actions} - when CallbackMode =:= state_functions, is_atom(NewState); + State, NextState, NewData, []); + {next_state,NextState,NewData,Actions} + when CallbackMode =:= state_functions, is_atom(NextState); CallbackMode =/= state_functions -> loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, Actions); + State, NextState, NewData, Actions); {keep_state,NewData} -> loop_event_actions( Parent, Debug, S, Events, Event, @@ -902,13 +921,13 @@ loop_event_result( end. loop_event_actions( - Parent, Debug, S, Events, Event, State, NewState, NewData, Actions) -> + Parent, Debug, S, Events, Event, State, NextState, NewData, Actions) -> Postpone = false, % Shall we postpone this event, true or false Hibernate = false, Timeout = undefined, NextEvents = [], loop_event_actions( - Parent, Debug, S, Events, Event, State, NewState, NewData, + Parent, Debug, S, Events, Event, State, NextState, NewData, if is_list(Actions) -> Actions; @@ -920,19 +939,19 @@ loop_event_actions( %% Process all action()s loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, [Action|Actions], + State, NextState, NewData, [Action|Actions], Postpone, Hibernate, Timeout, NextEvents) -> case Action of %% Actions that set options postpone -> loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, Actions, + State, NextState, NewData, Actions, true, Hibernate, Timeout, NextEvents); {postpone,NewPostpone} when is_boolean(NewPostpone) -> loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, Actions, + State, NextState, NewData, Actions, NewPostpone, Hibernate, Timeout, NextEvents); {postpone,_} -> ?TERMINATE( @@ -940,12 +959,12 @@ loop_event_actions( hibernate -> loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, Actions, + State, NextState, NewData, Actions, Postpone, true, Timeout, NextEvents); {hibernate,NewHibernate} when is_boolean(NewHibernate) -> loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, Actions, + State, NextState, NewData, Actions, Postpone, NewHibernate, Timeout, NextEvents); {hibernate,_} -> ?TERMINATE( @@ -953,12 +972,12 @@ loop_event_actions( {timeout,infinity,_} -> % Clear timer - it will never trigger loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, Actions, + State, NextState, NewData, Actions, Postpone, Hibernate, undefined, NextEvents); {timeout,Time,_} = NewTimeout when is_integer(Time), Time >= 0 -> loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, Actions, + State, NextState, NewData, Actions, Postpone, Hibernate, NewTimeout, NextEvents); {timeout,_,_} -> ?TERMINATE( @@ -970,7 +989,7 @@ loop_event_actions( NewDebug = do_reply(Debug, S, Caller, Reply), loop_event_actions( Parent, NewDebug, S, Events, Event, - State, NewState, NewData, Actions, + State, NextState, NewData, Actions, Postpone, Hibernate, Timeout, NextEvents); false -> ?TERMINATE( @@ -981,7 +1000,7 @@ loop_event_actions( true -> loop_event_actions( Parent, Debug, S, Events, Event, - State, NewState, NewData, Actions, + State, NextState, NewData, Actions, Postpone, Hibernate, Timeout, [{Type,Content}|NextEvents]); false -> @@ -996,7 +1015,7 @@ loop_event_actions( %% End of actions list loop_event_actions( Parent, Debug, #{postponed := P0} = S, Events, Event, - State, NewState, NewData, [], + State, NextState, NewData, [], Postpone, Hibernate, Timeout, NextEvents) -> %% %% All options have been collected and next_events are buffered. @@ -1011,7 +1030,7 @@ loop_event_actions( end, {Q2,P} = % Move all postponed events to queue if state change if - NewState =:= State -> + NextState =:= State -> {Events,P1}; true -> {lists:reverse(P1, Events),[]} @@ -1024,9 +1043,9 @@ loop_event_actions( Debug, S, case Postpone of true -> - {postpone,Event,NewState}; + {postpone,Event,NextState}; false -> - {consume,Event,NewState} + {consume,Event,NextState} end), %% Have a peek on the event queue so we can avoid starting %% the state timer unless we have to @@ -1057,7 +1076,7 @@ loop_event_actions( loop_events( Parent, NewDebug, S#{ - state := NewState, + state := NextState, data := NewData, timer := Timer, postponed := P, diff --git a/lib/tools/doc/src/erlang_mode.xml b/lib/tools/doc/src/erlang_mode.xml index 4ecb8feadd..00cf5196b4 100644 --- a/lib/tools/doc/src/erlang_mode.xml +++ b/lib/tools/doc/src/erlang_mode.xml @@ -4,7 +4,7 @@
- 20032013 + 20032016 Ericsson AB. All Rights Reserved. @@ -252,6 +252,7 @@ behavior gen_event - skeleton for the OTP gen_event behavior gen_fsm - skeleton for the OTP gen_fsm behavior + gen_statem - skeleton for the OTP gen_statem behavior Library module - skeleton for a module that does not implement a process. Corba callback - skeleton for a Corba callback module. diff --git a/lib/tools/emacs/erlang-skels.el b/lib/tools/emacs/erlang-skels.el index 6880ec733c..cdf09ad502 100644 --- a/lib/tools/emacs/erlang-skels.el +++ b/lib/tools/emacs/erlang-skels.el @@ -1,7 +1,7 @@ ;; ;; %CopyrightBegin% ;; -;; Copyright Ericsson AB 2010-2014. All Rights Reserved. +;; Copyright Ericsson AB 2010-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. @@ -56,6 +56,8 @@ erlang-skel-gen-event erlang-skel-header) ("gen_fsm" "gen-fsm" erlang-skel-gen-fsm erlang-skel-header) + ("gen_statem" "gen-statem" + erlang-skel-gen-statem erlang-skel-header) ("wx_object" "wx-object" erlang-skel-wx-object erlang-skel-header) ("Library module" "gen-lib" @@ -858,6 +860,122 @@ Please see the function `tempo-define-template'.") "*The template of a gen_fsm. Please see the function `tempo-define-template'.") +(defvar erlang-skel-gen-statem + '((erlang-skel-include erlang-skel-large-header) + "-behaviour(gen_statem)." n n + + "%% API" n + "-export([start_link/0])." n + n + "%% gen_statem callbacks" n + "-export([init/1, terminate/3, code_change/4])." n + "-export([state_name/3])." n + "-export([handle_event/4])." n + n + "-define(SERVER, ?MODULE)." n + n + "-record(data, {})." n + n + (erlang-skel-double-separator-start 3) + "%%% API" n + (erlang-skel-double-separator-end 3) n + (erlang-skel-separator-start 2) + "%% @doc" n + "%% Creates a gen_statem process which calls Module:init/1 to" n + "%% initialize. To ensure a synchronized start-up procedure, this" n + "%% function does not return until Module:init/1 has returned." n + "%%" n + (erlang-skel-separator-end 2) + "-spec start_link() ->" n> + "{ok, Pid :: pid()} |" n> + "ignore |" n> + "{error, Error :: term()}." n + "start_link() ->" n> + "gen_statem:start_link({local, ?SERVER}, ?MODULE, [], [])." n + n + (erlang-skel-double-separator-start 3) + "%%% gen_statem callbacks" n + (erlang-skel-double-separator-end 3) n + (erlang-skel-separator-start 2) + "%% @private" n + "%% @doc" n + "%% Whenever a gen_statem is started using gen_statem:start/[3,4] or" n + "%% gen_statem:start_link/[3,4], this function is called by the new" n + "%% process to initialize." n + (erlang-skel-separator-end 2) + "-spec init(Args :: term()) -> " n> + "{gen_statem:callback_mode()," n> + "State :: term(), Data :: term()} |" n> + "{gen_statem:callback_mode()," n> + "State :: term(), Data :: term()," n> + "[gen_statem:action()] | gen_statem:action()} |" n> + "ignore |" n> + "{stop, Reason :: term()}." n + "init([]) ->" n> + "{state_functions, state_name, #data{}}." n + n + (erlang-skel-separator-start 2) + "%% @private" n + "%% @doc" n + "%% If the gen_statem runs with CallbackMode =:= state_functions" n + "%% there should be one instance of this function for each possible" n + "%% state name. Whenever a gen_statem receives an event," n + "%% the instance of this function with the same name" n + "%% as the current state name StateName is called to" n + "%% handle the event." n + (erlang-skel-separator-end 2) + "-spec state_name(" n> + "gen_statem:event_type(), Msg :: term()," n> + "Data :: term()) ->" n> + "gen_statem:state_function_result(). " n + "state_name({call,Caller}, _Msg, Data) ->" n> + "{next_state, state_name, Data, [{reply,Caller,ok}]}." n + n + (erlang-skel-separator-start 2) + "%% @private" n + "%% @doc" n + "%% If the gen_statem runs with CallbackMode =:= handle_event_function" n + "%% this function is called for every event a gen_statem receives." n + (erlang-skel-separator-end 2) + "-spec handle_event(" n> + "gen_statem:event_type(), Msg :: term()," n> + "State :: term(), Data :: term()) ->" n> + "gen_statem:handle_event_result(). " n + "handle_event({call,From}, _Msg, State, Data) ->" n> + "{next_state, State, Data, [{reply,From,ok}]}." n + n + (erlang-skel-separator-start 2) + "%% @private" n + "%% @doc" n + "%% This function is called by a gen_statem when it is about to" n + "%% terminate. It should be the opposite of Module:init/1 and do any" n + "%% necessary cleaning up. When it returns, the gen_statem terminates with" n + "%% Reason. The return value is ignored." n + (erlang-skel-separator-end 2) + "-spec terminate(Reason :: term(), State :: term(), Data :: term()) ->" n> + "any()." n + "terminate(_Reason, _State, _Data) ->" n> + "void." n + n + (erlang-skel-separator-start 2) + "%% @private" n + "%% @doc" n + "%% Convert process state when code is changed" n + (erlang-skel-separator-end 2) + "-spec code_change(" n> + "OldVsn :: term() | {down,term()}," n> + "State :: term(), Data :: term(), Extra :: term()) ->" n> + "{ok, NewState :: term(), NewData :: term()}." n + "code_change(_OldVsn, State, Data, _Extra) ->" n> + "{ok, State, Data}." n + n + (erlang-skel-double-separator-start 3) + "%%% Internal functions" n + (erlang-skel-double-separator-end 3) + ) + "*The template of a gen_statem. +Please see the function `tempo-define-template'.") + (defvar erlang-skel-wx-object '((erlang-skel-include erlang-skel-large-header) "-behaviour(wx_object)." n n -- cgit v1.2.3 From 447f3cf2d041670d93683de4fab02338b26fbaf1 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 3 Mar 2016 11:27:02 +0100 Subject: Rename state_timeout -> event_timeout --- lib/stdlib/doc/src/gen_statem.xml | 36 +++++++++++++++++++++++------------- lib/stdlib/src/gen_statem.erl | 10 +++++----- 2 files changed, 28 insertions(+), 18 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index db6a4e03ea..4489ddfb91 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -155,7 +155,7 @@ erlang:'!' -----> Module:StateName/3

Inserting an event replaces the trick of calling your own state handling functions that you often would have to - resort to in e.g gen_fsm + resort to in for example gen_fsm to force processing an inserted event before others. If you for example in gen_statem postpone an event in one state and then call some other state function of yours, @@ -529,17 +529,16 @@ ok all other events. - If a - - state_timeout() + If an + + event_timeout() is set through action() timeout - a state timer may be started or a timeout zero event - may be enqueued as the newest incoming, that is the last - to process before going into receive for new events. + an event timer may be started or a timeout zero event + may be enqueued. The (possibly new) @@ -588,7 +587,7 @@ ok - +

Generate an event of @@ -600,7 +599,8 @@ ok If the value is infinity no timer is started. If it is 0 the timeout event is immediately enqueued as the newest received - (unless there are retried or inserted events to process). + (unless there are retried or inserted events to process + since then the timeout is cancelled). Also note that it is not possible nor needed to cancel this timeout since it is cancelled automatically by any other event. @@ -653,15 +653,25 @@ ok for this state transition. + Timeout + + Short for {timeout,Timeout} that is + the timeout message is the timeout time. + This form exists to make the + state function + return value {next_state,NextState,NewData,Timeout} + allowed like for + + gen_fsm Module:StateName/2. + timeout Set the - transition_option() state_timeout() + transition_option() event_timeout() to Time with the - EventContent as Msg - for the next state. + EventContent as Msg. reply_action() Reply to a caller. @@ -1419,7 +1429,7 @@ ok

This function is called by a gen_statem when it should update its internal state during a release upgrade/downgrade, - i.e. when the instruction {update,Module,Change,...} + that is when the instruction {update,Module,Change,...} where Change={advanced,Extra} is given in the appup file. See diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 438c366f8e..92e26bd7ed 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -77,7 +77,7 @@ -type callback_mode() :: 'state_functions' | 'handle_event_function'. -type transition_option() :: - postpone() | hibernate() | state_timeout(). + postpone() | hibernate() | event_timeout(). -type postpone() :: %% If 'true' postpone the current event %% and retry it when the state changes (=/=) @@ -85,7 +85,7 @@ -type hibernate() :: %% If 'true' hibernate the server instead of going into receive boolean(). --type state_timeout() :: +-type event_timeout() :: %% Generate a ('timeout', Msg, ...) event after Time %% unless some other event is delivered Time :: timeout(). @@ -111,9 +111,9 @@ 'hibernate' | % Set the hibernate option {'hibernate', Hibernate :: hibernate()} | %% - (Timeout :: state_timeout()) | % {timeout,Timeout} - {'timeout', % Set the timeout option - Time :: state_timeout(), Msg :: term()} | + (Timeout :: event_timeout()) | % {timeout,Timeout} + {'timeout', % Set the event timeout option + Time :: event_timeout(), Msg :: term()} | %% reply_action() | %% -- cgit v1.2.3 From 65d2eecef2dedb012abcc76c855b8da90934f10c Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 4 Mar 2016 14:28:27 +0100 Subject: Fix broken documenation reference --- lib/stdlib/doc/src/gen_statem.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 4489ddfb91..4708fc1301 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -361,7 +361,7 @@ ok

Debug option that can be used when starting a gen_statem server through for example - enter_loop/4. + enter_loop/5.

For every entry in Dbgs -- cgit v1.2.3 From 0eddc557de9b0a996ab42aefd66f3229a06f6675 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 17 Mar 2016 15:41:26 +0100 Subject: Do documentation improvements from Fred Hebert --- lib/stdlib/doc/src/gen_statem.xml | 66 ++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 12 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 4708fc1301..90d2326954 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -35,10 +35,10 @@ A behaviour module for implementing a state machine. Two callback modes are supported. One for a finite state machine like gen_fsm - that require the state to be an atom and use that state as + that requires the state to be an atom and use that state as the name of the callback function for a particular state, - and one without restriction on the state that use the same - callback function for all states. + and one without restriction on the state data type + that uses the same callback function for all states.

A generic state machine process (gen_statem) implemented @@ -96,7 +96,7 @@ erlang:'!' -----> Module:StateName/3

When callback_mode - is state_functions the state has to be an atom and + is state_functions, the state has to be an atom and is used as the state function name. See Module:StateName/3 @@ -127,8 +127,8 @@ erlang:'!' -----> Module:StateName/3 state function in that order. The state function can postpone an event so it is not retried in the current state. - After a state change all enqueued events (including postponed) - are again presented to the state function. + After a state change all enqueued events (including postponed ones) + are presented again to the state function.

The gen_statem event queue model is sufficient @@ -177,8 +177,10 @@ erlang:'!' -----> Module:StateName/3

Note that a gen_statem does not trap exit signals - automatically, this must be explicitly initiated by - the callback module. + automatically, this must be explicitly initiated in + the callback module (by calling + + process_flag(trap_exit, true).

Unless otherwise stated, all functions in this module fail if @@ -293,6 +295,42 @@ ok in function gen:do_for_proc/2 (gen.erl, line 261) in call from gen_statem:call/3 (gen_statem.erl, line 386) + +

+ And just to compare styles here is the same example using + + callback_mode + + state_functions, or rather here is code to replace + from the init/1 function of the pushbutton + example module above: +

+ +init([]) -> + %% Set the callback mode and initial state + data. + %% Data is used only as a counter. + State = off, Data = 0, + {handle_event_function,State,Data}. + + +%%% Event handling + +handle_event({call,Caller}, push, off, Data) -> + %% Go to 'on', increment count and reply + %% that the resulting status is 'on' + {next_state,on,Data+1,[{reply,Caller,on}]}; +handle_event({call,Caller}, push, on, Data) -> + %% Go to 'off' and reply that the resulting status is 'off' + {next_state,off,Data,[{reply,Caller,off}]}; +%% +%% Event handling common to all states +handle_event({call,Caller}, get_count, State, Data) -> + %% Reply with the current count + {next_state,State,Data,[{reply,Caller,Data}]}; +handle_event(_, _, State, Data) -> + %% Ignore all other events + {next_state,State,Data}. + @@ -564,8 +602,8 @@ ok

If true postpone the current event and retry - it when the state changes that is: - NextState =/= State. + it when the state changes + (NextState =/= State).

@@ -778,14 +816,18 @@ ok The gen_statem will keep the current state, or do a state transition to the current state if you like, set NewData - and execute all Actions + and execute all Actions. + This is the same as + {next_state,CurrentState,NewData,Actions}. keep_state_and_data The gen_statem will keep the current state or do a state transition to the current state if you like, keep the current server data, - and execute all Actions + and execute all Actions. + This is the same as + {next_state,CurrentState,CurrentData,Actions}.

-- cgit v1.2.3 From 2cd331fec3e08799bdd482399e8c969102e1142f Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Thu, 17 Mar 2016 16:05:35 +0100 Subject: Change Caller -> From as suggested by Fred Hebert --- lib/stdlib/doc/src/gen_statem.xml | 50 +++++++++++++++++++-------------------- lib/stdlib/src/gen_statem.erl | 46 +++++++++++++++++------------------ 2 files changed, 48 insertions(+), 48 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 90d2326954..83f7dc1c8a 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -253,23 +253,23 @@ init([]) -> %%% State functions -off({call,Caller}, push, Data) -> +off({call,From}, push, Data) -> %% Go to 'on', increment count and reply %% that the resulting status is 'on' - {next_state,on,Data+1,[{reply,Caller,on}]}; + {next_state,on,Data+1,[{reply,From,on}]}; off(EventType, EventContent, Data) -> handle_event(EventType, EventContent, Data). -on({call,Caller}, push, Data) -> +on({call,From}, push, Data) -> %% Go to 'off' and reply that the resulting status is 'off' - {next_state,off,Data,[{reply,Caller,off}]}; + {next_state,off,Data,[{reply,From,off}]}; on(EventType, EventContent, Data) -> handle_event(EventType, EventContent, Data). %% Handle events common to all states -handle_event({call,Caller}, get_count, Data) -> +handle_event({call,From}, get_count, Data) -> %% Reply with the current count - {keep_state,Data,[{reply,Caller,Data}]}; + {keep_state,Data,[{reply,From,Data}]}; handle_event(_, _, Data) -> %% Ignore all other events {keep_state,Data}. @@ -315,18 +315,18 @@ init([]) -> %%% Event handling -handle_event({call,Caller}, push, off, Data) -> +handle_event({call,From}, push, off, Data) -> %% Go to 'on', increment count and reply %% that the resulting status is 'on' - {next_state,on,Data+1,[{reply,Caller,on}]}; -handle_event({call,Caller}, push, on, Data) -> + {next_state,on,Data+1,[{reply,From,on}]}; +handle_event({call,From}, push, on, Data) -> %% Go to 'off' and reply that the resulting status is 'off' - {next_state,off,Data,[{reply,Caller,off}]}; + {next_state,off,Data,[{reply,From,off}]}; %% %% Event handling common to all states -handle_event({call,Caller}, get_count, State, Data) -> +handle_event({call,From}, get_count, State, Data) -> %% Reply with the current count - {next_state,State,Data,[{reply,Caller,Data}]}; + {next_state,State,Data,[{reply,From,Data}]}; handle_event(_, _, State, Data) -> %% Ignore all other events {next_state,State,Data}. @@ -429,12 +429,12 @@ handle_event(_, _, State, Data) -> - +

Destination to use when replying through for example the - action() {reply,Caller,Reply} + action() {reply,From,Reply} to a process that has called the gen_statem server using call/2. @@ -483,7 +483,7 @@ handle_event(_, _, State, Data) ->

External events are of 3 different type: - {call,Caller}, cast or info. + {call,From}, cast or info. Calls (synchronous) and casts @@ -739,9 +739,9 @@ handle_event(_, _, State, Data) ->

Reply to a caller waiting for a reply in call/2. - Caller must be the term from the + From must be the term from the - {call,Caller} + {call,From} argument to the state function. @@ -1029,14 +1029,14 @@ handle_event(_, _, State, Data) -> The gen_statem will call the state function with event_type() - {call,Caller} and event content + {call,From} and event content Request.

A Reply is generated when a state function returns with - {reply,Caller,Reply} as one + {reply,From,Reply} as one action(), and that Reply becomes the return value of this function. @@ -1100,13 +1100,13 @@ handle_event(_, _, State, Data) -> state function.

- Caller must be the term from the + From must be the term from the - {call,Caller} + {call,From} argument to the state function. - Caller and Reply + From and Reply can also be specified using a reply_action() @@ -1340,15 +1340,15 @@ handle_event(_, _, State, Data) ->

If EventType is - {call,Caller} + {call,From} the caller is waiting for a reply. The reply can be sent from this or from any other state function - by returning with {reply,Caller,Reply} in + by returning with {reply,From,Reply} in Actions, in Replies or by calling - reply(Caller, Reply). + reply(From, Reply).

If this function returns with a next state that diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 92e26bd7ed..82ac5824eb 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -59,7 +59,7 @@ %%% Interface functions. %%%========================================================================== --type caller() :: +-type from() :: {To :: pid(), Tag :: term()}. % Reply-to specifier for call -type state() :: @@ -71,7 +71,7 @@ -type data() :: term(). -type event_type() :: - {'call',Caller :: caller()} | 'cast' | + {'call',From :: from()} | 'cast' | 'info' | 'timeout' | 'internal'. -type callback_mode() :: 'state_functions' | 'handle_event_function'. @@ -125,7 +125,7 @@ EventContent :: term()}. -type reply_action() :: {'reply', % Reply to a caller - Caller :: caller(), Reply :: term()}. + From :: from(), Reply :: term()}. -type state_function_result() :: {'next_state', % {next_state,NextStateName,NewData,[]} @@ -253,13 +253,13 @@ callback_mode(CallbackMode) -> false end. %% -caller({Pid,_}) when is_pid(Pid) -> +from({Pid,_}) when is_pid(Pid) -> true; -caller(_) -> +from(_) -> false. %% -event_type({call,Caller}) -> - caller(Caller); +event_type({call,From}) -> + from(From); event_type(Type) -> case Type of cast -> @@ -378,7 +378,7 @@ cast(ServerRef, Msg) when is_pid(ServerRef) -> send(ServerRef, wrap_cast(Msg)). %% Call a state machine (synchronous; a reply is expected) that -%% arrives with type {call,Caller} +%% arrives with type {call,From} -spec call(ServerRef :: server_ref(), Request :: term()) -> Reply :: term(). call(ServerRef, Request) -> call(ServerRef, Request, infinity). @@ -437,12 +437,12 @@ call(ServerRef, Request, Timeout) -> %% Reply from a state machine callback to whom awaits in call/2 -spec reply([reply_action()] | reply_action()) -> ok. -reply({reply,Caller,Reply}) -> - reply(Caller, Reply); +reply({reply,From,Reply}) -> + reply(From, Reply); reply(Replies) when is_list(Replies) -> replies(Replies). %% --spec reply(Caller :: caller(), Reply :: term()) -> ok. +-spec reply(From :: from(), Reply :: term()) -> ok. reply({To,Tag}, Reply) when is_pid(To) -> Msg = {Tag,Reply}, try To ! Msg of @@ -509,8 +509,8 @@ enter_loop(Module, Opts, CallbackMode, State, Data, Server, Actions) -> wrap_cast(Event) -> {'$gen_cast',Event}. -replies([{reply,Caller,Reply}|Replies]) -> - reply(Caller, Reply), +replies([{reply,From,Reply}|Replies]) -> + reply(From, Reply), replies(Replies); replies([]) -> ok. @@ -782,8 +782,8 @@ loop_receive(Parent, Debug, #{timer := Timer} = S) -> end, Event = case Msg of - {'$gen_call',Caller,Request} -> - {{call,Caller},Request}; + {'$gen_call',From,Request} -> + {{call,From},Request}; {'$gen_cast',E} -> {cast,E}; _ -> @@ -983,10 +983,10 @@ loop_event_actions( ?TERMINATE( error, {bad_action,Action}, Debug, S, [Event|Events]); %% Actual actions - {reply,Caller,Reply} -> - case caller(Caller) of + {reply,From,Reply} -> + case from(From) of true -> - NewDebug = do_reply(Debug, S, Caller, Reply), + NewDebug = do_reply(Debug, S, From, Reply), loop_event_actions( Parent, NewDebug, S, Events, Event, State, NextState, NewData, Actions, @@ -1101,17 +1101,17 @@ do_reply_then_terminate(Class, Reason, Stacktrace, Debug, S, Q, []) -> do_reply_then_terminate( Class, Reason, Stacktrace, Debug, S, Q, [R|Rs]) -> case R of - {reply,{_To,_Tag}=Caller,Reply} -> - NewDebug = do_reply(Debug, S, Caller, Reply), + {reply,{_To,_Tag}=From,Reply} -> + NewDebug = do_reply(Debug, S, From, Reply), do_reply_then_terminate( Class, Reason, Stacktrace, NewDebug, S, Q, Rs); _ -> [error,{bad_action,R},?STACKTRACE(),Debug] end. -do_reply(Debug, S, Caller, Reply) -> - reply(Caller, Reply), - sys_debug(Debug, S, {out,Reply,Caller}). +do_reply(Debug, S, From, Reply) -> + reply(From, Reply), + sys_debug(Debug, S, {out,Reply,From}). terminate( -- cgit v1.2.3 From 6bf2768c6836440634019d69231534ed69fcf982 Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Fri, 18 Mar 2016 10:23:48 +0100 Subject: Do more intricate Fred Hebert doc changes --- lib/stdlib/doc/src/gen_statem.xml | 126 +++++++++++++++++++++----------------- lib/stdlib/src/gen_statem.erl | 10 +-- 2 files changed, 74 insertions(+), 62 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index 83f7dc1c8a..bd70c4c37e 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -127,8 +127,7 @@ erlang:'!' -----> Module:StateName/3 state function in that order. The state function can postpone an event so it is not retried in the current state. - After a state change all enqueued events (including postponed ones) - are presented again to the state function. + After a state change the queue restarts with the postponed events.

The gen_statem event queue model is sufficient @@ -157,10 +156,10 @@ erlang:'!' -----> Module:StateName/3 state handling functions that you often would have to resort to in for example gen_fsm to force processing an inserted event before others. - If you for example in gen_statem postpone an event - in one state and then call some other state function of yours, - you have not changed states and hence the postponed event - will not be retried, which is logical but might be confusing. + A warning, though: if you in gen_statem for example + postpone an event in one state and then call some other state function of yours, + you have not changed states and hence the postponed event will not be retried, + which is logical but might be confusing.

See the type @@ -302,8 +301,8 @@ ok callback_mode state_functions, or rather here is code to replace - from the init/1 function of the pushbutton - example module above: + from the init/1 function of the pushbutton.erl + example file above:

init([]) -> @@ -364,8 +363,8 @@ handle_event(_, _, State, Data) ->

It can be:

- pid() - LocalName + pid()
+ LocalName
The gen_statem is locally registered. Name, Node @@ -489,13 +488,10 @@ handle_event(_, _, State, Data) -> casts originate from the corresponding API functions. For calls the event contain whom to reply to. - Type info originates from - regular process messages sent - to the gen_statem. - It is also possible for the state machine - implementation to insert events to itself, - in particular of types - timeout and internal. + Type info originates from regular process messages sent + to the gen_statem. It is also possible for the state machine + implementation to generate events of types + timeout and internal to itself.

@@ -559,7 +555,7 @@ handle_event(_, _, State, Data) -> is reset to start with the oldest postponed.
- All events inserted with + All events stored with action() next_event @@ -634,14 +630,21 @@ handle_event(_, _, State, Data) -> event arrives in which case this timeout is cancelled. Note that a retried or inserted event counts just like a new in this respect. - If the value is infinity no timer is started. - If it is 0 the timeout event - is immediately enqueued as the newest received - (unless there are retried or inserted events to process - since then the timeout is cancelled). - Also note that it is not possible nor needed - to cancel this timeout since it is cancelled automatically - by any other event. +

+

+ If the value is infinity no timer is started since + it will never trigger anyway. +

+

+ If the value is 0 the timeout event is immediately enqueued + unless there already are enqueued events since then the + timeout is immediately cancelled. + This is a feature ensuring that a timeout 0 event + will be processed before any not yet received external event. +

+

+ Note that it is not possible nor needed to cancel this timeout + since it is cancelled automatically by any other event.

@@ -658,16 +661,19 @@ handle_event(_, _, State, Data) ->

Actions are executed in the containing list order. - The order matters for some actions such as next_event - and reply_action().

Actions that set transition options - overrides any previous of the same kind, + overrides any previous of the same type, so the last in the containing list wins. + For example the last + + event_timeout() + + overrides any other event_timeout() in the list.

postpone @@ -693,7 +699,7 @@ handle_event(_, _, State, Data) ->
Timeout - Short for {timeout,Timeout} that is + Short for {timeout,Timeout,Timeout} that is the timeout message is the timeout time. This form exists to make the state function @@ -708,27 +714,33 @@ handle_event(_, _, State, Data) -> transition_option() event_timeout() - to Time with the - EventContent as Msg. + to Time with EventContent. reply_action() Reply to a caller. next_event - Insert the given EventType - and EventContent as the next to process. - This will bypass any events in the process mailbox as well - as any other queued events. - All next_event actions - in the containing list are buffered and inserted - after the actions have been done - so that the first in the list will be the first to process. - An event of type - - internal - - should be used when you want to reliably distinguish - an event inserted this way from any external event. + Store the given EventType + and EventContent for insertion after all + actions have been executed. + + +

+ The stored events are inserted in the queue as the next to process + before any already queued events. The order of these stored events + is preserved so the first next_event in the containing list + will become the first to process. +

+
+ +

+ An event of type + + internal + + should be used when you want to reliably distinguish + an event inserted this way from any external event. +

@@ -1047,17 +1059,17 @@ handle_event(_, _, State, Data) -> or the atom infinity to wait indefinitely, which is the default. If no reply is received within the specified time, the function call fails. - -

- To avoid getting a late reply in the caller's - inbox this function spawns a proxy process that - does the call. A late reply gets delivered to the - dead proxy process hence gets discarded. This is - less efficient than using - Timeout =:= infinity. -

-

+ +

+ To avoid getting a late reply in the caller's + inbox this function spawns a proxy process that + does the call. A late reply gets delivered to the + dead proxy process hence gets discarded. This is + less efficient than using + Timeout =:= infinity. +

+

The call may fail for example if the gen_statem dies before or during this function call. @@ -1218,7 +1230,7 @@ handle_event(_, _, State, Data) -> CALLBACK FUNCTIONS

The following functions should be exported from a - gen_statem callback module. + gen_statem callback module.

diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 82ac5824eb..9c1e126507 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -86,7 +86,7 @@ %% If 'true' hibernate the server instead of going into receive boolean(). -type event_timeout() :: - %% Generate a ('timeout', Msg, ...) event after Time + %% Generate a ('timeout', EventContent, ...) event after Time %% unless some other event is delivered Time :: timeout(). @@ -113,7 +113,7 @@ %% (Timeout :: event_timeout()) | % {timeout,Timeout} {'timeout', % Set the event timeout option - Time :: event_timeout(), Msg :: term()} | + Time :: event_timeout(), EventContent :: term()} | %% reply_action() | %% @@ -1054,17 +1054,17 @@ loop_event_actions( undefined -> %% No state timeout has been requested {Q3,undefined}; - {timeout,Time,Msg} -> + {timeout,Time,EventContent} -> %% A state timeout has been requested case Q3 of [] when Time =:= 0 -> %% Immediate timeout - simulate it %% so we do not get the timeout message %% after any received event - {[{timeout,Msg}],undefined}; + {[{timeout,EventContent}],undefined}; [] -> %% Actually start a timer - {Q3,erlang:start_timer(Time, self(), Msg)}; + {Q3,erlang:start_timer(Time, self(), EventContent)}; _ -> %% Do not start a timer since any queued %% event cancels the state timer so we pretend -- cgit v1.2.3 From 9db6054d674969fef314bf8676b6f9b583af3bef Mon Sep 17 00:00:00 2001 From: Raimo Niskanen Date: Tue, 12 Apr 2016 14:30:02 +0200 Subject: Fix missing short forms for event timeout --- lib/stdlib/src/gen_statem.erl | 75 +++++++++++++++++++++--------------- lib/stdlib/test/gen_statem_SUITE.erl | 8 ++-- 2 files changed, 48 insertions(+), 35 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 9c1e126507..b3a149a569 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -942,12 +942,32 @@ loop_event_actions( State, NextState, NewData, [Action|Actions], Postpone, Hibernate, Timeout, NextEvents) -> case Action of + %% Actual actions + {reply,From,Reply} -> + case from(From) of + true -> + NewDebug = do_reply(Debug, S, From, Reply), + loop_event_actions( + Parent, NewDebug, S, Events, Event, + State, NextState, NewData, Actions, + Postpone, Hibernate, Timeout, NextEvents); + false -> + ?TERMINATE( + error, {bad_action,Action}, Debug, S, [Event|Events]) + end; + {next_event,Type,Content} -> + case event_type(Type) of + true -> + loop_event_actions( + Parent, Debug, S, Events, Event, + State, NextState, NewData, Actions, + Postpone, Hibernate, Timeout, + [{Type,Content}|NextEvents]); + false -> + ?TERMINATE( + error, {bad_action,Action}, Debug, S, [Event|Events]) + end; %% Actions that set options - postpone -> - loop_event_actions( - Parent, Debug, S, Events, Event, - State, NextState, NewData, Actions, - true, Hibernate, Timeout, NextEvents); {postpone,NewPostpone} when is_boolean(NewPostpone) -> loop_event_actions( Parent, Debug, S, Events, Event, @@ -956,11 +976,11 @@ loop_event_actions( {postpone,_} -> ?TERMINATE( error, {bad_action,Action}, Debug, S, [Event|Events]); - hibernate -> + postpone -> loop_event_actions( Parent, Debug, S, Events, Event, State, NextState, NewData, Actions, - Postpone, true, Timeout, NextEvents); + true, Hibernate, Timeout, NextEvents); {hibernate,NewHibernate} when is_boolean(NewHibernate) -> loop_event_actions( Parent, Debug, S, Events, Event, @@ -969,6 +989,11 @@ loop_event_actions( {hibernate,_} -> ?TERMINATE( error, {bad_action,Action}, Debug, S, [Event|Events]); + hibernate -> + loop_event_actions( + Parent, Debug, S, Events, Event, + State, NextState, NewData, Actions, + Postpone, true, Timeout, NextEvents); {timeout,infinity,_} -> % Clear timer - it will never trigger loop_event_actions( Parent, Debug, S, Events, Event, @@ -982,31 +1007,17 @@ loop_event_actions( {timeout,_,_} -> ?TERMINATE( error, {bad_action,Action}, Debug, S, [Event|Events]); - %% Actual actions - {reply,From,Reply} -> - case from(From) of - true -> - NewDebug = do_reply(Debug, S, From, Reply), - loop_event_actions( - Parent, NewDebug, S, Events, Event, - State, NextState, NewData, Actions, - Postpone, Hibernate, Timeout, NextEvents); - false -> - ?TERMINATE( - error, {bad_action,Action}, Debug, S, [Event|Events]) - end; - {next_event,Type,Content} -> - case event_type(Type) of - true -> - loop_event_actions( - Parent, Debug, S, Events, Event, - State, NextState, NewData, Actions, - Postpone, Hibernate, Timeout, - [{Type,Content}|NextEvents]); - false -> - ?TERMINATE( - error, {bad_action,Action}, Debug, S, [Event|Events]) - end; + infinity -> % Clear timer - it will never trigger + loop_event_actions( + Parent, Debug, S, Events, Event, + State, NextState, NewData, Actions, + Postpone, Hibernate, undefined, NextEvents); + Time when is_integer(Time), Time >= 0 -> + NewTimeout = {timeout,Time,Time}, + loop_event_actions( + Parent, Debug, S, Events, Event, + State, NextState, NewData, Actions, + Postpone, Hibernate, NewTimeout, NextEvents); _ -> ?TERMINATE( error, {bad_action,Action}, Debug, S, [Event|Events]) diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 573a64bbb7..27fb7d9650 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -1305,10 +1305,10 @@ terminate(_Reason, _State, _Data) -> idle(cast, {connect,Pid}, Data) -> Pid ! accept, - {next_state,wfor_conf,Data}; + {next_state,wfor_conf,Data,infinity}; % NoOp timeout just to test API idle({call,From}, connect, Data) -> gen_statem:reply(From, accept), - {next_state,wfor_conf,Data}; + {next_state,wfor_conf,Data,infinity}; % NoOp timeout just to test API idle(cast, badreturn, _Data) -> badreturn; idle({call,_From}, badreturn, _Data) -> @@ -1343,13 +1343,15 @@ idle(Type, Content, Data) -> timeout(timeout, idle, {From,Time}) -> TRef = erlang:start_timer(Time, self(), ok), + {keep_state,{From,TRef},0}; % Immediate timeout 0 +timeout(timeout, 0, {From,TRef}) -> {next_state,timeout2,{From,TRef}, [{timeout,1,should_be_cancelled}, postpone]}; % Should cancel state timeout timeout(_, _, _) -> keep_state_and_data. -timeout2(timeout, idle, _) -> +timeout2(timeout, 0, _) -> keep_state_and_data; timeout2(timeout, Reason, _) -> {stop,Reason}; -- 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 --- lib/stdlib/doc/src/gen_statem.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index bd70c4c37e..c21398d64d 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -45,7 +45,7 @@ using this module will have a standard set of interface functions and include functionality for tracing and error reporting. It will also fit into an OTP supervision tree. Refer to - + OTP Design Principles for more information. -- 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. --- lib/stdlib/doc/src/gen_statem.xml | 71 ++++++++++++++++++++++-------------- lib/stdlib/src/gen_statem.erl | 60 +++++++++++++++--------------- lib/stdlib/test/gen_statem_SUITE.erl | 8 ++-- 3 files changed, 79 insertions(+), 60 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index c21398d64d..adca3673be 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -32,8 +32,9 @@ Generic State Machine Behaviour

- A behaviour module for implementing a state machine. - Two callback modes are supported. One for a finite state + A behaviour module for implementing a state machine. Two + callback modes + are supported. One for a finite state machine like gen_fsm that requires the state to be an atom and use that state as the name of the callback function for a particular state, @@ -89,13 +90,13 @@ erlang:'!' -----> Module:StateName/3 The "state function" for a specific state in a gen_statem is the callback function that is called - for all events in this state, and is selected depending on - callback_mode + for all events in this state, and is selected depending on which + callback mode that the implementation specifies when the the server starts.

- When - callback_mode + When the + callback mode is state_functions, the state has to be an atom and is used as the state function name. See @@ -110,7 +111,8 @@ erlang:'!' -----> Module:StateName/3 makes the state name terminate unusable.

- When callback_mode + When the + callback mode is handle_event_function the state can be any term and the state function name is @@ -208,9 +210,7 @@ erlang:'!' -----> Module:StateName/3

This example shows a simple pushbutton model for a toggling pushbutton implemented with - - callback_mode() - + callback mode state_functions. You can push the button and it replies if it went on or off, and you can ask for a count of how many times it has been @@ -226,6 +226,7 @@ erlang:'!' -----> Module:StateName/3 -export([on/3,off/3]). name() -> pushbutton_statem. % The registered server name +callback_mode() -> state_functions. %% API. This example uses a registered name name() %% and does not link to the caller. @@ -242,12 +243,12 @@ stop() -> terminate(_Reason, _State, _Data) -> void. code_change(_Vsn, State, Data, _Extra) -> - {ok,State,Data}. + {callback_mode(),State,Data}. init([]) -> %% Set the callback mode and initial state + data. %% Data is used only as a counter. State = off, Data = 0, - {state_functions,State,Data}. + {callback_mode(),State,Data}. %%% State functions @@ -297,9 +298,7 @@ ok

And just to compare styles here is the same example using - - callback_mode - + callback mode state_functions, or rather here is code to replace from the init/1 function of the pushbutton.erl example file above: @@ -453,10 +452,8 @@ handle_event(_, _, State, Data) ->

- If - - callback_mode - + If the + callback mode is state_functions, the state has to be of this type.

@@ -499,7 +496,7 @@ handle_event(_, _, State, Data) ->

- The callback_mode is selected when starting the + The callback mode is selected when starting the gen_statem using the return value from Module:init/1 or when calling @@ -1277,7 +1274,7 @@ handle_event(_, _, State, Data) -> return {CallbackMode,State,Data} or {CallbackMode,State,Data,Actions}. CallbackMode selects the - callback_mode(). + callback mode. of the gen_statem. State is the initial state() @@ -1344,8 +1341,8 @@ handle_event(_, _, State, Data) -> Whenever a gen_statem receives an event from call/2, cast/2 or - as a normal process message one of these functions is called. - If callback_mode + as a normal process message one of these functions is called. If + callback mode is state_functions then Module:StateName/3 is called, and if it is handle_event_function then Module:handle_event/4 is called. @@ -1468,7 +1465,11 @@ handle_event(_, _, State, Data) ->   Vsn = term() OldState = NewState = term() Extra = term() - Result = {ok,NewState,NewData} | Reason + Result = {NewCallbackMode,NewState,NewData} | Reason + + NewCallbackMode = + callback_mode() + OldState = NewState = state() @@ -1484,8 +1485,9 @@ handle_event(_, _, State, Data) -> This function is called by a gen_statem when it should update its internal state during a release upgrade/downgrade, that is when the instruction {update,Module,Change,...} - where Change={advanced,Extra} is given in - the appup file. See + where Change={advanced,Extra} is given in the + appup + file. See OTP Design Principles @@ -1499,6 +1501,21 @@ handle_event(_, _, State, Data) -> Module. If no such attribute is defined, the version is the checksum of the BEAM file.

+ +

+ If you would dare to change + callback mode + during release upgrade/downgrade, the upgrade is no problem + since the new code surely knows what callback mode + it needs, but for a downgrade this function will have to + know from the Extra argument that comes from the + appup + file what callback mode the old code did use. + It may also be possible to figure this out + from the {down,Vsn} argument since Vsn + in effect defines the old callback module version. +

+

OldState and OldData is the internal state of the gen_statem. @@ -1510,7 +1527,7 @@ handle_event(_, _, State, Data) ->

If successful, the function shall return the updated internal state in an - {ok,NewState,NewData} tuple. + {NewCallbackMode,NewState,NewData} tuple.

If the function returns Reason, the ongoing diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index b3a149a569..c85c521d8e 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -219,7 +219,9 @@ OldState :: state(), OldData :: data(), Extra :: term()) -> - {ok, NewState :: state(), NewData :: data()}. + {NewCallbackMode :: callback_mode(), + NewState :: state(), + NewData :: data()}. %% Format the callback module state in some sensible that is %% often condensed way. For StatusOption =:= 'normal' the perferred @@ -491,17 +493,10 @@ enter_loop(Module, Opts, CallbackMode, State, Data, Server_or_Actions) -> Actions :: [action()] | action()) -> no_return(). enter_loop(Module, Opts, CallbackMode, State, Data, Server, Actions) -> - case is_atom(Module) andalso callback_mode(CallbackMode) of - true -> - Parent = gen:get_parent(), - enter( - Module, Opts, CallbackMode, State, Data, Server, - Actions, Parent); - false -> - error( - badarg, - [Module,Opts,CallbackMode,State,Data,Server,Actions]) - end. + is_atom(Module) orelse error({atom,Module}), + callback_mode(CallbackMode) orelse error({callback_mode,CallbackMode}), + Parent = gen:get_parent(), + enter(Module, Opts, CallbackMode, State, Data, Server, Actions, Parent). %%--------------------------------------------------------------------------- %% API helpers @@ -580,15 +575,13 @@ init_result(Starter, Parent, ServerRef, Module, Result, Opts) -> case Result of {CallbackMode,State,Data} -> case callback_mode(CallbackMode) of - true - when CallbackMode =:= state_functions, is_atom(State); - CallbackMode =/= state_functions -> + true -> proc_lib:init_ack(Starter, {ok,self()}), enter( Module, Opts, CallbackMode, State, Data, ServerRef, [], Parent); false -> - Error = {bad_return_value,Result}, + Error = {callback_mode,CallbackMode}, proc_lib:init_ack(Starter, {error,Error}), exit(Error) end; @@ -600,7 +593,7 @@ init_result(Starter, Parent, ServerRef, Module, Result, Opts) -> Module, Opts, CallbackMode, State, Data, ServerRef, Actions, Parent); false -> - Error = {bad_return_value,Result}, + Error = {callback_mode,CallbackMode}, proc_lib:init_ack(Starter, {error,Error}), exit(Error) end; @@ -638,11 +631,12 @@ system_code_change( Result -> Result end of - {ok,NewState,NewData} -> - {ok, - S#{ - state := NewState, - data := NewData}}; + {NewCallbackMode,NewState,NewData} -> + callback_mode(NewCallbackMode) orelse + error({callback_mode,NewCallbackMode}), + {ok,S#{state := NewState, data := NewData}}; + {ok,_} = Error -> + error({case_clause,Error}); Error -> Error end. @@ -825,6 +819,18 @@ loop_events( Result -> loop_event_result( Parent, Debug, S, Events, Event, Result); + error:badarg when CallbackMode =:= state_functions -> + case erlang:get_stacktrace() of + [{erlang,apply,[Module,State,_],_}|Stacktrace] -> + Args = [Type,Content,Data], + terminate( + error, + {undef_state_function,{Module,State,Args}}, + Stacktrace, + Debug, S, Q); + Stacktrace -> + terminate(error, badarg, Stacktrace, Debug, S, Q) + end; error:undef -> %% Process an undef to check for the simple mistake %% of calling a nonexistent state function @@ -861,7 +867,7 @@ loop_events( %% Interpret all callback return variants loop_event_result( Parent, Debug, - #{callback_mode := CallbackMode, state := State, data := Data} = S, + #{state := State, data := Data} = S, Events, Event, Result) -> case Result of stop -> @@ -887,15 +893,11 @@ loop_event_result( exit, Reason, ?STACKTRACE(), Debug, NewS, Q, Replies), %% Since we got back here Replies was bad terminate(Class, NewReason, Stacktrace, NewDebug, NewS, Q); - {next_state,NextState,NewData} - when CallbackMode =:= state_functions, is_atom(NextState); - CallbackMode =/= state_functions -> + {next_state,NextState,NewData} -> loop_event_actions( Parent, Debug, S, Events, Event, State, NextState, NewData, []); - {next_state,NextState,NewData,Actions} - when CallbackMode =:= state_functions, is_atom(NextState); - CallbackMode =/= state_functions -> + {next_state,NextState,NewData,Actions} -> loop_event_actions( Parent, Debug, S, Events, Event, State, NextState, NewData, Actions); diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 27fb7d9650..3deb5fd986 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -637,9 +637,9 @@ code_change(Config) -> {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), {idle,data} = sys:get_state(Pid), sys:suspend(Pid), - sys:change_code(Pid, ?MODULE, old_vsn, extra), + sys:change_code(Pid, ?MODULE, old_vsn, state_functions), sys:resume(Pid), - {idle,{old_vsn,data,extra}} = sys:get_state(Pid), + {idle,{old_vsn,data,state_functions}} = sys:get_state(Pid), stop_it(Pid). call_format_status(Config) -> @@ -1561,8 +1561,8 @@ wrap_result(Result) -> -code_change(OldVsn, State, Data, Extra) -> - {ok,State,{OldVsn,Data,Extra}}. +code_change(OldVsn, State, Data, CallbackMode) -> + {CallbackMode,State,{OldVsn,Data,CallbackMode}}. format_status(terminate, [_Pdict,State,Data]) -> {formatted,State,Data}; -- 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 --- lib/stdlib/doc/src/gen_fsm.xml | 16 +++- lib/stdlib/doc/src/gen_server.xml | 3 +- lib/stdlib/doc/src/gen_statem.xml | 187 ++++++++++++++++++++++++-------------- 3 files changed, 137 insertions(+), 69 deletions(-) (limited to 'lib') diff --git a/lib/stdlib/doc/src/gen_fsm.xml b/lib/stdlib/doc/src/gen_fsm.xml index 4d594b8eb2..835e252704 100644 --- a/lib/stdlib/doc/src/gen_fsm.xml +++ b/lib/stdlib/doc/src/gen_fsm.xml @@ -4,7 +4,7 @@

- 19962014 + 19962016 Ericsson AB. All Rights Reserved. @@ -31,12 +31,23 @@ gen_fsm Generic Finite State Machine Behaviour + +

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

+

A behaviour module for implementing a finite state machine. A generic finite state machine process (gen_fsm) implemented using this module will have a standard set of interface functions and include functionality for tracing and error reporting. It will also fit into an OTP supervision tree. Refer to - OTP Design Principles for more information.

+ OTP Design Principles for more information. +

A gen_fsm assumes all specific parts to be located in a callback module exporting a pre-defined set of functions. The relationship between the behaviour functions and the callback functions can be @@ -848,6 +859,7 @@ gen_fsm:sync_send_all_state_event -----> Module:handle_sync_event/4 SEE ALSO

gen_event(3), gen_server(3), + gen_statem(3), supervisor(3), proc_lib(3), sys(3)

diff --git a/lib/stdlib/doc/src/gen_server.xml b/lib/stdlib/doc/src/gen_server.xml index 6d04771cd4..10dc978afc 100644 --- a/lib/stdlib/doc/src/gen_server.xml +++ b/lib/stdlib/doc/src/gen_server.xml @@ -4,7 +4,7 @@
- 19962014 + 19962016 Ericsson AB. All Rights Reserved. @@ -715,6 +715,7 @@ gen_server:abcast -----> Module:handle_cast/2 SEE ALSO

gen_event(3), gen_fsm(3), + gen_statem(3), supervisor(3), proc_lib(3), sys(3)

diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index adca3673be..91332fbdde 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -34,12 +34,40 @@

A behaviour module for implementing a state machine. Two callback modes - are supported. One for a finite state - machine like gen_fsm - that requires the state to be an atom and use that state as - the name of the callback function for a particular state, + are supported. One for finite state machines + (gen_fsm like) + that requires the state to be an atom and uses that state as + the name of the current callback function, and one without restriction on the state data type - that uses the same callback function for all states. + that uses one callback function for all states. +

+

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

+

+ The gen_statem behaviour is intended to replace + gen_fsm for new code. + It has the same features and add some really useful: +

+ + State code is gathered + The state can be any term + Events can be postponed + Events can be self generated + A reply can be sent from a later state + There can be multiple sys traceable replies + +

+ The callback model(s) for gen_statem differs from + the one for gen_fsm, + but it is still fairly easy to rewrite + from gen_fsm to gen_statem.

A generic state machine process (gen_statem) implemented @@ -148,15 +176,15 @@ erlang:'!' -----> Module:StateName/3 to the state function. That is: as if it is the oldest incoming event. There is a dedicated - event_type() internal + event_type() - that can be used for such events making them impossible + internal that can be used for such events making them impossible to mistake for external events.

Inserting an event replaces the trick of calling your own state handling functions that you often would have to - resort to in for example gen_fsm + resort to in for example gen_fsm to force processing an inserted event before others. A warning, though: if you in gen_statem for example postpone an event in one state and then call some other state function of yours, @@ -172,9 +200,8 @@ erlang:'!' -----> Module:StateName/3

A gen_statem handles system messages as documented in - sys. - The sys module - can be used for debugging a gen_statem. + sys. + The sysmodule can be used for debugging a gen_statem.

Note that a gen_statem does not trap exit signals @@ -354,7 +381,7 @@ handle_event(_, _, State, Data) ->

Server specification to use when addressing a gen_statem server. - See call/2 and + See call/2 and server_name() @@ -397,7 +424,7 @@ handle_event(_, _, State, Data) ->

Debug option that can be used when starting a gen_statem server through for example - enter_loop/5. + enter_loop/5.

For every entry in Dbgs @@ -412,7 +439,7 @@ handle_event(_, _, State, Data) ->

Options that can be used when starting a gen_statem server through for example - start_link/3. + start_link/3.

@@ -421,7 +448,7 @@ handle_event(_, _, State, Data) ->

Return value from the start functions for_example - start_link/3. + start_link/3.

@@ -432,10 +459,11 @@ handle_event(_, _, State, Data) ->

Destination to use when replying through for example the - action() {reply,From,Reply} + action() + {reply,From,Reply} to a process that has called the gen_statem server using - call/2. + call/2.

@@ -465,7 +493,7 @@ handle_event(_, _, State, Data) ->

A term in which the state machine implementation should store any server data it needs. The difference between - this and the state() + this and the state() itself is that a change in this data does not cause postponed events to be retried. Hence if a change in this data would change the set of events that @@ -498,9 +526,13 @@ handle_event(_, _, State, Data) ->

The callback mode is selected when starting the gen_statem using the return value from - Module:init/1 + Module:init/1 or when calling - enter_loop/5-7. + enter_loop/5-7, + and with the return value from + + Module:code_change/4. +

state_functions @@ -536,7 +568,7 @@ handle_event(_, _, State, Data) -> All - actions + actions are processed in order of appearance. @@ -554,20 +586,22 @@ handle_event(_, _, State, Data) -> All events stored with - action() next_event + action() + next_event are inserted in the queue to be processed before all other events. If an - event_timeout() + event_timeout() is set through - action() timeout + action() + timeout an event timer may be started or a timeout zero event may be enqueued. @@ -622,7 +656,8 @@ handle_event(_, _, State, Data) ->

Generate an event of - type timeout + event_type() + timeout after this time (in milliseconds) unless some other event arrives in which case this timeout is cancelled. Note that a retried or inserted event @@ -652,9 +687,9 @@ handle_event(_, _, State, Data) -> These state transition actions may be invoked by returning them from the state function, - from Module:init/1 + from Module:init/1 or by giving them to - enter_loop/6,7. + enter_loop/6,7.

Actions are executed in the containing list order. @@ -677,20 +712,26 @@ handle_event(_, _, State, Data) -> Set the - transition_option() postpone() + transition_option() + + + postpone() for this state transition. This action is ignored when returned from - Module:init/1 + Module:init/1 or given to - enter_loop/5,6 + enter_loop/5,6 since there is no event to postpone in those cases. hibernate Set the - transition_option() hibernate() + transition_option() + + + hibernate() for this state transition. @@ -703,13 +744,17 @@ handle_event(_, _, State, Data) -> return value {next_state,NextState,NewData,Timeout} allowed like for - gen_fsm Module:StateName/2. + gen_fsm Module:StateName/2. + timeout Set the - transition_option() event_timeout() + transition_option() + + + event_timeout() to Time with EventContent. @@ -906,7 +951,7 @@ handle_event(_, _, State, Data) -> If the option {spawn_opt,SpawnOpts} is present in Opts, SpawnOpts will be passed as option list to - spawn_opt/2 + spawn_opt/2 which is used to spawn the gen_statem process.

@@ -967,7 +1012,7 @@ handle_event(_, _, State, Data) -> to start a child.

- See start_link/3,4 + See start_link/3,4 for a description of arguments and return values.

@@ -979,7 +1024,9 @@ handle_event(_, _, State, Data) ->

The same as - stop(ServerRef, normal, infinity). + + stop(ServerRef, normal, infinity). +

@@ -996,7 +1043,7 @@ handle_event(_, _, State, Data) -> and waits for it to terminate. The gen_statem will call - Module:terminate/3 + Module:terminate/3 before exiting.

@@ -1005,7 +1052,9 @@ handle_event(_, _, State, Data) -> with the expected reason. Any other reason than normal, shutdown, or {shutdown,Term} will cause an error report to be issued through - error_logger:format/2. + + error_logger:format/2. + The default Reason is normal.

@@ -1125,7 +1174,7 @@ handle_event(_, _, State, Data) ->

A reply sent with this function will not be visible - in sys debug output. + in sys debug output.

@@ -1194,7 +1243,9 @@ handle_event(_, _, State, Data) -> Module, Opts and Server have the same meanings as when calling - gen_statem:start[_link]/3,4. + + gen_statem:start[_link]/3,4. + However, the server_name() @@ -1205,7 +1256,7 @@ handle_event(_, _, State, Data) -> CallbackMode, State, Data and Actions have the same meanings as in the return value of - Module:init/1. + Module:init/1. Also, the callback module Module does not need to export an init/1 function.

@@ -1259,9 +1310,9 @@ handle_event(_, _, State, Data) ->

Whenever a gen_statem is started using - start_link/3,4 + start_link/3,4 or - start/3,4, + start/3,4, this function is called by the new process to initialize the implementation state and server data.

@@ -1277,9 +1328,9 @@ handle_event(_, _, State, Data) -> callback mode. of the gen_statem. State is the initial - state() + state() and Data the initial server - data(). + data().

The Actions @@ -1291,11 +1342,11 @@ handle_event(_, _, State, Data) -> If something goes wrong during the initialization the function should return {stop,Reason} or ignore. See - start_link/3,4. + start_link/3,4.

This function may use - throw + throw/1 to return Result.

@@ -1339,8 +1390,8 @@ handle_event(_, _, State, Data) ->

Whenever a gen_statem receives an event from - call/2, - cast/2 or + call/2, + cast/2 or as a normal process message one of these functions is called. If callback mode is state_functions then Module:StateName/3 is called, @@ -1354,8 +1405,8 @@ handle_event(_, _, State, Data) -> from this or from any other state function by returning with {reply,From,Reply} in - Actions, in - Replies + Actions, in + Replies or by calling reply(From, Reply).

@@ -1371,13 +1422,13 @@ handle_event(_, _, State, Data) -> there is no restriction on the next state.

- See action() + See action() for options that can be set and actions that can be done by gen_statem after returning from this function.

These functions may use - throw, + throw/1, to return the result.

@@ -1402,7 +1453,7 @@ handle_event(_, _, State, Data) -> value is ignored.

Reason is a term denoting the stop reason and - State + State is the internal state of the gen_statem.

@@ -1410,7 +1461,7 @@ handle_event(_, _, State, Data) -> is terminating. If it is because another callback function has returned a stop tuple {stop,Reason} in - Actions, + Actions, Reason will have the value specified in that tuple. If it is due to a failure, Reason is the error reason.

@@ -1445,11 +1496,13 @@ handle_event(_, _, State, Data) -> shutdown, or {shutdown,Term} the gen_statem is assumed to terminate due to an error and an error report is issued using - error_logger:format/2. + + error_logger:format/2. +

This function may use - throw + throw/1 to return Ignored, which is ignored anyway.

@@ -1504,7 +1557,9 @@ handle_event(_, _, State, Data) ->

If you would dare to change - callback mode + + callback mode + during release upgrade/downgrade, the upgrade is no problem since the new code surely knows what callback mode it needs, but for a downgrade this function will have to @@ -1534,7 +1589,7 @@ handle_event(_, _, State, Data) -> upgrade will fail and roll back to the old release.

This function may use - throw + throw/1 to return Result or Reason.

@@ -1640,7 +1695,7 @@ handle_event(_, _, State, Data) ->

This function may use - throw + throw/1 to return Status.

@@ -1650,11 +1705,11 @@ handle_event(_, _, State, Data) ->
SEE ALSO -

gen_event, - gen_fsm, - gen_server, - supervisor, - proc_lib, - sys

+

gen_event(3), + gen_fsm(3), + gen_server(3), + supervisor(3), + proc_lib(3), + sys(3)

-- cgit v1.2.3