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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With