MongoDB Node.js native driver silently swallows `b

2019-01-20 03:01发布

问题:

The script below has a bug in the mongo bulkWrite op syntax: $setOnInsert: { count:0 }, is unnecessary and thus mongo throws an exception: "Cannot update 'count' and 'count' at the same time".

The problem is, the node.js driver doesn't seem to catch it. This script logs "Success!" to the console.

(async () => {

  let db = await require('mongodb').MongoClient.connect('mongodb://localhost:27017/myNewDb');

  let mongoOps = [{
    updateOne: {
      filter: { foo: "bar" },
      update: {
        $setOnInsert: { count:0 },
        $inc: { count:1 },
      },
      upsert: true,
    }
  }];

  try {
    await db.collection("myNewCollection").bulkWrite(mongoOps);
    console.log("Success!");
  } catch(e) {
    console.log("Failed:");
    console.log(e);
  }

})();

Examining db.system.profile.find({}) with db.setProfileLevel(2) we can see the exception:

{
    "op" : "update",
    "ns" : "myNewDb.myNewCollection",
    "query" : {
        "foo" : "bar"
    },
    "updateobj" : {
        "$setOnInsert" : {
            "count" : 0
        },
        "$inc" : {
            "count" : 1
        }
    },
    "keyUpdates" : 0,
    "writeConflicts" : 0,
    "numYield" : 0,
    "locks" : {
        "Global" : {
            "acquireCount" : {
                "r" : NumberLong(1),
                "w" : NumberLong(1)
            }
        },
        "Database" : {
            "acquireCount" : {
                "w" : NumberLong(1)
            }
        },
        "Collection" : {
            "acquireCount" : {
                "w" : NumberLong(1)
            }
        }
    },
    "exception" : "Cannot update 'count' and 'count' at the same time",
    "exceptionCode" : 16836,
    "millis" : 0,
    "execStats" : {},
    "ts" : ISODate("2017-10-12T01:57:03.008Z"),
    "client" : "127.0.0.1",
    "allUsers" : [],
    "user" : ""
}

Why is the driver swallowing errors like this? I definitely seems like a bug, but I figured I'd ask here first just to be sure.

回答1:

So as commented, "It's a bug". Specifically the bug is right here:

 // Return a Promise
  return new this.s.promiseLibrary(function(resolve, reject) {
    bulkWrite(self, operations, options, function(err, r) {
      if(err && r == null) return reject(err);
      resolve(r);
    });
  });

The problem is that the "response" ( or r ) in the callback which is being wrapped in a Promise is not actually null, and therefore despite the error being present the condition is therefore not true and reject(err) is not being called, but rather the resolve(r) is being sent and hence this is not considered an exception.

Correcting would need some triage, but you can either 'work around' as mentioned by inspecting the writeErrors property in the response from the current bulkWrite() implementation or consider one of the other alternatives as:

Using the Bulk API methods directly:

const MongoClient = require('mongodb').MongoClient,
      uri  = 'mongodb://localhost:27017/myNewDb';

(async () => {

  let db;

  try {

    db = await MongoClient.connect(uri);

    let bulk = db.collection('myNewCollection').initializeOrderedBulkOp();

    bulk.find({ foo: 'bar' }).upsert().updateOne({
      $setOnInsert: { count: 0 },
      $inc: { count: 0 }
    });

    let result = await bulk.execute();
    console.log(JSON.stringify(result,undefined,2));

  } catch(e) {
    console.error(e);
  } finally {
    db.close();
  }

})();

Perfectly fine but of course has the issue of not naturally regressing on server implementations without Bulk API support to using the legacy API methods instead.

Wrapping the Promise Manually

(async () => {

  let db = await require('mongodb').MongoClient.connect('mongodb://localhost:27017/myNewDb');

  let mongoOps = [{
    updateOne: {
      filter: { foo: "bar" },
      update: {
        $setOnInsert: { count:0 },
        $inc: { count:1 },
      },
      upsert: true,
    }
  }];

  try {
    let result = await new Promise((resolve,reject) => {

      db.collection("myNewCollection").bulkWrite(mongoOps, (err,r) => {
        if (err) reject(err);
        resolve(r);
      });
    });
    console.log(JSON.stringify(result,undefined,2));
    console.log("Success!");
  } catch(e) {
    console.log("Failed:");
    console.log(e);
  }

})();

As noted the problem lies within the implementation of how bulkWrite() is returning as a Promise. So instead you can code with the callback() form and do your own Promise wrapping in order to act how you expect it to.

Again as noted, needs a JIRA issue and Triage to which is the correct way to handle the exceptions. But hopefully gets resolved soon. In the meantime, pick an approach from above.