Saturday, November 24, 2012

Source Saturday: I'm an idiot

One of the best things about Node.js is that it's single threaded, using a callback model to manage asynchronous processing.  One of the most annoying things about Node is that it uses a callback model...you get the picture.  Callbacks can be immensely powerful but are trickier to write and understand than the traditional blocking methods.  Case in point: I managed to screw up callbacks while simultaneously forgetting one of the nicer features of databases.

I had an array of ids and wanted to get the various items from the database that were associated with those ids.  (Yes, yes, I know about the $in command.  I don't know what I was thinking either.)
for(var id in ids) {
    //Convert text id to BSON id
    var BSON = mongo.BSONPure;
    var b_id = BSON.ObjectID(ids[id]);
    collection.findOne({_id: b_id}, function(err, document) {
        if(err) { console.error(err); db.close(); return;}
        returnObject[id] = document;
        if(id == ids.length - 1) {
           res.render('checkout', {classes: returnObject});
           db.close();
        }
    });
}
This code set the land speed record for fastest turnaround from written code to "What on earth were you thinking, past Randall?"  (2 days).  While most of you probably have figured out what's wrong, let's look into the details.  In a purely synchronous language, this makes sense.  Loop through the array, find the entry associated with the id at that point in the array and add it to a return object.  When we've reached the last point in the array, render our page with the generated object and close our database connection.

Here's what really happens:
for loop executes, adding collection.findOne callbacks to the node callback "todo" pile.  After the loop finishes, `id` is the last `id` in the array.
Sometime after the for loop finishes, the callbacks are executed, all with the last id.  Since the last id will trigger the render and database close, only one callback is run.  The rest encounter a closed database.

Fortunately, this had an incredibly simple answer: Use mongodb's $in function.  I still had to loop through and convert to BSON ObjectIDs.  This resulted in much simpler code that used callbacks the right way.
for(var i=0;i<ids.length;i++) {
    var b_id = BSON.ObjectID(ids[i]);
    b_ids.push(b_id);
}
collection.find({_id: {$in: b_ids}}).toArray(function(err, results) {
    if(err) { console.error(err); db.close(); return;}
    res.render('checkout', {classes: results});
    db.close();
});
Now, there's only one callback, and it'll contain exactly the data we want it to.  Any number of ids can and will be handled correctly.

A whole blog post, because I forgot one ruttin' command...

No comments:

Post a Comment