skip to Main Content

How do I prevent this senario
Request 1:I fetch document A and change something
Request 2:I fetch document A and change something else
Request 1:I save document A
Request 2:I save document A
Now the changes from Request 1 will be overwritten How do I prevent this senario

if this request was called often asyncly a change wouldn’t be saved its just a example
I have more complex code in the application were the chance is higher

exports.readInfo = async (req, res, n) => {
  const user = req.user;
  const data = req.data;
  const doc = await Doc.findOne({ _id: new ObjectId(data._id) });
//changes from request 1 might be saved now
  doc.infos[data.key][data.skey][data.i].read.push(user._id.toString());
  doc.markModified("infos." + data.key + "." + data.skey + "." + data.i);
  await doc.save();
  return res.end("success");
};

2

Answers


  1. This is more of a systems design issue than anything else, as JavaScript doesn’t really have anything built in (that I’m aware of anyway) to deal with it.

    Your issue is basically a race condition, as described here: https://stackoverflow.com/a/34550/8346513

    In short, two processes are trying to modify the same piece of data at the same time, and which one wins is a little arbitrary and hard to pin down.

    There are a few ways to handle this problem.

    Use a mutex, or lock, on the resource.

    This is a simple way of preventing this sort of issue from happening. It would look something like this:

    const locks = {};
    
    exports.readInfo = async (req, res, n) => {
        const key = // generate key somehow from your request
        // wait for there to be no lock. This may require a timeout or something of the like
    
        locks[key] = true; // acquire the lock, now we own the lock and nothing else can take actions on this resource
    
        // do our actions
    
        locks[key] = false; // release the lock when done
    }
    

    This is relatively cheap, and because JavaScript is single threaded, works fairly well for this case. However it has some downsides, namely that you have to have this same lock in every place this resource might be used (which can be a lot of boilerplate) and also if your process crashes in the middle of working, it never lets up the lock, which could cause your application to deadlock.

    Use a history queue to process changes

    This is similar to the above example, where only one thing can make changes at a time. But now instead of putting that thing inside your API like this, you would have a system that runs through the following steps:

    • Enqueue the change to be made
    • Periodically run a process that picks up changes from the queue and writes them.

    This ensures that only one place is ever writing your data, and allows much less boilerplate to be needed. However this also has some issues. Namely it is much more complex to setup, and also all your operations will run with out of date data.

    You could also use a simpler in-memory caching layer to keep a live copy of your data. If you modify that, then enqueue that something changed, then you can update data without needing to do it async, which means your requests don’t need to block anymore, but then periodically send that data to the database. This is also complex and does fix some issues of the above, but also runs the risk of not fully writing all data if the program crashes.

    Conclusion

    So as you can see, it’s not a simple thing to fix. I would recommend the simple mutex as it’s probably the easiest thing to get up and running for an application that doesn’t need to be perfect. But if you’re building an enterprise grade application it might be good to look into one of the other methods or something else (probably an off the shelf solution) to ensure that as many bases are covered as possible.

    Login or Signup to reply.
  2. you can use this solution

    db.coll.findAndModify({ status : "unlocked" },{$set : { status : "locked"}});
    

    But returns nothing when the status is locked

    For a better solution you need to implement lock functionality in the application layer (I think Mongodb not implemented this, But Postgres did)

    Simple example

    import { Types } from "mongoose";
    
     export class Locking {
      locked_documents = new Set<Types.ObjectId>();
      pending_documents = new Map<Types.ObjectId, NodeJS.Timeout[]>();
      removed_intervals: NodeJS.Timeout[] = [];
    
      INTERVAL_TIME = 1000;
      TIME_OUT_COUNT = 60;
      
      add(_id: Types.ObjectId) {
        this.locked_documents.add(_id);
      }
      
      remove(_id: Types.ObjectId) {
        this.locked_documents.delete(_id);
        const pending_document_list = this.pending_documents.get(_id);
    
        if(pending_document_list) {
          const pending_document = pending_document_list.shift();
          if(pending_document) this.removed_intervals.push(pending_document);
          this.locked_documents.add(_id);
        }
      }
      
      async delay(_id: Types.ObjectId) {
        const promise = new Promise<void>((resolve, reject) => {
          let count = 1;
          const timeout = setInterval(() => {
            if(count === this.TIME_OUT_COUNT) {
              reject("Time out");
              return;
            }
            console.log("Count", count);
            const removedIntervalIndex = this.removed_intervals.findIndex((t) => t === timeout);
            if(removedIntervalIndex > -1) {
              clearInterval(timeout);
              this.removed_intervals.splice(removedIntervalIndex, 1);
              console.log("Resolved. Count: ", count);
              resolve(undefined);
            }
            count++;
          }, this.INTERVAL_TIME);
    
          const pending_document_list = this.pending_documents.get(_id);
          if(pending_document_list) {
            pending_document_list.push(timeout);
          } else {
            this.pending_documents.set(_id, [timeout]);
          }
        });
    
        return promise;
      }
    }
      
    const locking = new Locking();
      
    const startTime = Date.now();
      
    const doc1 = {
      _id: new Types.ObjectId(),
      name: "Name1",
    };
      
      
    async function test() {
      locking.add(doc1._id);
        
      setTimeout(() => {
        doc1.name = "Updated1";
        locking.remove(doc1._id);
      }, 10000);
    
      await locking.delay(doc1._id);
    
      console.log("Waited", Date.now() - startTime, doc1.name);
    
      locking.add(doc1._id);
    
      setTimeout(() => {
        doc1.name = "Updated2";
        locking.remove(doc1._id);
      }, 1500);
    
      await locking.delay(doc1._id);
    
      console.log("Waited", Date.now() - startTime, doc1.name);
      
      locking.add(doc1._id);
    
      setTimeout(() => {
        doc1.name = "Updated3";
        locking.remove(doc1._id);
      }, 5000);
    
      await locking.delay(doc1._id);
    
      console.log("Waited", Date.now() - startTime, doc1.name);
    }
      
    test();
    

    The Result:

    Count 1
    Count 2
    Count 3
    Count 4
    Count 5
    Count 6
    Count 7
    Count 8
    Count 9
    Count 10
    Resolved. Count:  10
    Waited 10013 Updated1
    Count 1
    Count 2
    Resolved. Count:  2
    Waited 12016 Updated2
    Count 1
    Count 2
    Count 3
    Count 4
    Count 5
    Resolved. Count:  5
    Waited 17023 Updated3
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search