Slowdown due to non-parallel awaiting of promises in async generators
Solution 1:
You are spending too much time waiting for I/O from different sources.
In normal promise code, you'd use Promise.all
for this, however - people have a tendency to write code that waits for requests with generators. Your code does the following:
<-client service->
countryFor..
''--..
''--..
''--.. country server sends response
..--''
..--''
..--''
getCommentDataFor
''--..
''--..
''--..
''--.. comment service returns response
..--''
..--''
..--''
authenticate
''--..
''--..
''--.. authentication service returns
..--''
..--''
..--''
Generator done.
Instead, it should be doing:
<-client service->
countryFor..
commentsFor..''--..
authenticate..''--..''--..
''--..''--..''--.. country server sends response
''--..--''.. comment service returns response
..--''..--''.. authentication service returns response
..--''..--''..
..--''..--''..--''
..--''..--''
..--''
Generator done
Simply put, all your I/O should be done in parallel here.
To fix this, I'd use Promise.props
. Promise.props
takes an objects and waits for all its properties to resolve (if they are promises).
Remember - generators and promises mix and match really well, you simply yield promises:
Client.prototype.fetchCommentData = async(function* (user){
var country = countryService.countryFor(user.ip);
var data = api.getCommentDataFor(user.id);
var notBanned = authServer.authenticate(user.id).then(function(val){
if(!val) throw new AuthenticationError(user.id);
});
return Promise.props({ // wait for all promises to resolve
country : country,
comments : data,
notBanned: notBanned
});
});
This is a very common mistake people make when using generators for the first time.
ascii art shamelessly taken from Q-Connection by Kris Kowal
Solution 2:
As it is mentioned in the Bluebird docs for Promise.coroutine
, you need to watch out not to yield
in a series.
var county = yield countryService.countryFor(user.ip); var data = yield api.getCommentDataFor(user.id); var notBanned = yield authServer.authenticate(user.id);
This code has 3 yield
expressions, each of them stopping execution until the particular promise is settled. The code will create and execute each of the async tasks consecutively.
To wait for multiple tasks in parallel, you should yield
an array of promises. This will wait until all of them are settled, and then return an array of result values. Using ES6 destructuring assignments leads to concise code for that:
Client.prototype.fetchCommentData = async(function* (user){
var [county, data, notBanned] = yield [
// a single yield only: ^^^^^
countryService.countryFor(user.ip),
api.getCommentDataFor(user.id),
authServer.authenticate(user.id)
];
if (!notBanned)
throw new AuthenticationError(user.id);
return {
country: country,
comments: data,
notBanned: true
};
});
Solution 3:
The answer by Benjamin Gruenbaum is correct, but it loses the generators aspect completely, which tends to happen a bit when you're trying to run multiple things in parallel. You can, however, make this work just fine with the yield
keyword. I'm also using some extra ES6 features like destructuring assignments and object initializer shorthand:
Client.prototype.fetchCommentData = async(function* (user){
var country = countryService.countryFor(user.ip);
var data = api.getCommentDataFor(user.id);
var notBanned = authServer.authenticate(user.id).then(function(val){
if(!val) throw new AuthenticationError(user.id);
});
// after each async operation finishes, reassign the actual values to the variables
[country, data, notBanned] = yield Promise.all([country, data, notBanned]);
return { country, data, notBanned };
});
If you don't want those extra ES6 features being used:
Client.prototype.fetchCommentData = async(function* (user){
var country = countryService.countryFor(user.ip);
var data = api.getCommentDataFor(user.id);
var notBanned = authServer.authenticate(user.id).then(function(val){
if(!val) throw new AuthenticationError(user.id);
});
var values = yield Promise.all([country, data, notBanned]);
return {
country: values[0],
data: values[1],
notBanned: values[2]
};
});