0

Edit 6/15 I tried to run the same code adding a delay before calling $doSafePush() again when receiving a ConcurrencyDBError (i.e. doing return when.resolve("wait").delay(35).then(function() {$doSafePush(err.object, "set", value, retries-1);}); instead of return $doSafePush(err.object, "set", value, retries-1);). It then works just fine as long as I wait for at least 35ms; below that threshold it works sometimes. Feels like magic number but it's working...

Still would be interested in understanding why if anyone's got any clue :)


I'm running into an issue when having concurrent read & write operations on a single document in MongoDB under node.js

See below the code I'm running. Basically I defined a Collection into Mongo DB structured as such {_id: ObjectID, __occ: Number, set: Array}. Since I have multiple transactions to execute in "parallel" I wanted to write a safe push using the classical Optimistic Consistency Control framework when I only update a document if the version I'm working with has the right _id and right __occ; else I force a reload and try again.

Essentially it leads to multiple write and read (if the write fails) operations being run concurrently on the same single document.

Unfortunately, I can't get it to work. At some point, Mongo starts returning funny document and it messes everything up.

Running the code from below, I have the following output (corresponding to the debug(...) command at the very bottom of the last function below)

part 1: working just fine

29033b successfully added >>> occ=1 set={#1 elts, ["29033b"]} +87ms
29033c successfully added >>> occ=2 set={#2 elts, ["29033b","29033c"]} +33ms
29033d successfully added >>> occ=3 set={#3 elts, ["29033b","29033c","29033d"]} +37ms

part 2: first misshap: former item #3 is replaced by another one => first data is lost

290342 successfully added >>> occ=3 set={#3 elts, ["29033b","29033c",**"290342"**]} +37ms

part 3: 2nd misshap; back to the original item #3 => '290342' is lost

29033e successfully added >>> occ=4 set={#4 elts, ["29033b","29033c",**"29033d"**,"29033e"]} +32ms
290344 successfully added >>> occ=5 set={#5 elts, ["29033b","29033c","29033d","29033e","290344"]} +10ms
290343 successfully added >>> occ=6 set={#6 elts, ["29033b","29033c","29033d","29033e","290344","290343"]} +35ms

part 4: 3rd misshap; we are back to the item '290342' in 3rd position; it's getting ugly

29033f successfully added >>> occ=4 set={#4 elts, ["29033b","29033c",**"290342"**,"29033f"]} +31ms

part 5: 4th misshap; and yet again item #3 has changed, and item #4 from part 3 is lost

290340 successfully added >>> occ=5 set={#5 elts, ["29033b","29033c","29033d",**"29033e"**,**"290340"**]} +35ms

part 6: 5th misshap; item #4 has changed

290341 successfully added >>> occ=5 set={#5 elts, ["29033b","29033c","29033d","29033e",**"290341"**]} +45ms

And here is what I have in MongoDB (queried through mongo shell)

{ "_id" : ObjectId("575f8981762120ce1a290324"), "__occ" : 5, "set" : [ "29033b", "29033c", "29033d", "29033e", "290340" ] }

I was expecting findOneAndUpdate to be atomic yet it behaves weirdly to me. Anyone can help me figure out what I am doing wrong

Thanks in advance

PS: all number were shortened to ease reading but are actual proper MongoDB ID e.g. 575f8981762120ce1a29033b => 29033b

// ------ MAIN CODE ------
var document = 'a previously loaded document from MongoDB'; // has _id and __occ properties
var valueList = [
    "29033b",
    "29033c",
    "29033d",
    "29033e",
    "29033f",
    "290340",
    "290341",
    "290342",
    "290343",
    "290344"
];

var promiseArray = [];
for (let i = 0; i < valueList.length; i++) {
    promiseArray.push(safePush(document, valueList[i]));
}

return when.all(promiseArray)
.then(function(array) {
    // do something with results
})

// ------ FUNCTIONS USED ------
function safePush(obj, value) {
    return $doSafePush(obj, "set", value)
    .then(function(updatedObject) {
        return updatedObject;
    }).catch(function(err) {
        if (err instanceof ConcurrencyDBError) {
            if (retries > 0) {
                return $doSafePush(err.object, "set", value, retries-1);
            } else {
                err.message += " => max number of retries reached, failing";
            } 
        }
        // else
        var message = err.message || "ERROR Unexpected error";
        return when.reject(GenericError.create(message, err));
    });
}

function $doSafePush(obj, property, value) {
    var $query = {
        '_id': obj.id,
        '__occ': obj.__occ
    }
    var $update = {
        "$push": {}
        "$inc": {
            "__occ": 1
        }
    };
    $update["$push"][property] = value; // Update == $push value into 'property' array and $inc __occ counter

    return $getMongoCollection("Users").findOneAndUpdate($query, $update, {returnOriginal: false})
    .then(function(r) {
        var result = r.value;

        if (result == null) {
            // No object matching $query found => OCC error
            // Load new object and reject Promise
            return $getMongoCollection().findOne({_id: new ObjectID(obj._id)})
            .then(function(newObj) {
                return when.reject(new ConcurrencyDBError(util.format("ERROR Concurrency error trying to add %s to %s", value, property), newObj));
            });
        }
        // Else Object was found and updated => return it
        debug("BGCHECK 3 >>> OK >>> %s successfully added >>> occ=%d set={#%d elts, %j}", value, result.__occ, u.getProperty(result, property).length, u.getProperty(result, property));
        return when.resolve(result);
    });
}
Boris
  • 991
  • 1
  • 9
  • 15
  • What happens if you don't use `__occ` at all? – robertklep Jun 14 '16 at 06:09
  • If I don't use `__occ` I have random cases when items are missed but it does work *sometimes*. I also have no way of knowing whether a document that I had loaded has been modified (and hence needs to be reloaded) or not, have I? – Boris Jun 14 '16 at 22:39
  • @robertklep FYI it is working with `__occ`if I add a delay of at least 35ms before trying again (see details above). Would you have any idea why it would behave as such by any chance? Thanks again for your help ! – Boris Jun 14 '16 at 23:04
  • Your use of `__occ` assumes that document _N_ gets saved to the database _after_ document _N-1_, which isn't the case with your code (because it runs all updates in parallel, not sequentially). That's why adding a delay will probably make it work. Here's a simple example that I think mimics what you want to do: [gist](https://gist.github.com/robertklep/ba09f5273a1ceba2d4bbc561900ad1fd). – robertklep Jun 15 '16 at 07:01

0 Answers0