Andrew Rasmussen
Andrew Rasmussen

Reputation: 15099

Find and update multiple documents atomically

I'm trying to implement an email notifications feature. I have a notifications collection where each document has the following:

{
  userID: {type: ObjectId, index: true, required: true},
  read: {type: Boolean, default: false, index: true},
  emailed: {type: Boolean, default: false, index: true},
}

I have a node CronJob that once a day calls a function which is supposed to do the following (pseudocode):

foreach (user)
  db.notifications.find({
    userID: user._id,
    read: false,
    emailed: false
  }, function(e, notifications) {
    set emailed to true
    sendNotificationsEmail(user, notifications)
  });

However I can't figure out a way to fetch the relevant notifications and mark them as "emailed" in an atomic way such that if this code is executing on multiple servers at the same time there won't be a race condition where the user receives multiple emails.

Any ideas?

Upvotes: 0

Views: 247

Answers (1)

Andrew Rasmussen
Andrew Rasmussen

Reputation: 15099

The following question and answer was extremely helpful: Solution to Bulk FindAndModify in MongoDB

Here's my solution:

  1. Replace the Boolean emailed field with a String emailID field. Give each machine/reader a uniquely generated ID, emailID.
  2. Find all of the relevant notifications
  3. Update them with:

    db.notifications.update( {_id: {$in: notificationIDs}, emailID: null, $isolated: true}, {$set: {emailID: emailID}}, {multi: true}

  4. Find notifications with emailID set.

The trick is that with $isolated: true, either the whole write will happen or none of it. So if some other reader has already claimed the notifications with its emailID then this update will not go through and you can guarantee that one reader's update will finished before the other starts.

findEmailNotifications: function(user, emailID, callback) {
  Notification.find({
    read: false,
    deleted: false,
    userID: user._id,
    emailID: null,
  }, function(findError, notifications) {
    // handle findError

    var notificationIDs = getKeys(notifications, '_id');
    if (notificationIDs.length === 0) {
      callback(null, []);
      return;
    }

    Notification.update(
      {_id: {$in: notificationIDs}, emailID: null, $isolated: true},
      {$set: {emailID: emailID}},
      {multi: true},
      function(updateError) {
        // handle updateError

        Notification.find({
          read: false,
          deleted: false,
          userID: user._id,
          emailID: emailID
        }, function(findError, notifications) {
          // handle findError

          callback(null, notifications);
        });
      }
    );
  });
},

No thanks to downvoters who downvoted a well-formed question without specifying any valid reason. Hope this helps someone else.

Upvotes: 2

Related Questions