Wednesday, July 25, 2012

On javascript asynchronous callbacks

One of the true beauty of javascript, in my opinion, is the callback we use in asynchronous calls.
Most of the time callbacks are anonymous functions passed as the last parameter to an asynchronous function. We use a lot of callbacks in node.js environment.
However, sometimes it can become a bit confusing when many asynchronous functions need to be called. In this post I'll focus on parallel asynchronous function calls and overall completion callback.



While writing an RRD update script, I needed to get the value of a set of data sources that can be dynamically specified by the caller. The data sources values are extracted from a Mongodb database. Before doing the RRD update call, I needed to collect the data sources values asynchronously. The process was something like this:

function collectValues (data_sources, when_done_callback) {
  /* Collect values */
  data_sources.forEach(function(ds) {
    /* asynchronous mongodb call for each ds */
  }
}
The problem is how to do the asynchronous database calls and then when they all complete, execute the when_done_callback ?
I'm sure everyone has his own way of solving this problem but, here is mine:

function whenDone(doneCallback) {
  var left = 0
  , done = function() { if (--left === 0) { doneCallback(); } };
  return function doThis(work, n) {
    if (typeof n !== 'undefined') left += n;
    else left++;
    process.nextTick(function(){ work(done); });
  };
}
So, how do I use this function in the context of previous example?
function collectValues (data_sources, when_done_callback) {
  var after = whenDone(when_done_callback);

  /* Collect values */
  data_sources.forEach(function(ds_name) {
    after(function(done) {
      collection.count(/* query for this ds */, function (error, count) {
        /* Do something with the error and count */
        done(); /* It is important to call the done function */
      });
    });
  }
}

What it does

The function takes an unique doneCallback parameter and returns a function that has 3 variables set in it's context. 
  • A counter (left) that will record how many asynchronous functions must complete before calling the doneCallback
  • A done() function that is bound to the same context (i.e it can access the left counter). Calling this function will decrease the left counter and if the counter is 0 call the doneCallback
  • The doneCallback
When you first call the whenDone() function you bind the doneCallback and get a new doThis function that you can use to execute asynchronous functions. At this point the counter is still 0.

When you use the doThis function you pass it an anonymous function as argument. Using the doThis function automatically increases the counter by one (you can however specify by how much it should increase as second argument). 
All your asynchronous work is done in this anonymous function. The anonymous function gets as unique argument the done function that you need to run to decrease the counter.

For best results we need to call the doThis function in the same tick. Else it could happen that the doneCallback would run multiple times.


No comments:

Post a Comment