[erlang-questions] gen_statem: next event internal and reply
Raimo Niskanen
raimo+erlang-questions@REDACTED
Wed Jul 31 12:00:14 CEST 2019
On Wed, Jul 31, 2019 at 09:41:07AM +0100, Peter Morgan wrote:
> Hi,
>
> I regularly use the following pattern in gen_statem:
>
> handle_event({call, From}, a, _, _) ->
> {keep_state_and_data, [{next_event, internal, work}, {reply, From, ok}]};
>
> Where `work` is some side effect that needs to be done, and may be used by several other actions (hence wrapping it in a next_event) but the result is unimportant and doesn’t affect the reply. I use next_event rather than calling work() directly because it can be traced via sys.
>
> My assumption was that work would be processed before the reply was sent to the caller - i.e., that the actions were processed in order.
>
> Instead, it appears that the reply action is processed immediately?
>
> To work around this I have started using:
>
> handle_event({call, From}, a, _, _) ->
> {keep_state_and_data, [{next_event, internal, work}, {next_event, internal, {reply, From, ok}]}};
> handle_event(internal, {reply, From, Result}, _, _) ->
> {keep_state_and_data, {reply, From, Result}}.
>
> The above ensures that work is completed before the reply is sent (result of the work is a side-effect here) - but is a rather ugly solution.
>
> Is the above intended behaviour? In some cases the `work` event causes state changes, or queueing subsequent other work (via next_event, internal) that was expected to be completed before the {reply, From, ok} was consumed.
This is by design, intentional and documented (somewhat convoluted) in:
http://erlang.org/doc/man/gen_statem.html#type-transition_option
http://erlang.org/doc/man/gen_statem.html#type-action
http://erlang.org/doc/man/gen_statem.html#type-enter_action
http://erlang.org/doc/man/gen_statem.html#type-reply_action
The {next_event, Type, Content} transition action inserts an event
to be handled during later event handler invocations.
{reply, From, Reply} immediately sends a reply during the state transition
hence before the next event handler invocation.
Most of the transition actions set options for the state transition, and
how these options affect the state transition is described in the first
link above:
http://erlang.org/doc/man/gen_statem.html#type-transition_option
The actions are processed in order, but that is far from the whole truth.
The only transition action that immediately does something for which the
order matters is {reply,,}, the others just set transition options.
Order also matters between {next_event,,} actions since their inserted
events will be handled in order.
Your assumption that a {next_event,,,} action should be processed before a
{reply,,} action would also be very confusing: if handling of the
inserted event also would use {reply,,}; what would the order of the
replies be? Especially if both event handler invocations would insert
multiple events and generate multiple replies...
Then add state enter calls to that mix.
I think your solution is logical and only moderatly ugly. Would it be
possible to instead have the handler of (internal, work) decide on sending
the reply based on some other state data instead of having to generate
two internal events?
Best Regards
/ Raimo Niskanen
>
> The above ordering of actions assumption caught me out recently, the early reply was causing a process to be killed before it had actually completed its work causing an unclean shutdown in a system.
>
> BTW the additional of log is very useful in failure analysis - what did this statem recently do? :) thanks!
>
>
> Thanks,
> Peter.
>
> Full example:
>
> -module(exaple_statem_reply).
>
> -behaviour(gen_statem).
> -export([a/0]).
> -export([b/0]).
> -export([callback_mode/0]).
> -export([handle_event/4]).
> -export([init/1]).
> -export([start/0]).
> -export([stop/1]).
> -include_lib("kernel/include/logger.hrl").
>
> start() ->
> gen_statem:start({local, ?MODULE},
> ?MODULE,
> [],
> [{debug, [log, trace]}]).
>
> a() ->
> gen_statem:call(?MODULE, ?FUNCTION_NAME).
>
> b() ->
> gen_statem:call(?MODULE, ?FUNCTION_NAME).
>
> stop(S) ->
> gen_statem:stop(S).
>
> init([]) ->
> {ok, limbo, #{}}.
>
>
> callback_mode() ->
> handle_event_function.
>
> handle_event({call, From}, a, _, _) ->
> {keep_state_and_data, [nei(work), {reply, From, ok}]};
>
> handle_event({call, From}, b, _, _) ->
> {keep_state_and_data, [nei(work), nei({reply, From, ok})]};
>
> handle_event(internal, work, _, _) ->
> keep_state_and_data;
>
> handle_event(internal, {reply, From, Result}, _, _) ->
> {keep_state_and_data, {reply, From, Result}}.
>
> nei(Event) ->
> {next_event, internal, Event}.
>
>
> 1> exaple_statem_reply:start().
> *DBG* exaple_statem_reply enter in state limbo
> *DBG* exaple_statem_reply consume internal init_state in state limbo
> {ok,<0.161.0>}
> 2> exaple_statem_reply:a().
> *DBG* exaple_statem_reply receive call a from <0.78.0> in state limbo
> *DBG* exaple_statem_reply receive internal work in state limbo
> *DBG* exaple_statem_reply send ok to <0.78.0>
> *DBG* exaple_statem_reply consume call a from <0.78.0> in state limbo
> ok
> *DBG* exaple_statem_reply consume internal work in state limbo
> 3> sys:get_status(exaple_statem_reply).
> {status,<0.161.0>,
> {module,gen_statem},
> [[{'$ancestors',[<0.78.0>]},
> {'$initial_call',{exaple_statem_reply,init,1}}],
> running,<0.161.0>,
> [{trace,true},
> {log,[3,
> {{consume,{internal,work},limbo,limbo},
> exaple_statem_reply,#Fun<gen_statem.1.50707408>},
> {{consume,{{call,{<0.78.0>,
> #Ref<0.2400559651.2919759873.28633>}},
> a},
> limbo,limbo},
> exaple_statem_reply,#Fun<gen_statem.1.50707408>},
> {{out,ok,{<0.78.0>,#Ref<0.2400559651.2919759873.28633>}},
> exaple_statem_reply,#Fun<gen_statem.1.50707408>},
> {{in,{internal,work},limbo},
> exaple_statem_reply,#Fun<gen_statem.1.50707408>},
> {{in,{{call,{<0.78.0>,#Ref<0.2400559651.2919759873.28633>}},
> a},
> limbo},
> exaple_statem_reply,#Fun<gen_statem.1.50707408>},
> {{consume,{internal,init_state},limbo,limbo},
> exaple_statem_reply,#Fun<gen_statem.1.50707408>},
> {{enter,limbo},
> exaple_statem_reply,#Fun<gen_statem.1.50707408>}]}],
> [{header,"Status for state machine exaple_statem_reply"},
> {data,[{"Status",running},
> {"Parent",<0.161.0>},
> {"Logged Events",
> [{enter,limbo},
> {consume,{internal,init_state},limbo,limbo},
> {in,{{call,{<0.78.0>,#Ref<0.2400559651.2919759873.28633>}},
> a},
> limbo},
> {in,{internal,work},limbo},
> {out,ok,{<0.78.0>,#Ref<0.2400559651.2919759873.28633>}},
> {consume,{{call,{...}},a},limbo,limbo},
> {consume,{internal,work},limbo,limbo}]},
> {"Postponed",[]}]},
> {data,[{"State",{limbo,#{}}}]}]]}
> 4>
>
>
> _______________________________________________
> erlang-questions mailing list
> erlang-questions@REDACTED
> http://erlang.org/mailman/listinfo/erlang-questions
--
/ Raimo Niskanen, Erlang/OTP, Ericsson AB
More information about the erlang-questions
mailing list