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(-) 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