@@ -1362,7 +1475,8 @@ handle_event(_, _, State, Data) ->
once after server start and after code change,
but before the first
state function
- is called. More occasions may be added in future versions
+ in the current code version is called.
+ More occasions may be added in future versions
of gen_statem.
@@ -1380,7 +1494,7 @@ handle_event(_, _, State, Data) ->
or a list containing
callback_mode()
and possibly the atom
- state_entry_events.
+ state_enter.
@@ -1601,7 +1715,8 @@ handle_event(_, _, State, Data) ->
The function is to return Status, a term that
- changes the details of the current state and status of
+ contains the appropriate 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
@@ -1625,11 +1740,17 @@ handle_event(_, _, State, Data) ->
+ Module:StateName(enter, OldState, Data) ->
+ StateFunctionEnterResult
+
Module:StateName(EventType, EventContent, Data) ->
StateFunctionResult
- Module:handle_event(EventType, EventContent,
- State, Data) -> HandleEventResult
+ Module:handle_event(enter, OldState, State, Data) ->
+ HandleEventResult
+
+ Module:handle_event(EventType, EventContent, State, Data) ->
+ HandleEventResult
Handle an event.
@@ -1650,10 +1771,18 @@ handle_event(_, _, State, Data) ->
StateFunctionResult =
state_function_result()
+
+ StateFunctionEnterResult =
+ state_function_enter_result()
+
HandleEventResult =
handle_event_result()
+
+ HandleEventEnterResult =
+ handle_event_enter_result()
+
@@ -1694,6 +1823,24 @@ handle_event(_, _, State, Data) ->
by gen_statem after returning from this function,
see action().
+
+ When the gen_statem runs with
+ state enter calls,
+ these functions are also called with arguments
+ (enter, OldState, ...) whenever the state changes.
+ In this case there are some restrictions on the
+ actions
+ that may be returned:
+ postpone()
+ and
+ {next_event,_,_}
+ are not allowed.
+ You may also not change states from this call.
+ Should you return {next_state,NextState, ...}
+ with NextState =/= State the gen_statem crashes.
+ You are advised to use {keep_state,...} or
+ keep_state_and_data.
+
Note the fact that you can use
throw
diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl
index 7f437404ed..aedcfc932f 100644
--- a/lib/stdlib/src/gen_statem.erl
+++ b/lib/stdlib/src/gen_statem.erl
@@ -47,13 +47,16 @@
%% Type exports for templates
-export_type(
[event_type/0,
- callback_mode/0,
+ state_name/0,
+ callback_mode_result/0,
state_function_result/0,
+ state_function_enter_result/0,
handle_event_result/0,
+ handle_event_enter_result/0,
action/0]).
%% Fix problem for doc build
--export_type([state_entry_mode/0,transition_option/0]).
+-export_type([transition_option/0]).
%%%==========================================================================
%%% Interface functions.
@@ -72,10 +75,12 @@
-type event_type() ::
{'call',From :: from()} | 'cast' |
- 'info' | 'timeout' | 'enter' | 'internal'.
+ 'info' | 'timeout' | 'internal'.
+-type callback_mode_result() ::
+ callback_mode() | [callback_mode() | state_enter()].
-type callback_mode() :: 'state_functions' | 'handle_event_function'.
--type state_entry_mode() :: 'state_entry_events'.
+-type state_enter() :: 'state_enter'.
-type transition_option() ::
postpone() | hibernate() | event_timeout().
@@ -109,6 +114,14 @@
'postpone' | % Set the postpone option
{'postpone', Postpone :: postpone()} |
%%
+ %% 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()} |
+ enter_action().
+-type enter_action() ::
'hibernate' | % Set the hibernate option
{'hibernate', Hibernate :: hibernate()} |
%%
@@ -116,14 +129,7 @@
{'timeout', % Set the event timeout option
Time :: event_timeout(), EventContent :: 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()}.
+ reply_action().
-type reply_action() ::
{'reply', % Reply to a caller
From :: from(), Reply :: term()}.
@@ -137,6 +143,16 @@
NewData :: data(),
Actions :: [action()] | action()} |
common_state_callback_result().
+-type state_function_enter_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 :: [enter_action()] | enter_action()} |
+ common_state_callback_result().
+
-type handle_event_result() ::
{'next_state', % {next_state,NextState,NewData,[]}
NextState :: state(),
@@ -146,6 +162,16 @@
NewData :: data(),
Actions :: [action()] | action()} |
common_state_callback_result().
+-type handle_event_enter_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 :: [enter_action()] | enter_action()} |
+ common_state_callback_result().
+
-type common_state_callback_result() ::
'stop' | % {stop,normal}
{'stop', % Stop the server
@@ -164,10 +190,10 @@
NewData :: data()} |
{'keep_state', % Keep state, change data
NewData :: data(),
- Actions :: [action()] | action()} |
+ Actions :: [ActionType] | ActionType} |
'keep_state_and_data' | % {keep_state_and_data,[]}
{'keep_state_and_data', % Keep state and data -> only actions
- Actions :: [action()] | action()}.
+ Actions :: [ActionType] | ActionType}.
%% The state machine init function. It is called only once and
@@ -184,9 +210,7 @@
%%
%% 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_mode() | state_entry_mode()].
+-callback callback_mode() -> callback_mode_result().
%% Example state callback for StateName = 'state_name'
%% when callback_mode() =:= state_functions.
@@ -197,7 +221,11 @@
%% StateName/3 callbacks and terminate/3, so the state name
%% 'terminate' is unusable in this mode.
-callback state_name(
- event_type(),
+ 'enter',
+ OldStateName :: state_name(),
+ Data :: data()) ->
+ state_function_enter_result();
+ (event_type(),
EventContent :: term(),
Data :: data()) ->
state_function_result().
@@ -205,7 +233,12 @@
%% State callback for all states
%% when callback_mode() =:= handle_event_function.
-callback handle_event(
- event_type(),
+ 'enter',
+ OldState :: state(),
+ State :: state(), % Current state
+ Data :: data()) ->
+ handle_event_enter_result();
+ (event_type(),
EventContent :: term(),
State :: state(), % Current state
Data :: data()) ->
@@ -547,7 +580,7 @@ enter(Module, Opts, State, Data, Server, Actions, Parent) ->
Name = gen:get_proc_name(Server),
Debug = gen:debug_options(Name, Opts),
P = Events = [],
- Event = {internal,initial_state},
+ Event = {internal,init_state},
%% We enforce {postpone,false} to ensure that
%% our fake Event gets discarded, thought it might get logged
NewActions =
@@ -559,7 +592,7 @@ enter(Module, Opts, State, Data, Server, Actions, Parent) ->
end,
S = #{
callback_mode => undefined,
- state_entry_events => false,
+ state_enter => false,
module => Module,
name => Name,
%% The rest of the fields are set from to the arguments to
@@ -886,51 +919,13 @@ loop_events_done(Parent, Debug, S, Timer, State, Data, P, Hibernate) ->
-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)
+ call_callback_mode_result(S, CallbackMode)
catch
CallbackMode ->
- call_callback_mode(S, CallbackMode);
+ call_callback_mode_result(S, CallbackMode);
error:undef ->
%% Process undef to check for the simple mistake
%% of calling a nonexistent state function
@@ -948,38 +943,57 @@ call_callback_mode(#{module := Module} = S) ->
{Class,Reason,erlang:get_stacktrace()}
end.
-loop_event(
- Parent, Debug,
+call_callback_mode_result(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,StateEnter} ->
+ {ok,
+ S#{
+ callback_mode := CBMode,
+ state_enter := StateEnter}}
+ end.
+
+parse_callback_mode([], CBMode, StateEnter) ->
+ {CBMode,StateEnter};
+parse_callback_mode([H|T], CBMode, StateEnter) ->
+ case callback_mode(H) of
+ true ->
+ parse_callback_mode(T, H, StateEnter);
+ false ->
+ case H of
+ state_enter ->
+ parse_callback_mode(T, CBMode, true);
+ _ ->
+ {undefined,StateEnter}
+ end
+ end;
+parse_callback_mode(_, _CBMode, StateEnter) ->
+ {undefined,StateEnter}.
+
+call_state_function(
#{callback_mode := undefined} = S,
- Events,
- State, Data, P, Event, Hibernate) ->
- %% This happens after code_change/4
+ Type, Content, State, Data) ->
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)
+ call_state_function(NewS, Type, Content, State, Data);
+ Error ->
+ Error
end;
-loop_event(
- Parent, Debug,
+call_state_function(
#{callback_mode := CallbackMode,
module := Module} = S,
- Events,
- State, Data, P, {Type,Content} = Event, Hibernate) ->
- %%
- %% If Hibernate is true here it can only be
- %% because it was set from an event action
- %% and we did not go into hibernation since there
- %% were events in queue, so we do what the user
- %% might depend on i.e collect garbage which
- %% would have happened if we actually hibernated
- %% and immediately was awakened
- Hibernate andalso garbage_collect(),
- %%
+ Type, Content, State, Data) ->
try
case CallbackMode of
state_functions ->
@@ -989,12 +1003,10 @@ loop_event(
end
of
Result ->
- loop_event_result(
- Parent, Debug, S, Events, State, Data, P, Event, Result)
+ {ok,Result,S}
catch
Result ->
- loop_event_result(
- Parent, Debug, S, Events, State, Data, P, Event, Result);
+ {ok,Result,S};
error:badarg ->
case erlang:get_stacktrace() of
[{erlang,apply,
@@ -1004,15 +1016,11 @@ loop_event(
when CallbackMode =:= state_functions ->
%% We get here e.g if apply fails
%% due to State not being an atom
- terminate(
- error,
- {undef_state_function,{Module,State,Args}},
- Stacktrace,
- Debug, S, [Event|Events], State, Data, P);
+ {error,
+ {undef_state_function,{Module,State,Args}},
+ Stacktrace};
Stacktrace ->
- terminate(
- error, badarg, Stacktrace,
- Debug, S, [Event|Events], State, Data, P)
+ {error,badarg,Stacktrace}
end;
error:undef ->
%% Process undef to check for the simple mistake
@@ -1022,34 +1030,54 @@ loop_event(
[{Module,State,[Type,Content,Data]=Args,_}
|Stacktrace]
when CallbackMode =:= state_functions ->
- terminate(
- error,
- {undef_state_function,{Module,State,Args}},
- Stacktrace,
- Debug, S, [Event|Events], State, Data, P);
+ {error,
+ {undef_state_function,{Module,State,Args}},
+ Stacktrace};
[{Module,handle_event,[Type,Content,State,Data]=Args,_}
|Stacktrace]
when CallbackMode =:= handle_event_function ->
- terminate(
- error,
- {undef_state_function,{Module,handle_event,Args}},
- Stacktrace,
- Debug, S, [Event|Events], State, Data, P);
+ {error,
+ {undef_state_function,{Module,handle_event,Args}},
+ Stacktrace};
Stacktrace ->
- terminate(
- error, undef, Stacktrace,
- Debug, S, [Event|Events], State, Data, P)
+ {error,undef,Stacktrace}
end;
Class:Reason ->
- Stacktrace = erlang:get_stacktrace(),
+ {Class,Reason,erlang:get_stacktrace()}
+ end.
+
+loop_event(
+ Parent, Debug, S, Events,
+ State, Data, P, {Type,Content} = Event, Hibernate) ->
+ %%
+ %% If Hibernate is true here it can only be
+ %% because it was set from an event action
+ %% and we did not go into hibernation since there
+ %% were events in queue, so we do what the user
+ %% might depend on i.e collect garbage which
+ %% would have happened if we actually hibernated
+ %% and immediately was awakened
+ Hibernate andalso garbage_collect(),
+ case call_state_function(S, Type, Content, State, Data) of
+ {ok,Result,NewS} ->
+ {NewData,NextState,Actions} =
+ parse_event_result(
+ Parent, Debug, NewS, Events,
+ State, Data, P, Event,
+ Result, true),
+ loop_event_actions(
+ Parent, Debug, S, Events,
+ State, NewData, P, Event, NextState, Actions);
+ {Class,Reason,Stacktrace} ->
terminate(
Class, Reason, 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) ->
+parse_event_result(
+ _Parent, Debug, S, Events, State, Data, P, Event,
+ Result, AllowStateChange) ->
case Result of
stop ->
terminate(
@@ -1073,30 +1101,22 @@ loop_event_result(
reply_then_terminate(
exit, Reason, ?STACKTRACE(),
Debug, S, Q, State, NewData, P, Replies);
- {next_state,NextState,NewData} ->
- loop_event_actions(
- Parent, Debug, S, Events,
- State, NewData, P, Event, NextState, []);
- {next_state,NextState,NewData,Actions} ->
- loop_event_actions(
- Parent, Debug, S, Events,
- State, NewData, P, Event, NextState, Actions);
+ {next_state,State,NewData} ->
+ {NewData,State,[]};
+ {next_state,NextState,NewData} when AllowStateChange ->
+ {NewData,NextState,[]};
+ {next_state,State,NewData,Actions} ->
+ {NewData,State,Actions};
+ {next_state,NextState,NewData,Actions} when AllowStateChange ->
+ {NewData,NextState,Actions};
{keep_state,NewData} ->
- loop_event_actions(
- Parent, Debug, S, Events,
- State, NewData, P, Event, State, []);
+ {NewData,State,[]};
{keep_state,NewData,Actions} ->
- loop_event_actions(
- Parent, Debug, S, Events,
- State, NewData, P, Event, State, Actions);
+ {NewData,State,Actions};
keep_state_and_data ->
- loop_event_actions(
- Parent, Debug, S, Events,
- State, Data, P, Event, State, []);
+ {Data,State,[]};
{keep_state_and_data,Actions} ->
- loop_event_actions(
- Parent, Debug, S, Events,
- State, Data, P, Event, State, Actions);
+ {Data,State,Actions};
_ ->
terminate(
error,
@@ -1105,134 +1125,178 @@ loop_event_result(
Debug, S, [Event|Events], State, Data, P)
end.
-loop_event_actions(
- Parent, Debug, S, Events, State, NewData, P, Event, NextState, Actions) ->
- Postpone = false, % Shall we postpone this event; boolean()
+parse_enter_actions(Debug, S, State, Actions, Hibernate, Timeout) ->
+ Postpone = forbidden,
+ NextEvents = forbidden,
+ parse_actions(
+ Debug, S, State, listify(Actions),
+ Hibernate, Timeout, Postpone, NextEvents).
+
+parse_actions(Debug, S, State, Actions) ->
+ Postpone = false,
Hibernate = false,
Timeout = undefined,
NextEvents = [],
- loop_event_actions(
- Parent, Debug, S, Events, State, NewData, P, Event, NextState,
- if
- is_list(Actions) ->
- Actions;
- true ->
- [Actions]
- end,
- Postpone, Hibernate, Timeout, NextEvents).
+ parse_actions(
+ Debug, S, State, listify(Actions),
+ Hibernate, Timeout, Postpone, NextEvents).
%%
-%% Process all actions
-loop_event_actions(
- Parent, Debug, S, Events,
- State, NewData, P, Event, NextState, [Action|Actions],
- Postpone, Hibernate, Timeout, NextEvents) ->
+parse_actions(
+ Debug, _S, _State, [], Hibernate, Timeout, Postpone, NextEvents) ->
+ {ok,Debug,Hibernate,Timeout,Postpone,NextEvents};
+parse_actions(
+ Debug, S, State, [Action|Actions],
+ Hibernate, Timeout, Postpone, NextEvents) ->
case Action of
%% Actual actions
{reply,From,Reply} ->
case from(From) of
true ->
NewDebug = do_reply(Debug, S, State, From, Reply),
- loop_event_actions(
- Parent, NewDebug, S, Events,
- State, NewData, P, Event, NextState, Actions,
- Postpone, Hibernate, Timeout, NextEvents);
+ parse_actions(
+ NewDebug, S, State, Actions,
+ Hibernate, Timeout, Postpone, NextEvents);
false ->
- terminate(
- error,
- {bad_action_from_state_function,Action},
- ?STACKTRACE(),
- Debug, S, [Event|Events], State, NewData, P)
- end;
- {next_event,Type,Content} ->
- case event_type(Type) of
- true ->
- NewDebug =
- sys_debug(Debug, S, State, {in,{Type,Content}}),
- loop_event_actions(
- Parent, NewDebug, S, Events,
- State, NewData, P, Event, NextState, Actions,
- Postpone, Hibernate, Timeout,
- [{Type,Content}|NextEvents]);
- false ->
- terminate(
- error,
- {bad_action_from_state_function,Action},
- ?STACKTRACE(),
- Debug, S, [Event|Events], State, NewData, P)
+ {error,
+ {bad_action_from_state_function,Action},
+ ?STACKTRACE()}
end;
%% Actions that set options
- {postpone,NewPostpone} when is_boolean(NewPostpone) ->
- loop_event_actions(
- Parent, Debug, S, Events,
- State, NewData, P, Event, NextState, Actions,
- NewPostpone, Hibernate, Timeout, NextEvents);
- {postpone,_} ->
- terminate(
- error,
- {bad_action_from_state_function,Action},
- ?STACKTRACE(),
- Debug, S, [Event|Events], State, NewData, P);
- postpone ->
- loop_event_actions(
- Parent, Debug, S, Events,
- State, NewData, P, Event, NextState, Actions,
- true, Hibernate, Timeout, NextEvents);
{hibernate,NewHibernate} when is_boolean(NewHibernate) ->
- loop_event_actions(
- Parent, Debug, S, Events,
- State, NewData, P, Event, NextState, Actions,
- Postpone, NewHibernate, Timeout, NextEvents);
+ parse_actions(
+ Debug, S, State, Actions,
+ NewHibernate, Timeout, Postpone, NextEvents);
{hibernate,_} ->
- terminate(
- error,
- {bad_action_from_state_function,Action},
- ?STACKTRACE(),
- Debug, S, [Event|Events], State, NewData, P);
+ {error,
+ {bad_action_from_state_function,Action},
+ ?STACKTRACE()};
hibernate ->
- loop_event_actions(
- Parent, Debug, S, Events,
- State, NewData, P, Event, NextState, Actions,
- Postpone, true, Timeout, NextEvents);
+ parse_actions(
+ Debug, S, State, Actions,
+ true, Timeout, Postpone, NextEvents);
{timeout,infinity,_} -> % Clear timer - it will never trigger
- loop_event_actions(
- Parent, Debug, S, Events,
- State, NewData, P, Event, NextState, Actions,
- Postpone, Hibernate, undefined, NextEvents);
+ parse_actions(
+ Debug, S, State, Actions,
+ Hibernate, undefined, Postpone, NextEvents);
{timeout,Time,_} = NewTimeout when is_integer(Time), Time >= 0 ->
- loop_event_actions(
- Parent, Debug, S, Events,
- State, NewData, P, Event, NextState, Actions,
- Postpone, Hibernate, NewTimeout, NextEvents);
+ parse_actions(
+ Debug, S, State, Actions,
+ Hibernate, NewTimeout, Postpone, NextEvents);
{timeout,_,_} ->
- terminate(
- error,
- {bad_action_from_state_function,Action},
- ?STACKTRACE(),
- Debug, S, [Event|Events], State, NewData, P);
+ {error,
+ {bad_action_from_state_function,Action},
+ ?STACKTRACE()};
infinity -> % Clear timer - it will never trigger
- loop_event_actions(
- Parent, Debug, S, Events,
- State, NewData, P, Event, NextState, Actions,
- Postpone, Hibernate, undefined, NextEvents);
+ parse_actions(
+ Debug, S, State, Actions,
+ Hibernate, undefined, Postpone, NextEvents);
Time when is_integer(Time), Time >= 0 ->
NewTimeout = {timeout,Time,Time},
- loop_event_actions(
- Parent, Debug, S, Events,
- State, NewData, P, Event, NextState, Actions,
- Postpone, Hibernate, NewTimeout, NextEvents);
+ parse_actions(
+ Debug, S, State, Actions,
+ Hibernate, NewTimeout, Postpone, NextEvents);
+ {postpone,NewPostpone}
+ when is_boolean(NewPostpone), Postpone =/= forbidden ->
+ parse_actions(
+ Debug, S, State, Actions,
+ Hibernate, Timeout, NewPostpone, NextEvents);
+ {postpone,_} ->
+ {error,
+ {bad_action_from_state_function,Action},
+ ?STACKTRACE()};
+ postpone when Postpone =/= forbidden ->
+ parse_actions(
+ Debug, S, State, Actions,
+ Hibernate, Timeout, true, NextEvents);
+ {next_event,Type,Content} ->
+ case event_type(Type) of
+ true when NextEvents =/= forbidden ->
+ NewDebug =
+ sys_debug(Debug, S, State, {in,{Type,Content}}),
+ parse_actions(
+ NewDebug, S, State, Actions,
+ Hibernate, Timeout, Postpone,
+ [{Type,Content}|NextEvents]);
+ _ ->
+ {error,
+ {bad_action_from_state_function,Action},
+ ?STACKTRACE()}
+ end;
_ ->
+ {error,
+ {bad_action_from_state_function,Action},
+ ?STACKTRACE()}
+ end.
+
+loop_event_actions(
+ Parent, Debug, #{state_enter := StateEnter} = S, Events,
+ State, NewData, P, Event, NextState, Actions) ->
+ case parse_actions(Debug, S, State, Actions) of
+ {ok,NewDebug,Hibernate,Timeout,Postpone,NextEvents} ->
+ case
+ StateEnter andalso
+ ((NextState =/= State)
+ orelse maps:is_key(init_state, S)) of
+ true ->
+ loop_event_enter(
+ Parent, NewDebug, S, Events,
+ State, NewData, P, Event, NextState,
+ Hibernate, Timeout, Postpone, NextEvents);
+ false ->
+ loop_event_result(
+ Parent, NewDebug, S, Events,
+ State, NewData, P, Event, NextState,
+ Hibernate, Timeout, Postpone, NextEvents)
+ end;
+ {Class,Reason,Stacktrace} ->
terminate(
- error,
- {bad_action_from_state_function,Action},
- ?STACKTRACE(),
+ Class, Reason, Stacktrace,
Debug, S, [Event|Events], State, NewData, P)
- end;
-%%
-%% End of actions list
-loop_event_actions(
- Parent, Debug, #{state_entry_events := SEEvents} = S, Events,
- State, NewData, P0, Event, NextState, [],
- Postpone, Hibernate, Timeout, NextEvents) ->
+ end.
+
+loop_event_enter(
+ Parent, Debug, S, Events,
+ State, NewData, P, Event, NextState,
+ Hibernate, Timeout, Postpone, NextEvents) ->
+ case call_state_function(S, enter, State, NextState, NewData) of
+ {ok,Result,NewS} ->
+ {NewerData,_,Actions} =
+ parse_event_result(
+ Parent, Debug, NewS, Events,
+ NextState, NewData, P, Event,
+ Result, false),
+ loop_event_enter_actions(
+ Parent, Debug, NewS, Events,
+ State, NewerData, P, Event, NextState,
+ Hibernate, Timeout, Postpone, NextEvents, Actions);
+ {Class,Reason,Stacktrace} ->
+ terminate(
+ Class, Reason, Stacktrace,
+ Debug, S, [Event|Events], NextState, NewData, P)
+ end.
+
+loop_event_enter_actions(
+ Parent, Debug, S, Events,
+ State, NewData, P, Event, NextState,
+ Hibernate, Timeout, Postpone, NextEvents, Actions) ->
+ case
+ parse_enter_actions(Debug, S, NextState, Actions, Hibernate, Timeout)
+ of
+ {ok,NewDebug,NewHibernate,NewTimeout,_,_} ->
+ loop_event_result(
+ Parent, NewDebug, S, Events,
+ State, NewData, P, Event, NextState,
+ NewHibernate, NewTimeout, Postpone, NextEvents);
+ {Class,Reason,Stacktrace} ->
+ terminate(
+ Class, Reason, Stacktrace,
+ Debug, S, [Event|Events], NextState, NewData, P)
+ end.
+
+loop_event_result(
+ Parent, Debug, S, Events,
+ State, NewData, P0, Event, NextState,
+ Hibernate, Timeout, Postpone, NextEvents) ->
%%
%% All options have been collected and next_events are buffered.
%% Do the actual state transition.
@@ -1252,44 +1316,21 @@ loop_event_actions(
{lists:reverse(P1, Events),[]}
end,
%% Place next events first in queue
- 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,
+ Q = lists:reverse(NextEvents, Q2),
%%
NewDebug =
sys_debug(
Debug, S, State,
case Postpone of
true ->
- {postpone,Event,NextState};
+ {postpone,Event,State};
false ->
- {consume,Event,NextState}
+ {consume,Event,State}
end),
loop_events(
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,
+ maps:remove(init_state, S),
Q, NextState, NewData, P, Hibernate, Timeout).
%%---------------------------------------------------------------------------
@@ -1369,7 +1410,7 @@ error_info(
Class, Reason, Stacktrace,
#{name := Name,
callback_mode := CallbackMode,
- state_entry_events := SEEvents},
+ state_enter := StateEnter},
Q, P, FmtData) ->
{FixedReason,FixedStacktrace} =
case Stacktrace of
@@ -1397,9 +1438,9 @@ error_info(
_ -> {Reason,Stacktrace}
end,
CBMode =
- case SEEvents of
+ case StateEnter of
true ->
- [CallbackMode,state_entry_events];
+ [CallbackMode,state_enter];
false ->
CallbackMode
end,
@@ -1471,3 +1512,8 @@ format_status_default(Opt, State, Data) ->
_ ->
[{data,[{"State",StateData}]}]
end.
+
+listify(Item) when is_list(Item) ->
+ Item;
+listify(Item) ->
+ [Item].
diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl
index eef8f265c4..48f93b1de7 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, enter_events, event_order, code_change,
+ shutdown, stop_and_reply, state_enter, event_order, code_change,
{group, sys},
hibernate, enter_loop].
@@ -582,7 +582,7 @@ stop_and_reply(_Config) ->
-enter_events(_Config) ->
+state_enter(_Config) ->
process_flag(trap_exit, true),
Self = self(),
@@ -615,7 +615,7 @@ enter_events(_Config) ->
end},
{ok,STM} =
gen_statem:start_link(
- ?MODULE, {map_statem,Machine,[state_entry_events]}, []),
+ ?MODULE, {map_statem,Machine,[state_enter]}, []),
[{enter,start,start,1}] = flush(),
{echo,start,2} = gen_statem:call(STM, echo),
diff --git a/lib/tools/emacs/erlang-skels.el b/lib/tools/emacs/erlang-skels.el
index 0284c9d686..95cc989c73 100644
--- a/lib/tools/emacs/erlang-skels.el
+++ b/lib/tools/emacs/erlang-skels.el
@@ -904,7 +904,7 @@ Please see the function `tempo-define-template'.")
"%% @doc" n
"%% Define the callback_mode() for this callback module." n
(erlang-skel-separator-end 2)
- "-spec callback_mode() -> gen_statem:callback_mode()." n
+ "-spec callback_mode() -> gen_statem:callback_mode_result()." n
"callback_mode() -> state_functions." n
n
(erlang-skel-separator-start 2)
@@ -936,6 +936,10 @@ Please see the function `tempo-define-template'.")
"%% instead of StateName/3 functions like this!" n
(erlang-skel-separator-end 2)
"-spec state_name(" n>
+ "'enter', OldState :: gen_statem:state_name()," n>
+ "Data :: term()) ->" n>
+ "gen_statem:state_function_enter_result();" n
+ " (" n>
"gen_statem:event_type(), Msg :: term()," n>
"Data :: term()) ->" n>
"gen_statem:state_function_result()." n
@@ -1015,7 +1019,7 @@ Please see the function `tempo-define-template'.")
"%% @doc" n
"%% Define the callback_mode() for this callback module." n
(erlang-skel-separator-end 2)
- "-spec callback_mode() -> gen_statem:callback_mode()." n
+ "-spec callback_mode() -> gen_statem:callback_mode_result()." n
"callback_mode() -> handle_event_function." n
n
(erlang-skel-separator-start 2)
@@ -1044,6 +1048,10 @@ Please see the function `tempo-define-template'.")
"%% StateName/3 functions are called instead!" n
(erlang-skel-separator-end 2)
"-spec handle_event(" n>
+ "'enter', OldState :: term()," n>
+ "State :: term(), Data :: term()) ->" n>
+ "gen_statem:handle_event_enter_result();" n
+ " (" n>
"gen_statem:event_type(), Msg :: term()," n>
"State :: term(), Data :: term()) ->" n>
"gen_statem:handle_event_result()." n
diff --git a/system/doc/design_principles/statem.xml b/system/doc/design_principles/statem.xml
index 565b0e5274..d2a9b23570 100644
--- a/system/doc/design_principles/statem.xml
+++ b/system/doc/design_principles/statem.xml
@@ -195,21 +195,19 @@ handle_event(EventType, EventContent, State, Data) ->
-
- State Entry Events
+
+ State Enter Calls
The gen_statem behavior can regardless of callback mode
- automatically generate an
-
- event whenever the state changes
-
- so you can write state entry code
+ automatically call the state function
+ with special arguments whenever the state changes
+ so you can write state entry actions
near the rest of the state transition rules.
It typically looks like this:
StateName(enter, _OldState, Data) ->
- ... code for state entry here ...
+ ... code for state entry actions here ...
{keep_state, NewData};
StateName(EventType, EventContent, Data) ->
... code for actions here ...
@@ -217,7 +215,7 @@ StateName(EventType, EventContent, Data) ->
Depending on how your state machine is specified,
this can be a very useful feature, but if you use it
- you will have to handle the state entry events in all states.
+ you will have to handle the state enter call in all states.
@@ -751,12 +749,6 @@ stop() ->
Generated by any regular process message sent to
the gen_statem process.
- enter
- -
- Generated by a state transition with
- OldState =/= NewState when running with
- state entry events.
-
timeout
-
Generated by state transition action
@@ -972,63 +964,35 @@ do_unlock() ->
- Self-Generated Events
-
- It can sometimes be beneficial to be able to generate events
- to your own state machine.
- This can be done with the state transition
- action
- {next_event,EventType,EventContent}.
-
+ State Entry Actions
- You can generate events of any existing
- type,
- but the internal type can only be generated through action
- next_event. Hence, it cannot come from an external source,
- so you can be certain that an internal event is an event
- from your state machine to itself.
+ Say you have a state machine specification
+ that uses state entry actions.
+ Allthough you can code this using self-generated events
+ (described in the next section), especially if just
+ one or a few states has got state entry actions,
+ this is a perfect use case for the built in
+ state enter calls.
- One example for this is to pre-process incoming data, for example
- decrypting chunks or collecting characters up to a line break.
- This could be modelled with a separate state machine that sends
- the pre-processed events to the main state machine, or to decrease
- overhead the small pre-processing state machine can be implemented
- in the common state event handling of the main state machine
- using a few state data variables and then send the pre-processed
- events as internal events to the main state machine.
-
-
- Another example of using self-generated events can be when you have
- a state machine specification that uses state entry actions.
- You can code that using a dedicated function
- to do the state transition. But if you want that code to be
- visible besides the other state logic, you can insert
- an internal event that does the entry actions.
- This has the same unfortunate consequence as using
- state transition functions: everywhere you go to
- the state, you must explicitly
- insert the internal event
- or use a state transition function.
- This is something that can be forgotten, and if you find that
- annoying please look at the next chapter.
-
-
- The following is an implementation of entry actions
- using internal events with content enter
- using a helper function enter/3 for state entry:
+ You return a list containing state_enter from your
+ callback_mode/0
+ function and the gen_statem engine will call your
+ state function once with the arguments
+ (enter, OldState, ...) whenever the state changes.
+ Then you just need to handle these event-like calls in all states.
process_flag(trap_exit, true),
Data = #{code => Code},
- enter(ok, locked, Data).
+ {ok, locked, Data}.
callback_mode() ->
- state_functions.
+ [state_functions,state_enter].
-locked(internal, enter, Data) ->
+locked(enter, _OldState, Data) ->
do_lock(),
{keep_state,Data#{remaining => Code}};
locked(
@@ -1036,79 +1000,94 @@ locked(
#{code := Code, remaining := Remaining} = Data) ->
case Remaining of
[Digit] ->
- enter(next_state, open, Data);
+ {next_state, open, Data};
...
-open(internal, enter, Data) ->
+open(enter, _OldState, Data) ->
Tref = erlang:start_timer(10000, self(), lock),
do_unlock(),
{keep_state,Data#{timer => Tref}};
open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->
- enter(next_state, locked, Data);
+ {next_state, locked, Data};
...
-
-enter(Tag, State, Data) ->
- {Tag,State,Data,[{next_event,internal,enter}]}.
]]>
- Using State Entry Events
+ Self-Generated Events
- Here is the same example as the previous but instead using
- the built in
- state entry events.
+ It can sometimes be beneficial to be able to generate events
+ to your own state machine.
+ This can be done with the state transition
+ action
+ {next_event,EventType,EventContent}.
- Since the state entry events are unconditionally inserted by
- the gen_statem engine you can not forget to insert them
- yourself and you will have to handle the state entry events
- in every state.
+ You can generate events of any existing
+ type,
+ but the internal type can only be generated through action
+ next_event. Hence, it cannot come from an external source,
+ so you can be certain that an internal event is an event
+ from your state machine to itself.
- If you want state entry code in just a few states the previous
- example may be more suitable, especially to only send internal
- events when entering just those few states.
- Note: additional discipline will be required.
+ One example for this is to pre-process incoming data, for example
+ decrypting chunks or collecting characters up to a line break.
+ Purists may argue that this should be modelled with a separate
+ state machine that sends pre-processed events
+ to the main state machine.
+ But to decrease overhead the small pre-processing state machine
+ can be implemented in the common state event handling
+ of the main state machine using a few state data variables
+ that then sends the pre-processed events as internal events
+ to the main state machine.
- You can also in the previous example choose to generate
- events looking just like the events you get from using
- state entry events.
- This may be confusing, or practical,
- depending on your point of view.
+ The following example use an input model where you give the lock
+ characters with put_chars(Chars) and then call
+ enter() to finish the input.
- process_flag(trap_exit, true),
- Data = #{code => Code},
- {ok, locked, Data}.
+-export(put_chars/1, enter/0).
+...
+put_chars(Chars) when is_binary(Chars) ->
+ gen_statem:call(?NAME, {chars,Chars}).
-callback_mode() ->
- [state_functions,state_entry_events].
+enter() ->
+ gen_statem:call(?NAME, enter).
+
+...
locked(enter, _OldState, Data) ->
do_lock(),
- {keep_state,Data#{remaining => Code}};
-locked(
- cast, {button,Digit},
- #{code := Code, remaining := Remaining} = Data) ->
- case Remaining of
- [Digit] ->
- {next_state, open, Data};
+ {keep_state,Data#{remaining => Code, buf => []}};
...
-open(enter, _OldState, Data) ->
- Tref = erlang:start_timer(10000, self(), lock),
- do_unlock(),
- {keep_state,Data#{timer => Tref}};
-open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->
- {next_state, locked, Data};
+handle_event({call,From}, {chars,Chars}, #{buf := Buf} = Data) ->
+ {keep_state, Data#{buf := [Chars|Buf],
+ [{reply,From,ok}]};
+handle_event({call,From}, enter, #{buf := Buf} = Data) ->
+ Chars = unicode:characters_to_binary(lists:reverse(Buf)),
+ try binary_to_integer(Chars) of
+ Digit ->
+ {keep_state, Data#{buf := []},
+ [{reply,From,ok},
+ {next_event,internal,{button,Chars}}]}
+ catch
+ error:badarg ->
+ {keep_state, Data#{buf := []},
+ [{reply,From,{error,not_an_integer}}]}
+ end;
...
]]>
+
+ If you start this program with code_lock:start([17])
+ you can unlock with code_lock:put_chars(<<"001">>),
+ code_lock:put_chars(<<"7">>), code_lock:enter().
+
@@ -1117,7 +1096,7 @@ open(info, {timeout,Tref,lock}, #{timer := Tref} = Data) ->
Example Revisited
This section includes the example after all mentioned modifications
- and some more using the entry actions,
+ and some more using state enter calls,
which deserves a new state diagram:
@@ -1163,7 +1142,7 @@ init(Code) ->
{ok, locked, Data}.
callback_mode() ->
- [state_functions,state_entry_events].
+ [state_functions,state_enter].
locked(enter, _OldState, #{code := Code} = Data) ->
do_lock(),
@@ -1215,8 +1194,7 @@ code_change(_Vsn, State, Data, _Extra) ->
This section describes what to change in the example
to use one handle_event/4 function.
The previously used approach to first branch depending on event
- does not work that well here because of
- the state entry events,
+ does not work that well here because of the state enter calls,
so this example first branches depending on state:
...
callback_mode() ->
- [handle_event_function,state_entry_events].
+ [handle_event_function,state_enter].
%% State: locked
handle_event(enter, _OldState, locked, #{code := Code} = Data) ->
@@ -1413,7 +1391,7 @@ init({Code,LockButton}) ->
{ok, {locked,LockButton}, Data}.
callback_mode() ->
- [handle_event_function,state_entry_events].
+ [handle_event_function,state_enter].
handle_event(
{call,From}, {set_lock_button,NewLockButton},
--
cgit v1.2.3