How to make parallel calls in Erlang and wait for all of the results?

983 views Asked by At

I'm working on a mobile game backend in Erlang. For each HTTP request, it might need to query different data sources, such as PostgreSQL, MongoDB and Redis. I want to make independent calls to these data sources in parallel but cannot find a clear Erlang way to do it.

For example,

handle_request(?POST, <<"login">>, UserId, Token) ->
    % Verify token from PostgreSQL
    AuthResult = auth_service:login(UserId, Token), 

    % Get user data such as outfits and level from MongoDB
    UserDataResult = userdata_service:get(UserId),

    % Get messages sent to the user from Redis
    MessageResult = message_service:get(UserId),

    % How to run the above 3 calls in parallel?
    % Then wait for all their results here? 

    % Combine the result and send back to client
    build_response(AuthResult, UserDataResult, MessageResult).

Each service will eventually call into the corresponding data driver (epgsql, eredis, mongo_erlang) that end up with some pooboy:transaction and gen_server:call there. How to design these services module are not decided yet either.

I want to make sure that the 3 data calls above could run in parallel, and then the handle_request function waits for all of those 3 calls finish, and then call build_response. How could I do that properly?

As a reference, in NodeJS, I might do this

var authPromise = AuthService.login(user_id, token);
var userDataPromise = UserdataService.get(user_id);
var messagePromise = MessageService.get(user_id);
Promise.all(authPromise, userDataPromise, messagePromise).then( function(values) { 
    buildResponse(values); 
}

In Scala I might do this

val authFuture = AuthService.login(userId, token)
val userDataFuture = UserdataService.get(userId)
val messageFuture = MessageService.get(userId)
for {
    auth <- authFuture
    userData <- userDataFuture
    message <- messageFuture
} yield ( buildResponse(auth, userData, message )

Apparently, I'm thinking the problem as a promise/future/yield problem. But I was told that if I'm looking for a Promise in Erlang I might be going in the wrong direction. What would be the best practice in Erlang to achieve this?

1

There are 1 answers

0
7stud On BEST ANSWER

How to make parallel calls in Erlang and wait for all of the results?

You can employ stacked receive clauses. Erlang will wait forever in a receive clause until a message arrives from a process (or you can specify a timeout with after)--which is similar to awaiting a promise in nodejs:

-module(my).
-compile(export_all).

all_results() -> 
    Pid1 = spawn(?MODULE, getdata1, [self(), {10, 20}]),
    Pid2 = spawn(?MODULE, getdata2, [self(), 30]),
    Pid3 = spawn(?MODULE, getdata3, [self()]),

    [receive {Pid1, Result1} -> Result1 end, 
     receive {Pid2, Result2} -> Result2 end,
     receive {Pid3, Result3} -> Result3 end].

getdata1(From, {X, Y}) -> 
    %% mimic the time it takes to retrieve the data:
    SleepTime = rand:uniform(100),
    io:format("Sleeping for ~w milliseconds~n", [SleepTime]), 
    timer:sleep(SleepTime),

    From ! {self(), X+Y}.  %% send the data back to the main process

getdata2(From, Z) ->
    SleepTime = rand:uniform(100),
    io:format("Sleeping for ~w milliseconds~n", [SleepTime]),
    timer:sleep(SleepTime),

    From ! {self(), Z+1}.

getdata3(From) ->
    SleepTime = rand:uniform(100),
    io:format("Sleeping for ~w milliseconds~n", [SleepTime]),
    timer:sleep(SleepTime),

    From ! {self(), 16}. 

Note that this code:

[receive {Pid1, Result1} -> Result1 end, 
 receive {Pid2, Result2} -> Result2 end,
 receive {Pid3, Result3} -> Result3 end].

is equivalent to:

R1 = receive {Pid1, Result1} -> 
         Result1 
     end,
R2 = receive {Pid2, Result2} -> 
         Result2 
     end,
R3 = receive {Pid3, Result3} -> 
         Result3 
     end,

[R1, R2, R3].

In the shell:

~/erlang_programs$ erl
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Eshell V9.3  (abort with ^G)

1> c(my).                        
my.erl:2: Warning: export_all flag enabled - all functions will be exported
{ok,my}

2> timer:tc(my, all_results, []).
Sleeping for 66 milliseconds
Sleeping for 16 milliseconds
Sleeping for 93 milliseconds
{96356,[30,31,16]}

3> timer:tc(my, all_results, []).
Sleeping for 57 milliseconds
Sleeping for 30 milliseconds
Sleeping for 99 milliseconds
{100153,[30,31,16]}

4> timer:tc(my, all_results, []).
Sleeping for 66 milliseconds
Sleeping for 31 milliseconds
Sleeping for 24 milliseconds
{66426,[30,31,16]}

timer:tc() returns the time that a function takes to execute in microseconds (1,000 microseconds = 1 millisecond) along with the function's return value. For instance, the first time that all_results() was called it took 96.4 milliseconds to complete, while the individual processes would have taken 66+16+93=175+ milliseconds to finish if executed sequentially.