mongodbatomic

dynamic atomic change in Mongodb, avoiding race conditions


We have these objects for which we want to retain user-defined order. For example:

class Book:
    id: ObjectId
    order: int


class ClubBookRanking:
    books: list[Book]

Before you suggest, relying on the list order doesn't work here. It just shifts the problem.

We store these objects in mongo. There are two kinds of operations we need to perform here.

  1. Add a new book: this should append to the end of the list (order should be equal to list length when including the new item)
  2. User re-ordering: the book must change it's order value, and all books that come after it must also have their order updated.

These situations are both vulnerable to race conditions. My instinct is to attempt to do these things in complex atomic transactions. I am assuming that mongo will execute atomic transaction sequentially and race conditions will therefore be avoided (ala redis).

Does anyone know the best way of handling this? We do use Atlas, so we can use their functionality.

Edit: The books have a uuid


Solution

  • To elaborate what as Joe said, you can really benefit from guaranteed order of elements in an array and atomicity of updates on document level.

    ClubBookRanking is a single document, and all books are ordered within the books array.

    All you need is to ensure no concurrent updates to this document which is easily resolved with revisions, e.g. see how mongoose implemented it with versionKey https://github.com/Automattic/mongoose/blob/e359b99e0d1a15669143363855207660aa508fb9/docs/guide.md#option-versionkey

    Essentially you add a monotonically incremented revision number to ClubBookRanking, which you increment on each update. In pseudocode:

    const cbr = db.ClubBookRanking.findOne(); //the document
    const __v = cbr.__v;  //the revision number
    reorder(cbr.book); // reshuffle the books
    const result = db.ClubBookRanking.updateOne(
        {
            _id: cbr._id,   // filter by unique id
            __v: __v        // and only when revision didn't change since we read the document
        }, {
            $set: {books: cbr.books}, // the new order
            $inc:{__v:1}              // increment the revision
        })
        if (result.nModified < 1 ) {
             throw new Error("Concurrent update") 
        }
    );
    

    Here we try an optimistic update, assuming there is no concurrent updates, so that updateOne updates at most 1 document. If there was a concurrent update, the __v won't match the filter, and no updates will happen - result.nModified will be 0.

    Now it's up to you how to resolve the conflict - you can do something smart by fetching the new revision and trying to re-apply the reordering to the latest revision programmatically, or through it back to the user saying that someone else update the order quicker than them, and ask to review and reorder.