One of the nice things about nodejs is that since the majority of its libraries are asynchronous it boasts strong support for concurrently performing IO heavy workloads. Even though node is single threaded the event loop is able to concurrently progress separate operations because of the asynchronous style of its libraries. A canonical example would something like fetching 10 web pages and extracting all the links from the fetched HTML. This fits into node’s computational model nicely since the most time consuming part of an HTTP request is waiting around for the network during which node can use the CPU for something else. For the sake of discussion, let’s consider this sample implementation:
var request = require("request");
var Q = require("q");
var cheerio = require('cheerio');
require('request').debug = true
function makeRequest(url){
var df = Q.defer();
request(url, function (error, response, body) {
var $ = cheerio.load(body);
var links = [];
$("a").each(function(){
links.push( $(this).attr("href") );
});
df.resolve(links);
});
return df.promise;
}
var targetUrls = ["http://www.cnn.com", "http://www.setfive.com", "http://gawker.com/"];
targetUrls = targetUrls.map(makeRequest);
Q.all(targetUrls).then(function(results){
process.exit(0);
});
Request debugging is enabled so you’ll see that node starts fetching all the URLs at the same time and then the various events fire at different times for each URL:
REQUEST { uri: 'http://www.cnn.com', callback: [Function] }
REQUEST { uri: 'http://www.setfive.com', callback: [Function] }
REQUEST { uri: 'http://gawker.com/', callback: [Function] }
REQUEST make request http://www.cnn.com/
REQUEST make request http://www.setfive.com/
REQUEST make request http://gawker.com/
REQUEST onRequestResponse http://www.cnn.com/ 200 { 'x-servedbyhost': 'prd-10-60-165-19.nodes.56m.dmtio.net'}
REQUEST reading response's body
REQUEST finish init function http://www.cnn.com/
REQUEST onRequestResponse http://gawker.com/ 200 { 'cache-control': 'stale-if-error=86400, stale-while-revalidate=300',}
REQUEST reading response's body
REQUEST finish init function http://gawker.com/
REQUEST response end http://www.cnn.com/ 200 { 'x-servedbyhost': 'prd-10-60-165-19.nodes.56m.dmtio.net', }
REQUEST end event http://www.cnn.com/
REQUEST has body http://www.cnn.com/ 103558
REQUEST emitting complete http://www.cnn.com/
REQUEST response end http://gawker.com/ 200 { 'cache-control': 'stale-if-error=86400, stale-while-revalidate=300',}
REQUEST end event http://gawker.com/
REQUEST has body http://gawker.com/ 373120
REQUEST emitting complete http://gawker.com/
REQUEST onRequestResponse http://www.setfive.com/ 200 { server: 'nginx',}
REQUEST reading response's body
REQUEST finish init function http://www.setfive.com/
REQUEST response end http://www.setfive.com/ 200 { server: 'nginx',
REQUEST end event http://www.setfive.com/
REQUEST has body http://www.setfive.com/ 13611
REQUEST emitting complete http://www.setfive.com/
So we’ve demonstrated that node will concurrently “do” several things at the same time but what happens if computationally intensive code is tying up the event loop? As a concrete example, imagine doing something like compressing the results of the HTTP request. For our purposes we’ll just throw in a while(1) so it’s easier to see what’s going on:
var request = require("request");
var Q = require("q");
var cheerio = require('cheerio');
require('request').debug = true
function makeRequest(url){
var df = Q.defer();
request(url, function (error, response, body) {
var $ = cheerio.load(body);
var links = [];
var startedAt = Math.floor(new Date() / 1000);
while( Math.floor(new Date() / 1000) - startedAt < 5 ){
true;
}
$("a").each(function(){
links.push( $(this).attr("href") );
});
df.resolve(links);
});
return df.promise;
}
var targetUrls = ["http://www.cnn.com", "http://www.setfive.com", "http://gawker.com/"];
targetUrls = targetUrls.map(makeRequest);
Q.all(targetUrls).then(function(results){
process.exit(0);
});
If you run the script you’ll notice it takes much longer to finish since we’ve now introduced a while() loop that causes each URL to take at least 5 seconds to be processed:
# Without the 5 second penalty
ashish@ashish:~/workspace/$ time nodejs urltest.js
real 0m0.694s
user 0m0.657s
sys 0m0.040s
# With the 5 second penalty
ashish@ashish:~/workspace/$ time nodejs urltest.js
real 0m14.976s
user 0m14.915s
sys 0m0.076s
And now back to the original problem, how can we fetch the URLs in parallel so that our script completes in around 5 seconds? It turns out it’s possible to do this with node with the child_process module. Child_process basically lets you fire up a second nodejs instance and use IPC to pass messages between the parent and it’s child. We’ll need to move a couple of things around to get this to work and the implementation ends up looking like:
var request = require("request");
var Q = require("q");
var cheerio = require('cheerio');
var childProcess = require('child_process');
function makeRequest(url){
var df = Q.defer();
request(url, function (error, response, body) {
var $ = cheerio.load(body);
var links = [];
var startedAt = Math.floor(new Date() / 1000);
while( Math.floor(new Date() / 1000) - startedAt < 5 ){
true;
}
$("a").each(function(){
links.push( $(this).attr("href") );
});
df.resolve(links);
});
return df.promise;
}
var targetUrls = ["http://www.cnn.com", "http://www.setfive.com", "http://gawker.com/"];
var numRecieved = 0;
function recieveLinks(links){
console.log(links);
numRecieved += 1;
if(numRecieved == targetUrls.length){
process.exit(0);
}
}
/**
* We'll detect that we're in the "master"
* by passing an argument when running
*/
if( process.argv.length == 3 ){
for(var i = 0; i < targetUrls.length; i++){
var cp = childProcess.fork(__filename);
cp.on("message", recieveLinks);
cp.send( targetUrls[i] );
}
}else{
process.on('message', function(url) {
makeRequest(url).then(function(links){
process.send(links);
});
});
}
What’s happening now is that we’re launching a child process for each URL we want to process, passing a message with the target URL, and then passing the links back to the parent. And then running along with a timer results in:
ashish@ashish:~/workspace/$ time nodejs urltest.js master
real 0m5.878s
user 0m0.355s
sys 0m0.040s
It isn’t exactly 5 seconds since there’s a non-trivial amount of time required to start each of the child processes but it’s around what you’d expect. So there you have it, we’ve successfully demonstrated how you can achieve parallelism with nodejs.