18

One of the things that attracted me to Erlang in the first place is the Actor model; the idea that different processes run concurrently and interact via asynchronous messaging.

I'm just starting to get my teeth into OTP and in particular looking at gen_server. All the examples I've seen - and granted they are tutorial type examples - use handle_call() rather than handle_cast() to implement module behaviour.

I find that a little confusing. As far as I can tell, handle_call is a synchronous operation: the caller is blocked until the callee completes and returns. Which seems to run counter to the async message passing philosophy.

I'm about to start a new OTP application. This seems like a fundamental architectural decision so I want to be sure I understand before embarking.

My questions are:

  1. In real practice do people tend to use handle_call rather than handle_cast?
  2. If so, what's the scalability impact when multiple clients can call the same process/module?
2240
  • 1,547
  • 2
  • 12
  • 30
sfinnie
  • 9,854
  • 1
  • 38
  • 44

4 Answers4

24
  1. Depends on your situation.

    If you want to get a result, handle_call is really common. If you're not interested in the result of the call, use handle_cast. When handle_call is used, the caller will block, yes. This is most of time okay. Let's take a look at an example.

    If you have a web server, that returns contents of files to clients, you'll be able to handle multiple clients. Each client have to wait for the contents of files to be read, so using handle_call in such a scenario would be perfectly fine (stupid example aside).

    When you really need the behavior of sending a request, doing some other processing and then getting the reply later, typically two calls are used (for example, one cast and the one call to get the result) or normal message passing. But this is a fairly rare case.

  2. Using handle_call will block the process for the duration of the call. This will lead to clients queuing up to get their replies and thus the whole thing will run in sequence.

    If you want parallel code, you have to write parallel code. The only way to do that is to run multiple processes.

So, to summarize:

  • Using handle_call will block the caller and occupy the process called for the duration of the call.
  • If you want parallel activities to go on, you have to parallelize. The only way to do that is by starting more processes, and suddenly call vs cast is not such a big issue any more (in fact, it's more comfortable with call).
Adam Lindberg
  • 16,447
  • 6
  • 65
  • 85
11

Adam's answer is great, but I have one point to add

Using handle_call will block the process for the duration of the call.

This is always true for the client who made the handle_call call. This took me a while to wrap my head around but this doesn't necessarily mean the gen_server also has to block when answering the handle_call.

In my case, I encountered this when I created a database handling gen_server and deliberately wrote a query that executed SELECT pg_sleep(10), which is PostgreSQL-speak for "sleep for 10 seconds", and was my way of testing for very expensive queries. My challenge: I don't want the database gen_server to sit there waiting for the database to finish!

My solution was to use gen_server:reply/2:

This function can be used by a gen_server to explicitly send a reply to a client that called call/2,3 or multi_call/2,3,4, when the reply cannot be defined in the return value of Module:handle_call/3.

In code:

-module(database_server).
-behaviour(gen_server).
-define(DB_TIMEOUT, 30000).

<snip>

get_very_expensive_document(DocumentId) ->
    gen_server:call(?MODULE, {get_very_expensive_document, DocumentId}, ?DB_TIMEOUT).    

<snip>

handle_call({get_very_expensive_document, DocumentId}, From, State) ->     
    %% Spawn a new process to perform the query.  Give it From,
    %% which is the PID of the caller.
    proc_lib:spawn_link(?MODULE, query_get_very_expensive_document, [From, DocumentId]),    

    %% This gen_server process couldn't care less about the query
    %% any more!  It's up to the spawned process now.
    {noreply, State};        

<snip>

query_get_very_expensive_document(From, DocumentId) ->
    %% Reference: http://www.erlang.org/doc/man/proc_lib.html#init_ack-1
    proc_lib:init_ack(ok),

    Result = query(pgsql_pool, "SELECT pg_sleep(10);", []),
    gen_server:reply(From, {return_query, ok, Result}).
Asim Ihsan
  • 1,501
  • 8
  • 18
  • A more concise explanation of the above is also here: http://www.trapexit.org/Building_Non_Blocking_Erlang_apps – Asim Ihsan May 21 '11 at 09:51
  • Thx for the response @Asymptote. The pattern makes sense: in effect the gen_server becomes a 'dispatcher' with the work done in spawned children (which sounds a little like @Victor's 'sub-process C' scenario if I understand correctly? – sfinnie May 22 '11 at 20:24
  • @sfinnie: Precisely. However, this model opens up a big can of worms, and I've omitted many important details. 1) Since we've (correctly) spawn_link()-ed to child "workers" if the children exit unexpectedly the gen_server will come crashing down! This is no good, and we'll probably want to trap exit messages in the gen_server and explicitly handle them in handle_info. 2) What if too many requests come in simultaneously? We probably want to keep a track of how many queries are outstanding and reject/retry subsequent queries. – Asim Ihsan May 23 '11 at 10:11
  • Also, 3) what if many queries are failing one after another? This may indicate a problem with the database backend and the last thing we want to do is to allow the same rate of incoming queries to reach the database. This scenario calls for a "circuit breaker"; in case of a sharp increase in failures we deliberately tell clients to come back later and give the back-end breathing room. "Release it!" has more excellent ideas: http://pragprog.com/titles/mnee/release-it – Asim Ihsan May 23 '11 at 10:16
1

IMO, in concurrent world handle_call is generally a bad idea. Say we have process A (gen_server) receiving some event (user pressed a button), and then casting message to process B (gen_server) requesting heavy processing of this pressed button. Process B can spawn sub-process C, which in turn cast message back to A when ready (of to B which cast message to A then). During processing time both A and B are ready to accept new requests. When A receives cast message from C (or B) it e.g. displays result to the user. Of course, it is possible that second button will be processed before first, so A should probably accumulate results in proper order. Blocking A and B through handle_call will make this system single-threaded (though will solve ordering problem)

In fact, spawning C is similar to handle_call, the difference is that C is highly specialized, process just "one message" and exits after that. B is supposed to have other functionality (e.g. limit number of workers, control timeouts), otherwise C could be spawned from A.

Edit: C is asynchronous also, so spawning C it is not similar to handle_call (B is not blocked).

Victor Moroz
  • 9,167
  • 1
  • 19
  • 23
  • Thanks @Victor. That was my assumption too. Question arose precisely because examples for gen_server all seem to use `handle_call`. Upside of synchronous code is it's easier to follow and preserves ordering; downside is blocking behaviour. Horses for courses I suppose. Good point re. C being in effect a synchronous call. – sfinnie May 17 '11 at 21:06
0

There are two ways to go with this. One is to change to using an event management approach. The one I am using is to use cast as shown...

    submit(ResourceId,Query) ->
      %%
      %% non blocking query submission
      %%
      Ref = make_ref(),
      From = {self(),Ref},
      gen_server:cast(ResourceId,{submit,From,Query}),
      {ok,Ref}.

And the cast/submit code is...

    handle_cast({submit,{Pid,Ref},Query},State) ->
      Result = process_query(Query,State),
      gen_server:cast(Pid,{query_result,Ref,Result});

The reference is used to track the query asynchronously.

tony wallace
  • 135
  • 6