I want to make sure that a transaction I’m running is safe from race conditions. I’m using Mongoose’s withTransaction.
Are operations within MongoDB transactions executed one after another (so another caller running in parallel can see the first half of the transaction already applied to the DB, but not the second half), or are the changes only visible once the whole transaction is done?
I want to make sure that this endpoint can be called in parallel (or very fast succession) without causing any wrong updates.
await session.withTransaction(async () => {
const existingVote = await ResourceVote.findByIdAndDelete(voteId).session(session).exec();
if (existingVote?.upDown === 'up') {
await Resource.findByIdAndUpdate(resourceId, { $inc: { votePoints: -1 } }).session(session).exec();
const deletedKarmaLogEntry = await KarmaLogEntry.findOneAndDelete({ upvotedResource: resource._id, upvotingUser: authenticatedUserId }).session(session).exec();
if (deletedKarmaLogEntry) {
await UserModel.findByIdAndUpdate(resource.submittingUser, { $inc: { karma: -upVoteKarmaPoints } }).session(session).exec();
}
} else if (existingVote?.upDown === 'down') {
await Resource.findByIdAndUpdate(resourceId, { $inc: { votePoints: 1 } }).session(session).exec();
}
if (existingVote?.upDown !== upDown) {
await ResourceVote.create([{
_id: voteId,
userId: authenticatedUserId,
resourceId: resourceId,
upDown: upDown,
}], { session: session });
if (upDown === 'up') {
await Resource.findByIdAndUpdate(resourceId, { $inc: { votePoints: 1 } }).session(session).exec();
await KarmaLogEntry.create([{
user: resource.submittingUser,
points: upVoteKarmaPoints,
upvotedResource: resource._id,
upvotingUser: authenticatedUserId,
}], { session: session });
await UserModel.findByIdAndUpdate(resource.submittingUser, { $inc: { karma: upVoteKarmaPoints } }).session(session).exec();
} else if (upDown === 'down') {
await Resource.findByIdAndUpdate(resourceId, { $inc: { votePoints: -1 } }).session(session).exec();
}
}
});
Here’s a potential situation that I am worried about: Let’s say the existingVote
is down
and we send 2 upvotes in parallel. The first upvote deletes the existing downvote and executes the block below . The second caller skips the deletion and therefore the block below it. Now the second caller overtakes the first caller, creates a new ResourceVote
, and applies all the points and creates the documents. Now the first caller fails at the ResourceVote.create
because the unique id constraint kicks in, and therefore rolls back all previous changes, recreating the originally deleted existing vote.