2
votes

E.g. suppose I have a module that implements gen_server behavior, and it has

handle_call({foo, Foo}, _From, State) ->
  {reply, result(Foo), State}
;

I can reach this handler by doing gen_server:call(Server, {foo, Foo}) from some other process (I guess if a gen_server tries to gen_server:call itself, it will deadlock). But gen_server:call blocks on response (or timeout). What if I don't want to block on the response?

Imaginary use-case: Suppose I have 5 of these gen_servers, and a response from any 2 of them is enough for me. What I want to do is something like this:

OnResponse -> fun(Response) ->
  % blah
end,
lists:foreach(
  fun(S) ->
    gen_server:async_call(S, {foo, Foo}, OnResponse)
  end, 
  Servers),
Result = wait_for_two_responses(Timeout),
lol_i_dunno()

I know that gen_server has cast, but cast has no way to provide any response, so I don't think that that's what I want in this case. Also, seems like it should not be the gen_server's concern whether caller wants to handle response synchronously (using gen_server:call) or async (does not seem to exist?).

Also, the server is allowed to provide response asynchronously by having handle_call return no_reply and later calling gen_server:reply. So why not also support handling response asynchronously on the other side? Or does that exist, but I'm just failing to find it??

4
isn't that what handle_cast is for?CoderDennis
Oh, I see you mentioned cast in the question, but that is the async way. If the other process needs a response, then the server needs to send another async message.CoderDennis
If you are going to send a response asynchronously via handle_cast, then seems that the whole gen_server is useless, because you end up needing to send sender in casts. handle_call should not be concerned about whether other process is blocking on a response or not. I mean, I could start a new process to turn synchronous calls into async, but that seems wasteful.allyourcode
I don't think you need a new process to make it async. It looks like you're already passing in a callback function. I don't have enough erlang experience to be any more helpful, but I do know that handle_call is for synchronous methods and handle_cast is for async. Sending a message back to the sender is the perfect way to do async. Don't fight it.CoderDennis

4 Answers

2
votes

gen_server:call is basically a sequence of

send a message to the server (with identifier)
wait for the response of that particular message

wrapped in a single function.

for your example you can decompose the behavior in 2 steps: a loop that uses gen_server:cast(Server,{Message,UniqueID,self()} with all servers, and then a receive loop that wait for a minimum of 2 answers of the form {UniqueID,Answer}. But you must take care to empty your mail box at some point in time. A better solution should be to delegate this to a separate process which will simply die when it has received the required number of answers:

[edit] make some correction in the code now it should work :o)

get_n_answers(Msg,ServerList,N) when N =< length(ServerList) ->
    spawn(?MODULE,get_n_answers,[Msg,ServerList,N,[],self()]).

get_n_answers(_Msg,[],0,Rep,Pid) -> 
    Pid ! {Pid,Rep};
get_n_answers(_Msg,[],N,Rep,Pid) -> 
    NewRep = receive
        Answ -> [Answ|Rep]
    end,
    get_n_answers(_Msg,[],N-1,NewRep,Pid);
get_n_answers(Msg,[H|T],N,Rep,Pid) -> 
    %gen_server:cast(H,{Msg,Pid}),
    H ! {Msg,self()},
    get_n_answers(Msg,T,N,Rep,Pid).

and you cane use it like this:

ID = get_n_answers(Msg,ServerList,2),
% insert some code here
Answer = receive
    {ID,A} -> A % tagged with ID to do not catch another message in the mailbox
end
2
votes

You can easily implement that by sending each call in a separate process and waiting for responses from as many as required (in essence this is what async is about, isn't? :-)

Have a look at this simple implementation of parallel call which is based on the async_call from rpc library in OTP.

This is how it works in plain English.

  • You need to make 5 calls so (in the parent process) you spawn 5 child Erlang processes.
  • Each process sends back to the parent process a tuple containing its PID and the result of the call.
  • The tuple can be only constructed and send back only when the desired call has been completed.
  • In the parent process you loop through responses in the receive loop.
  • You can wait for all responses or just 2 or 3 out of the started 5.

The parent process (which spawns the worker processes) will eventually receive all responses (I mean those you want to ignore). You need a way to discard them if you don't want the message queue to grow infinitely. There are two options:

  1. The parent process itself can be a transient process, created only for the call to spawn the other 5 child processes. Once the desired amount of responses is collected it can send the response back to a caller and die. Messages send to the died process will be discarded.

  2. The parent process can continue receiving messages after it has received the desired amount of responses and simply discard them.

0
votes

gen_server do not have a concept of async calls on client side. It is not trivial how to implement in consistently because gen_server:call is a combination of monitor for server process, send request message and wait for either answer or monitor down or timeout. If you do something like what you mentioned you will need to deal with DOWN messages from server somehow ... so hypothetical async_call should return some key for yeld and also an internal monitor reference for a case you are processing DONW messages from other processes... and do not want to mix it with yeld errors.

Not that good but possible alternative is to use rpc:async_call(gen_server, call, [....])

But this approach have a limitation in calling process will be a short lived rex child, so if your gen server use caller pid somehow other than send it a reply logic will be broken.

0
votes

gen_sever:call to the process itself would surely block until timeout. To understand the reason, one should be aware of the fact that gen_server framework actually combine your specific code together into one single module, and gen_server:call would be "translated" as "pid ! Msg" form.

So imagine how this block of code takes effect, the process actually stay in a loop keeping receiving messages, and when the control flow run into a handling function, the receiving process is temporarily interrupted, so if you call gen_server:call to the process itself, since it is a synchronous function, it waits for response, which however would never come in until the handing function returns so that the process can continue to receive messages, so the code is in a deadlock.