gen_statem: next event internal and reply

classic Classic list List threaded Threaded
7 messages Options
Reply | Threaded
Open this post in threaded view
|

gen_statem: next event internal and reply

Peter Morgan
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.

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
[hidden email]
http://erlang.org/mailman/listinfo/erlang-questions
Reply | Threaded
Open this post in threaded view
|

Re: gen_statem: next event internal and reply

Jesper Louis Andersen-2
It is documented behaviour for next_event:

This action does not set any transition_option() but instead stores the specified EventType and EventContent for insertion after all actions have been executed.  

My guess is that replies are sent as early as possible because this exposes concurrency to the maximum.

I would also be very cautious around relying on orderings like these in a system since that is where concurrency bugs are hidden.


On Wed, Jul 31, 2019 at 10:41 AM Peter Morgan <[hidden email]> 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.

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
[hidden email]
http://erlang.org/mailman/listinfo/erlang-questions


--
J.

_______________________________________________
erlang-questions mailing list
[hidden email]
http://erlang.org/mailman/listinfo/erlang-questions
Reply | Threaded
Open this post in threaded view
|

Re: gen_statem: next event internal and reply

Peter Morgan
Thanks - In the back of my head I thought it was the case, but couldn't find that sentence. Log proved useful to find the cause.

Regards,
Peter.

On 31 Jul 2019, at 10:16, Jesper Louis Andersen <[hidden email]> wrote:

It is documented behaviour for next_event:

This action does not set any transition_option() but instead stores the specified EventType and EventContent for insertion after all actions have been executed.  

My guess is that replies are sent as early as possible because this exposes concurrency to the maximum.

I would also be very cautious around relying on orderings like these in a system since that is where concurrency bugs are hidden.


On Wed, Jul 31, 2019 at 10:41 AM Peter Morgan <[hidden email]> 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.

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
[hidden email]
http://erlang.org/mailman/listinfo/erlang-questions


--
J.


_______________________________________________
erlang-questions mailing list
[hidden email]
http://erlang.org/mailman/listinfo/erlang-questions
Reply | Threaded
Open this post in threaded view
|

Re: gen_statem: next event internal and reply

Raimo Niskanen-11
In reply to this post by Peter Morgan
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
> [hidden email]
> http://erlang.org/mailman/listinfo/erlang-questions


--

/ Raimo Niskanen, Erlang/OTP, Ericsson AB
_______________________________________________
erlang-questions mailing list
[hidden email]
http://erlang.org/mailman/listinfo/erlang-questions
Reply | Threaded
Open this post in threaded view
|

Re: gen_statem: next event internal and reply

Raimo Niskanen-11
I have checked in a branch in the daily builds that clarifies
the documentation for transition_option() to be more explicit
about when replies are sent, and some other stuff.

/ Raimo


On Wed, Jul 31, 2019 at 12:00:14PM +0200, Raimo Niskanen wrote:

> 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
> > [hidden email]
> > http://erlang.org/mailman/listinfo/erlang-questions
>
>
> --
>
> / Raimo Niskanen, Erlang/OTP, Ericsson AB
> _______________________________________________
> erlang-questions mailing list
> [hidden email]
> http://erlang.org/mailman/listinfo/erlang-questions

--

/ Raimo Niskanen, Erlang/OTP, Ericsson AB
_______________________________________________
erlang-questions mailing list
[hidden email]
http://erlang.org/mailman/listinfo/erlang-questions
Reply | Threaded
Open this post in threaded view
|

Re: gen_statem: next event internal and reply

Peter Morgan
Hi Raimo,

> On 31 Jul 2019, at 13:36, Raimo Niskanen <[hidden email]> wrote:
>
> I have checked in a branch in the daily builds that clarifies
> the documentation for transition_option() to be more explicit
> about when replies are sent, and some other stuff.
>
> / Raimo
>


Thanks very much for this. Making the documented reply order more explicit would be perfect. I found a couple of instances of:

{keep_state_and_data, [nei(work), {reply, From, ok}]};

and:

{keep_state_and_data, [{reply, From, ok}, nei(work)]};

Where the _assumption_ was that the order would be honoured (it is a list!). In the majority of cases that I’ve found it didn't matter - in one case the call was immediately followed by a terminate_child on that process, causing (sometimes) an unclean shutdown.

BTW - We’ve not seen any async timer related issues in 22 that we were seeing in earlier 21s. Thanks again for your speedy help on that issue too, much appreciated!

Regards,
Peter.

_______________________________________________
erlang-questions mailing list
[hidden email]
http://erlang.org/mailman/listinfo/erlang-questions
Reply | Threaded
Open this post in threaded view
|

Re: gen_statem: next event internal and reply

Raimo Niskanen-11
On Tue, Aug 06, 2019 at 12:22:29PM +0100, Peter Morgan wrote:

> Hi Raimo,
>
> > On 31 Jul 2019, at 13:36, Raimo Niskanen <[hidden email]> wrote:
> >
> > I have checked in a branch in the daily builds that clarifies
> > the documentation for transition_option() to be more explicit
> > about when replies are sent, and some other stuff.
> >
> > / Raimo
> >
>
>
> Thanks very much for this. Making the documented reply order more explicit would be perfect. I found a couple of instances of:

I try to think of every question as a potential bug report on the
documentation.


>
> {keep_state_and_data, [nei(work), {reply, From, ok}]};
>
> and:
>
> {keep_state_and_data, [{reply, From, ok}, nei(work)]};
>
> Where the _assumption_ was that the order would be honoured (it is a list!). In the majority of cases that I’ve found it didn't matter - in one case the call was immediately followed by a terminate_child on that process, causing (sometimes) an unclean shutdown.

I/We was/were thinking about letting Actions be a map() instead, and in
hindsight that may have been better, but at that time it felt
too esoteric...


>
> BTW - We’ve not seen any async timer related issues in 22 that we were seeing in earlier 21s. Thanks again for your speedy help on that issue too, much appreciated!

Glad to hear!  Thank you for the update!


>
> Regards,
> Peter.
>

--

/ Raimo Niskanen, Erlang/OTP, Ericsson AB
_______________________________________________
erlang-questions mailing list
[hidden email]
http://erlang.org/mailman/listinfo/erlang-questions