Home > database >  How to make update command with $inc operator idempotent in MongoDB?
How to make update command with $inc operator idempotent in MongoDB?

Time:08-25

As we know, the $inc, $mul, $add operators of update command are not idempotent in MongoDB. We want to know how to make the update command with $inc operator idempotent.


Given the collections users with one document {"_id" : "123", "age" : 24, "name": "me"}, we want to increase the age by 1 through db.users.update({_id: "123"}, {$inc: {age: 1}}). Obviously, this update command is not idempotent, because the age would be increased by 2 when this update command is executed twice by accident.

What we have tried.

  • Replace $inc with $set, since $set operator could be considered idempotent.

    var doc = db.users.findOne({_id: "123"})
    var oldAge = doc.age
    var newAge = oldAge   1
    db.users.updateOne({_id: "123", age: oldAge},{$set:{ age: newAge}})
    

    However, this solution is failed to keep update command idempotent.

  • Add one more field like "op": "pending" to act as a lock, the default value of op is none means there is no operation on this document. When try to increase the age, set the value of op to 'pending' and then reset to none after the increase operation is done.

    var doc = db.users.findOne({_id: "123", "op": "none"})
    if (doc.op === "none") {
       db.users.updateOne({_id: "123"},{$set:{ op: 'pending'}})
       db.users.updateOne({_id: "123", op: 'pending'},{$inc: {age: 1}, $set:{ op: 'none'}})
    }
    

    The issue of this solution is that it is hard to keep the above operation atomic.

  • Add one more fields like "version": 1 to act as one optimistic lock.

    while (true) {
      var doc = db.users.findOne({_id: "123"})
      var curVersion = doc.version   1
      db.users.updateOne({_id: "123"},{$set:{ version: curVersion}})
      var ret = db.users.updateOne({_id: "123", version: curVersion},{$inc: {age: 1}})
      if (ret.modifiedCount == 1) {
         break; 
      }
    }
    

    The issue of this solution is the bad efficeicy when there are multiple inc operation simutinaously.

Is there any better soltion to make the update command with $inc operator idempotent?


The requirement is based on the real case that the AWS documentDB is used in my case, the primary node of documentDB could not be available for several seconds during the maintenance period. Per AWS support guys, we should make the update command with $inc operator idempotent within the retry strategy to handle the update command failure in the maintenance period.

CodePudding user response:

This usecase and edge case you're describing sound very bizarre to me but here's how I would deal with such an issue:

For each field you want to update, you have to keep track of it's last update somehow - otherwise there is never a way to guarantee no multiple updates happen, easiest way is to add a field on the document but this can also be tracked in a separate collection. For the purpose of this answer I will assume only the age field can be $inc'd.

So as I mentioned i'd add the age_update_time, then I can just execute the following update without worry:

const aYearAgo = new Date(new Date().getTime() - 1000 * 60 * 60 * 24 * 365)
db.users.updateOne(
    {
        _id: "123",
        $or: [
            {   // either it's been a year since last update.
                age_update_time: {$gt: aYearAgo}
            },
            {   // or an update is due.
                age_update_time: {$exists: false}
            }
        ]
    },
    {
        $set: {
            age_update_time: new Date()
        },
        $inc: {
            age: 1
        }
    }
)

Because mongo's updates are atomic this guarantee's that only a single update will get executed once a year.

Again as mentioned this usecase sounds very bizarre, I understand this is probably a toy example - but if you could share more details as to why this behavior exists in your system I might be able to offer better advice.

CodePudding user response:

There could another solution through $addToSet

The $addToSet operator adds a value to an array unless the value is already present, in which case $addToSet does nothing to that array.

code snippet

var opId = new ObjectId()
db.users.updateOne({"_id" : "123"}, {$addToSet: {ops: opId}})
db.users.updateOne({"_id" : "123, ops: opId}, {$inc:{age:1}, $pull:{ops:opId}})

Since the $addToSet could make the update idempotent, remove the operation id after update command is done.

  • Related