Use node.js+redis to store average request time
Problem
Hi folks I have a very practical redis use-case question. Say I want to store average request time with Redis with the following js code. Basically I am trying to calculate the average request time and save to redis upon each request entry ( [ req_path, req_time ] )
var rc=require('redis').createClient()
,rc2=require('redis').createClient()
,test_data=[
['path/1', 100]
,['path/2', 200]
,['path/1', 50]
,['path/1', 70]
,['path/3', 400]
,['path/2', 150]
];
rc.del('reqtime');
rc.del('reqcnt');
rc.del('avgreqtime');
for(var i=0, l=test_data.length; i
But it's not working as expected for the avgreqtime key. From the stdout I got
debug: iteration # 0, item=["path/1",100]
debug: iteration # 1, item=["path/2",200]
debug: iteration # 2, item=["path/1",50]
debug: iteration # 3, item=["path/1",70]
debug: iteration # 4, item=["path/3",400]
debug: iteration # 5, item=["path/2",150]
req_path=path/2,t=undefined,c=1
debug: added member path/2 to sorted set "avgreqtime" with score %f NaN
req_path=path/2,t=undefined,c=1
debug: added member path/2 to sorted set "avgreqtime" with score %f NaN
req_path=path/2,t=undefined,c=2
debug: added member path/2 to sorted set "avgreqtime" with score %f NaN
req_path=path/2,t=undefined,c=3
debug: added member path/2 to sorted set "avgreqtime" with score %f NaN
req_path=path/2,t=undefined,c=1
debug: added member path/2 to sorted set "avgreqtime" with score %f NaN
req_path=path/2,t=undefined,c=2
debug: added member path/2 to sorted set "avgreqtime" with score %f NaN
The debug lines inside redis functions are printed at once in the end instead of during each iteration. I think this has something to do with node.js's asynchronous nature but I have no clue how to get this work. As an experiment I also tried replacing the for loop with the following without success:
for(var i=0, l=test_data.length; i
I got total request time in each iteration this time but the problem is req_path sticks with 'path/2' which is the last req_path in test_data. As a result only 'path/2' gets saved to avgreqtime and it's wrong:
debug: iteration # 0, item=["path/1",100]
debug: iteration # 1, item=["path/2",200]
debug: iteration # 2, item=["path/1",50]
debug: iteration # 3, item=["path/1",70]
debug: iteration # 4, item=["path/3",400]
debug: iteration # 5, item=["path/2",150]
debug(path/2): got ["100","1"]
debug(path/2): got ["200","1"]
debug(path/2): got ["150","2"]
debug(path/2): got ["220","3"]
debug(path/2): got ["400","1"]
debug(path/2): got ["350","2"]
I am using Redis 2.4.5 and the node redis client is from https://github.com/mranney/node_redis
Solution
You are correct in your guess that it has to do with node's asynchronous nature. I will try a simple example here:
for(var i = 0; i
Here, i
will be what you expect it to be the first time you refer to it (as a parameter to someAsyncFunction
). Inside the callback to that function, i
will always be 10
. The for loop has already finished by the time the callback is executed. To fix this, you need to bind i
somehow. One way is an anonymous function, immediately executed:
for(var i = 0; i
Now, i
will be bound to the correct value even inside the callback. It's not optimal, because we need to specify a new function each time. This is better:
var executeTheFunction = function(i) {
someAsyncFunction(i, function(err, data) {
console.log("executed function for", i);
});
};
for(var i = 0; i
Note that our executeTheFunction
doesn't take a callback. This means that we can't really control execution -- all calls will be executed immediately, which may not be what we want if there are many calls. In such a case, I recommend the async module, which makes this stuff easy.
Update: Here's an example with async
:
var calculateAverage = function(item, callback) {
var req_path = item[0], req_time = item[1];
rc.multi()
.zincrby('reqtime', req_time, req_path )
.zincrby('reqcnt', 1, req_path )
.exec( function(err, replies) {
if(err) return callback(err);
console.log('debug(%s): got %j', req_path, replies);
var avg=replies[0]/replies[1];
rc2.zadd('avgreqtime', avg, req_path, callback);
});
}
async.map(test_data, calculateAverage, function(err) {
if(err)
console.error("Error:", err);
else
console.log("Finished");
});
Now, you can easily manage this kind of stuff with async.queue
etc.
Discussion
View additional discussion.