Is this transaction safe from race conditions?

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.

1 Like

Hi @Florian_Walther

Are operations within MongoDB transactions executed one after another

MongoDB’s transaction is using the usual ACID guarantees (see What are ACID Properties in Database Management Systems?)

so another caller running in parallel can see the first half of the transaction already applied to the DB, but not the second half

No. This is counter to the ACID property explained in the article linked above. It’s a pretty broken database system if this is allowed to happen.

I want to make sure that this endpoint can be called in parallel (or very fast succession) without causing any wrong updates.

With the ACID guarantees of a multi-document transaction, you’ll be able to safely do this. The whole transaction is an all-or-nothing proposition, so either all operations in the transaction succeed, or none of them, and it’s as if that transaction was never performed. However:

Here’s a potential situation that I am worried about

If I understand the scenario correctly, you’re worried about stale reads (when the transaction is seeing a snapshot of the previous data state at the time when the transaction starts, and makes decisions based on outdated information). Is this correct? If yes, I think this is a valid concern. In this case, you may want to check out In-progress Transactions and Stale Reads on how to cater for this scenario.

Best regards
Kevin

1 Like

@kevinadi

Thank you for the explanation! The worry-case I described can only happen if one caller can overtake the other (and see only parts of the changes the other caller is in the process of doing right now). From your explanation, I understand that this can’t happen.

I only change points in reaction to a successful document deletion (with a return value) or creation (where the primary key should make duplicates impossible).

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.