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 ofop
isnone
means there is no operation on this document. When try to increase theage
, set the value ofop
to 'pending' and then reset tonone
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.