Handling system messages in generic behaviours

I have a process which can take a long time to complete it’s work. I had expected that using handle_continue/2 to break it up into smaller tasks would allow the process to respond to system messages between each loop. however that is not the case.

-module(server).

-export([init/1, handle_cast/2, handle_call/3, handle_continue/2]).

-behaviour(gen_server).

init(_Args) ->
	{ok, #{}}.

handle_cast(N, State) ->
	erlang:display({?MODULE, ?FUNCTION_NAME, N}),
	{noreply, State, {continue, N}}.

handle_call(_, _From, State) ->
	{noreply, State}.

handle_continue(0, State) ->
	erlang:display({?MODULE, ?FUNCTION_NAME, 0}),
	{noreply, State};
handle_continue(N, State) ->
	{noreply, State, {continue, N - 1}}.

This will loop, calling handle_continue/2, N times. Choosing a large value of N keeps it busy enough that the sys:get_status/1 call times out.

1> {ok, S} = gen_server:start(server, [], []).
{ok,<0.86.0>}
2> gen_server:cast(S, 1000000000), sys:get_status(S).
{server,handle_cast,1000000000}
** exception exit: {timeout,{sys,get_status,[<0.86.0>]}}
     in function  sys:send_system_msg/2 (sys.erl, line 754)
3> {server,handle_continue,0}

I am assuming that sys:get_status/1 is representative of system messages, am I wrong?

Should we make it work?

handle_continue is not a message-based event (the handle_ part is misleading I think, as it puts it close to the other handle_* callbacks which are message-based). When a callback returns {..., {continue, ...}}, handle_continue is immediately called afterwards, ie without receiving and handling any messages. The documentation says as much :wink:

What the documentation says is:

The call is invoked immediately after the previous callback, which makes it useful for performing work after initialization or, for splitting the work in a callback into multiple steps, updating the process state along the way.

I think it SHOULD handle system messages before handle_continue/2.

Would this not be a sensible improvement?

I haven’t begun to look at how to implement it …

1 Like

Well, it does that. The important point is “… immediately after the previous callback, …”, it doesn’t say anything about receiving/handling any messages (but maybe it should explicitly say that it doesn’t do :woman_shrugging:).

Anyway, what the {continue, ...} feature does (and was AFAIK designed for) is to execute some code after a callback has executed. Like, when init returns {ok, ..., {continue, ...}}, proc_lib:init_ack is called (to signal back to a starting process that the process has been started and unblock it), then immediately execute the respective handle_continue clause. When handle_call returns {reply, ..., {continue, ...}}, it will reply to the process having made a gen_server:call, then immediately execute the respective handle_continue clause. In a way, it is something that actually concerns a process communicating with and waiting for the gen_server process in question, ie to unblock it early.

Maybe. Maybe not. :woman_shrugging: Why only system messages then? What about 'EXIT' messages, like, from the parent, or sibling processes, or any process the specific gen_server implementation thinks important?

Well, be my guest, impress and amaze me :wink::wink::wink:

Seriously though, the only way (that I can see) for this to go about is via (notoriously expensive) selective receives. I tried something similar with Add ability to prioritize termination to `gen_*` behaviors by Maria-12648430 · Pull Request #8371 · erlang/otp · GitHub, and using selective receives was a major show stopper there :woman_shrugging:

Here you go! https://erlangforums.com/t/eep-76-priority-messages/

@rickard that’s exactly the case I started with for “priority” messages. Of course it won’t be possible to distinct between gen* priority messages and user-generated priority messages, but I think it’s OK.

I guess in my mind I was thinking that system messages were being handled with selective receive. Although I’ve been using Erlang as my primary language since last century, I rarely manage processes outside of OTP and never, ever, write receive. I see now that it is gen_server which contains the loop and calls sys:handle_system_message/6 after receiving. As an OTP developer what I would want is for system messages to take priority, so for me, system messages would be the primary use case for EEP76.

It occurs to me that the semantics of what I actually want is provided by timeout:

-module(server).

-export([init/1, handle_cast/2, handle_call/3, handle_info/2]).

-behaviour(gen_server).

init(_Args) ->
	{ok, 0}.

handle_cast(N, _State) ->
	erlang:display({?MODULE, ?FUNCTION_NAME, N}),
	{noreply, N, 0}.

handle_call(_, _From, State) ->
	{noreply, State}.

handle_info(timeout, 0 = State) ->
	erlang:display({?MODULE, ?FUNCTION_NAME, State}),
	{noreply, State};
handle_info(timeout, State) ->
	{noreply, State - 1, 0}.

I want the opportunity to service incoming messages after each chunk of work, even if I do not expect regular messages. This works:

1> {ok, S} = gen_server:start(server, [], []).
{ok,<0.86.0>}
2> gen_server:cast(S, 100000000), sys:get_status(S).
{server,handle_cast,100000000}
{status,<0.86.0>,
        {module,gen_server},
        [[{'$ancestors',[<0.84.0>,<0.72.0>,<0.70.0>,user_drv,
                         <0.69.0>,<0.64.0>,kernel_sup,<0.47.0>]},
          {'$initial_call',{server,init,1}}],
         running,<0.86.0>,[],
         [{header,"Status for generic server <0.86.0>"},
          {data,[{"Status",running},
                 {"Parent",<0.86.0>},
                 {"Logged events",[]}]},
          {data,[{"State",99987107}]}]]}
3> {server,handle_info,0}
1 Like