aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/stdlib/doc/src/gen_statem.xml82
-rw-r--r--lib/stdlib/src/gen_statem.erl208
-rw-r--r--lib/stdlib/test/gen_statem_SUITE.erl66
3 files changed, 287 insertions, 69 deletions
diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml
index 17f1526a21..a4c5438a08 100644
--- a/lib/stdlib/doc/src/gen_statem.xml
+++ b/lib/stdlib/doc/src/gen_statem.xml
@@ -70,6 +70,7 @@
<item>The state can be any term.</item>
<item>Events can be postponed.</item>
<item>Events can be self-generated.</item>
+ <item>Automatic state entry events can be generated.</item>
<item>A reply can be sent from a later state.</item>
<item>There can be multiple <c>sys</c> traceable replies.</item>
</list>
@@ -193,6 +194,12 @@ erlang:'!' -----> Module:StateName/3
<seealso marker="gen_fsm"><c>gen_fsm</c></seealso>
to force processing an inserted event before others.
</p>
+ <p>
+ The <c>gen_statem</c> engine can automatically insert
+ a special event whenever a new state is entered; see
+ <seealso marker="#type-state_entry_mode"><c>state_entry_mode()</c></seealso>.
+ This makes it easy to handle code common to all state entries.
+ </p>
<note>
<p>If you in <c>gen_statem</c>, for example, postpone
an event in one state and then call another state function
@@ -515,7 +522,7 @@ handle_event(_, _, State, Data) ->
Type <c>info</c> originates from regular process messages sent
to the <c>gen_statem</c>. Also, the state machine
implementation can generate events of types
- <c>timeout</c> and <c>internal</c> to itself.
+ <c>timeout</c>, <c>enter</c> and <c>internal</c> to itself.
</p>
</desc>
</datatype>
@@ -551,6 +558,34 @@ handle_event(_, _, State, Data) ->
</desc>
</datatype>
<datatype>
+ <name name="state_entry_mode"/>
+ <desc>
+ <p>
+ The <em>state entry mode</em> is selected when starting the
+ <c>gen_statem</c> and after code change
+ using the return value from
+ <seealso marker="#Module:callback_mode/0"><c>Module:callback_mode/0</c></seealso>.
+ </p>
+ <p>
+ If
+ <seealso marker="#Module:callback_mode/0"><c>Module:callback_mode/0</c></seealso>
+ returns a list containing <c>state_entry_events</c>,
+ the <c>gen_statem</c> engine will, at every state change,
+ insert an event of type
+ <seealso marker="#type-event_type">enter</seealso>
+ with content <c>OldState</c>. This event will be inserted
+ before all other events such as those generated by
+ <seealso marker="#type-action"><c>action()</c></seealso>
+ <c>next_event</c>.
+ </p>
+ <p>
+ If
+ <seealso marker="#Module:callback_mode/0"><c>Module:callback_mode/0</c></seealso>
+ does not return such a list, no state entry events are inserted.
+ </p>
+ </desc>
+ </datatype>
+ <datatype>
<name name="transition_option"/>
<desc>
<p>
@@ -591,6 +626,16 @@ handle_event(_, _, State, Data) ->
</p>
</item>
<item>
+ <p>
+ If the state changes or is the initial state, and the
+ <seealso marker="#type-state_entry_mode"><em>state entry mode</em></seealso>
+ is <c>state_entry_events</c>, an event of type
+ <seealso marker="#type-event_type">enter</seealso>
+ with content <c>OldState</c> is inserted
+ to be processed before all other events including those above.
+ </p>
+ </item>
+ <item>
<p>
If an
<seealso marker="#type-event_timeout"><c>event_timeout()</c></seealso>
@@ -1288,7 +1333,9 @@ handle_event(_, _, State, Data) ->
<type>
<v>
CallbackMode =
- <seealso marker="#type-callback_mode">callback_mode()</seealso>
+ <seealso marker="#type-callback_mode">callback_mode()</seealso> |
+ [ <seealso marker="#type-callback_mode">callback_mode()</seealso>
+ | state_entry_events ]
</v>
</type>
<desc>
@@ -1313,12 +1360,35 @@ handle_event(_, _, State, Data) ->
<seealso marker="#Module:code_change/4"><c>Module:code_change/4</c></seealso>
returns.
</p>
+ <p>
+ The <c>CallbackMode</c> is either just
+ <seealso marker="#type-callback_mode">callback_mode()</seealso>
+ or a list containing
+ <seealso marker="#type-callback_mode">callback_mode()</seealso>
+ and possibly the atom
+ <seealso marker="#type-state_entry_mode"><c>state_entry_events</c></seealso>.
+ </p>
+ <p>
+ If the atom <c>state_entry_events</c> is present in the list,
+ the <c>gen_statem</c> engine will, at every state change,
+ insert an event of type
+ <seealso marker="#type-event_type">enter</seealso>
+ with content <c>OldState</c>. This event will be inserted
+ before all other events such as those generated by
+ <seealso marker="#type-action"><c>action()</c></seealso>
+ <c>next_event</c>.
+ </p>
+ <p>
+ No state entry event will be inserted after a
+ <seealso marker="#Module:code_change/4"><c>Module:code_change/4</c></seealso>
+ since transforming the state to a newer version is regarded
+ as staying in the same state even if the newer version state
+ should have a different name.
+ </p>
<note>
<p>
- If this function's body does not consist of solely one of two
- possible
- <seealso marker="#type-callback_mode">atoms</seealso>
- the callback module is doing something strange.
+ If this function's body does not return an inline constant
+ value the callback module is doing something strange.
</p>
</note>
</desc>
diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl
index 46c0e92a9b..7f437404ed 100644
--- a/lib/stdlib/src/gen_statem.erl
+++ b/lib/stdlib/src/gen_statem.erl
@@ -53,7 +53,7 @@
action/0]).
%% Fix problem for doc build
--export_type([transition_option/0]).
+-export_type([state_entry_mode/0,transition_option/0]).
%%%==========================================================================
%%% Interface functions.
@@ -72,9 +72,10 @@
-type event_type() ::
{'call',From :: from()} | 'cast' |
- 'info' | 'timeout' | 'internal'.
+ 'info' | 'timeout' | 'enter' | 'internal'.
-type callback_mode() :: 'state_functions' | 'handle_event_function'.
+-type state_entry_mode() :: 'state_entry_events'.
-type transition_option() ::
postpone() | hibernate() | event_timeout().
@@ -183,7 +184,9 @@
%%
%% It is called once after init/0 and code_change/4 but before
%% the first state callback StateName/3 or handle_event/4.
--callback callback_mode() -> callback_mode().
+-callback callback_mode() ->
+ callback_mode() |
+ [callback_mode() | state_entry_mode()].
%% Example state callback for StateName = 'state_name'
%% when callback_mode() =:= state_functions.
@@ -556,19 +559,27 @@ enter(Module, Opts, State, Data, Server, Actions, Parent) ->
end,
S = #{
callback_mode => undefined,
+ state_entry_events => false,
module => Module,
name => Name,
- %% All fields below will be replaced according to the arguments to
+ %% The rest of the fields are set from to the arguments to
%% loop_event_actions/10 when it finally loops back to loop/3
- state => State,
- data => Data,
- postponed => P,
- hibernate => false,
- timer => undefined},
+ %% in loop_events_done/8
+ %%
+ %% Marker for initial state, cleared immediately when used
+ init_state => true
+ },
NewDebug = sys_debug(Debug, S, State, {enter,Event,State}),
- loop_event_actions(
- Parent, NewDebug, S, Events,
- State, Data, P, Event, State, NewActions).
+ case call_callback_mode(S) of
+ {ok,NewS} ->
+ loop_event_actions(
+ Parent, NewDebug, NewS, Events,
+ State, Data, P, Event, State, NewActions);
+ {Class,Reason,Stacktrace} ->
+ terminate(
+ Class, Reason, Stacktrace,
+ NewDebug, S, [Event|Events], State, Data, P)
+ end.
%%%==========================================================================
%%% gen callbacks
@@ -866,13 +877,93 @@ loop_events(
loop_events_done(Parent, Debug, S, Timer, State, Data, P, Hibernate) ->
NewS =
S#{
- state := State,
- data := Data,
- postponed := P,
- hibernate := Hibernate,
- timer := Timer},
+ state => State,
+ data => Data,
+ postponed => P,
+ hibernate => Hibernate,
+ timer => Timer},
loop(Parent, Debug, NewS).
+
+
+parse_callback_mode([], CBMode, SEntry) ->
+ {CBMode,SEntry};
+parse_callback_mode([H|T], CBMode, SEntry) ->
+ case callback_mode(H) of
+ true ->
+ parse_callback_mode(T, H, SEntry);
+ false ->
+ case H of
+ state_entry_events ->
+ parse_callback_mode(T, CBMode, true);
+ _ ->
+ {undefined,SEntry}
+ end
+ end;
+parse_callback_mode(_, _CBMode, SEntry) ->
+ {undefined,SEntry}.
+
+call_callback_mode(S, CallbackMode) ->
+ case
+ parse_callback_mode(
+ if
+ is_atom(CallbackMode) ->
+ [CallbackMode];
+ true ->
+ CallbackMode
+ end, undefined, false)
+ of
+ {undefined,_} ->
+ {error,
+ {bad_return_from_callback_mode,CallbackMode},
+ ?STACKTRACE()};
+ {CBMode,SEntry} ->
+ {ok,
+ S#{
+ callback_mode := CBMode,
+ state_entry_events := SEntry}}
+ end.
+
+call_callback_mode(#{module := Module} = S) ->
+ try Module:callback_mode() of
+ CallbackMode ->
+ call_callback_mode(S, CallbackMode)
+ catch
+ CallbackMode ->
+ call_callback_mode(S, CallbackMode);
+ error:undef ->
+ %% Process undef to check for the simple mistake
+ %% of calling a nonexistent state function
+ %% to make the undef more precise
+ case erlang:get_stacktrace() of
+ [{Module,callback_mode,[]=Args,_}
+ |Stacktrace] ->
+ {error,
+ {undef_callback,{Module,callback_mode,Args}},
+ Stacktrace};
+ Stacktrace ->
+ {error,undef,Stacktrace}
+ end;
+ Class:Reason ->
+ {Class,Reason,erlang:get_stacktrace()}
+ end.
+
+loop_event(
+ Parent, Debug,
+ #{callback_mode := undefined} = S,
+ Events,
+ State, Data, P, Event, Hibernate) ->
+ %% This happens after code_change/4
+ case call_callback_mode(S) of
+ {ok,NewS} ->
+ loop_event(
+ Parent, Debug, NewS, Events,
+ State, Data, P, Event, Hibernate);
+ {Class,Reason,Stacktrace} ->
+ terminate(
+ Class, Reason, Stacktrace,
+ Debug, S, [Event|Events], State, Data, P)
+ end;
loop_event(
Parent, Debug,
#{callback_mode := CallbackMode,
@@ -891,24 +982,16 @@ loop_event(
%%
try
case CallbackMode of
- undefined ->
- Module:callback_mode();
state_functions ->
erlang:apply(Module, State, [Type,Content,Data]);
handle_event_function ->
Module:handle_event(Type, Content, State, Data)
end
of
- Result when CallbackMode =:= undefined ->
- loop_event_callback_mode(
- Parent, Debug, S, Events, State, Data, P, Event, Result);
Result ->
loop_event_result(
Parent, Debug, S, Events, State, Data, P, Event, Result)
catch
- Result when CallbackMode =:= undefined ->
- loop_event_callback_mode(
- Parent, Debug, S, Events, State, Data, P, Event, Result);
Result ->
loop_event_result(
Parent, Debug, S, Events, State, Data, P, Event, Result);
@@ -936,14 +1019,6 @@ loop_event(
%% of calling a nonexistent state function
%% to make the undef more precise
case erlang:get_stacktrace() of
- [{Module,callback_mode,[]=Args,_}
- |Stacktrace]
- when CallbackMode =:= undefined ->
- terminate(
- error,
- {undef_callback,{Module,callback_mode,Args}},
- Stacktrace,
- Debug, S, [Event|Events], State, Data, P);
[{Module,State,[Type,Content,Data]=Args,_}
|Stacktrace]
when CallbackMode =:= state_functions ->
@@ -972,25 +1047,6 @@ loop_event(
Debug, S, [Event|Events], State, Data, P)
end.
-%% Interpret callback_mode() result
-loop_event_callback_mode(
- Parent, Debug, S, Events, State, Data, P, Event, CallbackMode) ->
- case callback_mode(CallbackMode) of
- true ->
- Hibernate = false, % We have already GC:ed recently
- loop_event(
- Parent, Debug,
- S#{callback_mode := CallbackMode},
- Events,
- State, Data, P, Event, Hibernate);
- false ->
- terminate(
- error,
- {bad_return_from_callback_mode,CallbackMode},
- ?STACKTRACE(),
- Debug, S, [Event|Events], State, Data, P)
- end.
-
%% Interpret all callback return variants
loop_event_result(
Parent, Debug, S, Events, State, Data, P, Event, Result) ->
@@ -1174,7 +1230,7 @@ loop_event_actions(
%%
%% End of actions list
loop_event_actions(
- Parent, Debug, S, Events,
+ Parent, Debug, #{state_entry_events := SEEvents} = S, Events,
State, NewData, P0, Event, NextState, [],
Postpone, Hibernate, Timeout, NextEvents) ->
%%
@@ -1196,7 +1252,25 @@ loop_event_actions(
{lists:reverse(P1, Events),[]}
end,
%% Place next events first in queue
- Q = lists:reverse(NextEvents, Q2),
+ Q3 = lists:reverse(NextEvents, Q2),
+ %% State entry events
+ Q =
+ case SEEvents of
+ true ->
+ %% Generate state entry events
+ case
+ (NextState =/= State)
+ orelse maps:is_key(init_state, S)
+ of
+ true ->
+ %% State change or initial state
+ [{enter,State}|Q3];
+ false ->
+ Q3
+ end;
+ false ->
+ Q3
+ end,
%%
NewDebug =
sys_debug(
@@ -1208,7 +1282,15 @@ loop_event_actions(
{consume,Event,NextState}
end),
loop_events(
- Parent, NewDebug, S, Q, NextState, NewData, P, Hibernate, Timeout).
+ Parent, NewDebug,
+ %% Avoid infinite loop in initial state with state entry events
+ case maps:is_key(init_state, S) of
+ true ->
+ maps:remove(init_state, S);
+ false ->
+ S
+ end,
+ Q, NextState, NewData, P, Hibernate, Timeout).
%%---------------------------------------------------------------------------
%% Server helpers
@@ -1285,7 +1367,9 @@ terminate(
error_info(
Class, Reason, Stacktrace,
- #{name := Name, callback_mode := CallbackMode},
+ #{name := Name,
+ callback_mode := CallbackMode,
+ state_entry_events := SEEvents},
Q, P, FmtData) ->
{FixedReason,FixedStacktrace} =
case Stacktrace of
@@ -1312,6 +1396,13 @@ error_info(
end;
_ -> {Reason,Stacktrace}
end,
+ CBMode =
+ case SEEvents of
+ true ->
+ [CallbackMode,state_entry_events];
+ false ->
+ CallbackMode
+ end,
error_logger:format(
"** State machine ~p terminating~n" ++
case Q of
@@ -1338,8 +1429,9 @@ error_info(
[] -> [];
[Event|_] -> [Event]
end] ++
- [FmtData,Class,FixedReason,
- CallbackMode] ++
+ [FmtData,
+ Class,FixedReason,
+ CBMode] ++
case Q of
[_|[_|_] = Events] -> [Events];
_ -> []
diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl
index e092940174..eef8f265c4 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, event_order, code_change,
+ shutdown, stop_and_reply, enter_events, event_order, code_change,
{group, sys},
hibernate, enter_loop].
@@ -556,7 +556,8 @@ stop_and_reply(_Config) ->
{stop_and_reply,Reason,
[R1,{reply,From2,Reply2}]}
end},
- {ok,STM} = gen_statem:start_link(?MODULE, {map_statem,Machine}, []),
+ {ok,STM} =
+ gen_statem:start_link(?MODULE, {map_statem,Machine,[]}, []),
Self = self(),
Tag1 = make_ref(),
@@ -581,6 +582,61 @@ stop_and_reply(_Config) ->
+enter_events(_Config) ->
+ process_flag(trap_exit, true),
+ Self = self(),
+
+ Machine =
+ %% Abusing the internal format of From...
+ #{init =>
+ fun () ->
+ {ok,start,1}
+ end,
+ start =>
+ fun (enter, Prev, N) ->
+ Self ! {enter,start,Prev,N},
+ {keep_state,N + 1};
+ (internal, Prev, N) ->
+ Self ! {internal,start,Prev,N},
+ {keep_state,N + 1};
+ ({call,From}, echo, N) ->
+ {next_state,wait,N + 1,{reply,From,{echo,start,N}}};
+ ({call,From}, {stop,Reason}, N) ->
+ {stop_and_reply,Reason,[{reply,From,{stop,N}}],N + 1}
+ end,
+ wait =>
+ fun (enter, Prev, N) ->
+ Self ! {enter,wait,Prev,N},
+ {keep_state,N + 1};
+ ({call,From}, echo, N) ->
+ {next_state,start,N + 1,
+ [{next_event,internal,wait},
+ {reply,From,{echo,wait,N}}]}
+ end},
+ {ok,STM} =
+ gen_statem:start_link(
+ ?MODULE, {map_statem,Machine,[state_entry_events]}, []),
+
+ [{enter,start,start,1}] = flush(),
+ {echo,start,2} = gen_statem:call(STM, echo),
+ [{enter,wait,start,3}] = flush(),
+ {wait,[4|_]} = sys:get_state(STM),
+ {echo,wait,4} = gen_statem:call(STM, echo),
+ [{enter,start,wait,5},{internal,start,wait,6}] = flush(),
+ {stop,7} = gen_statem:call(STM, {stop,bye}),
+ [{'EXIT',STM,bye}] = flush(),
+
+ {noproc,_} =
+ ?EXPECT_FAILURE(gen_statem:call(STM, hej), Reason),
+ case flush() of
+ [] ->
+ ok;
+ Other2 ->
+ ct:fail({unexpected,Other2})
+ end.
+
+
+
event_order(_Config) ->
process_flag(trap_exit, true),
@@ -623,7 +679,7 @@ event_order(_Config) ->
Result
end},
- {ok,STM} = gen_statem:start_link(?MODULE, {map_statem,Machine}, []),
+ {ok,STM} = gen_statem:start_link(?MODULE, {map_statem,Machine,[]}, []),
Self = self(),
Tag1 = make_ref(),
gen_statem:cast(STM, {reply,{Self,Tag1},ok1}),
@@ -1315,9 +1371,9 @@ init({callback_mode,CallbackMode,Arg}) ->
ets:new(?MODULE, [named_table,private]),
ets:insert(?MODULE, {callback_mode,CallbackMode}),
init(Arg);
-init({map_statem,#{init := Init}=Machine}) ->
+init({map_statem,#{init := Init}=Machine,Modes}) ->
ets:new(?MODULE, [named_table,private]),
- ets:insert(?MODULE, {callback_mode,handle_event_function}),
+ ets:insert(?MODULE, {callback_mode,[handle_event_function|Modes]}),
case Init() of
{ok,State,Data,Ops} ->
{ok,State,[Data|Machine],Ops};