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);
});
}