Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DocumentNotFoundError instead of VersionError during save #11295

Closed
mjfwebb opened this issue Jan 29, 2022 · 1 comment
Closed

DocumentNotFoundError instead of VersionError during save #11295

mjfwebb opened this issue Jan 29, 2022 · 1 comment
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it.
Milestone

Comments

@mjfwebb
Copy link

mjfwebb commented Jan 29, 2022

Do you want to request a feature or report a bug?
Bug

What is the current behavior?
This is the same issue as described in #10974

If the current behavior is a bug, please provide the steps to reproduce.

Using the same script created by @IslandRhythms in #10974 (comment)

const mongoose = require('mongoose');
mongoose.set('debug', true)

const robotSchema = new mongoose.Schema({
  name: String
}, { optimisticConcurrency: true });

const Robot = new mongoose.model('Robot', robotSchema);

async function run() {
  mongoose.connect('mongodb://localhost:27017/', {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  });
  await mongoose.connection.dropDatabase();
  const entry = await Robot.create({ name: 'WallE' });
  const changes = await Robot.findOne({ _id: entry._id }).orFail().exec();
  const other = await Robot.findOne({ _id: entry._id }).orFail().exec();
  changes.name = 'John';
  await changes.save();
  other.name = 'WallE';
  await other.save();
}

run();

I am getting a DocumentNotFound:

DocumentNotFoundError: No document found for query "{ _id: new ObjectId("61f578fc226164d0c2fb411e"), __v: 0 }" on model "Robot"

What is the expected behavior?

A VersionError should be thrown, not a DocumentNotFound error.

What are the versions of Node.js, Mongoose and MongoDB you are using? Note that "latest" is not a version.

Mongoose: 6.1.8
Node: v16.6.1

@AbdelrahmanHafez AbdelrahmanHafez added the has repro script There is a repro script, the Mongoose devs need to confirm that it reproduces the issue label Jan 29, 2022
@vkarpov15 vkarpov15 added this to the 6.1.10 milestone Jan 30, 2022
@IslandRhythms IslandRhythms added confirmed-bug We've confirmed this is a bug in Mongoose and will fix it. and removed has repro script There is a repro script, the Mongoose devs need to confirm that it reproduces the issue labels Jan 31, 2022
vkarpov15 added a commit that referenced this issue Feb 5, 2022
@twhy
Copy link

twhy commented Feb 6, 2022

Although this has been fixed, I want to explain the cause of this bug and another possible fix, in case someone else is interested at this.

The cause of this bug is the line of code below. It assigns the same name value as created to other.name.
Remove this line and you still get the same error(DocumentNotFoundError ).

other.name = 'WallE'; 

If you assign a different value to other.name, you'll get VersionError as expected, because the original code covered this case.

other.name = 'Steve';   // get VersionError as expected

But why?
Because other.name = 'WallE'; didn't change other, so mongoose doesn't think other is 'dirty'.
But other.name = 'Steve' does change other, so Mongoose will know other is dirty and handle this differently.

https://github.com/Automattic/mongoose/blob/master/lib/model.js#L314

Model.prototype.$__handleSave = function(options, callback) {
// ...
  const delta = this.$__delta();
  if (delta) {
    // model has some changes, this case works well
  } else {
    // model has no changes, may fix this bug in this else block
  }

Mongoose use this.$__delta() to produces a special query document of the modified properties used in updates.
In the case of other.name = 'WallE';, the value of delta is undefined because other has no changes.
But in the case of other.name = 'Steve', delta is the array below.

[
  { _id: new ObjectId("..."), __v: 0 },
  { '$set': { name: 'Steve' }, '$inc': { __v: 1 } }
]

Since Mongoose has handled this case well, let's see how it handles the other.name = 'WallE'; case.
https://github.com/Automattic/mongoose/blob/master/lib/model.js#L338-L360

  } else {
    const optionsWithCustomValues = Object.assign({}, options, saveOptions);
    const where = this.$__where();
    if (this.$__schema.options.optimisticConcurrency) {
      const key = this.$__schema.options.versionKey;
      const val = this.$__getValue(key);
      if (val != null) {
        where[key] = val;
      }
    }
    // where = { _id: new ObjectId("..."), __v: 0 }
    this.constructor.exists(where, optionsWithCustomValues).
      then((documentExists) => {
        // Check whether document has beed changed before updating
        if (!documentExists) {
          // document has been changed, in this case, __v is 1 in the database
          const matchedCount = 0;
          return callback(null, { $where: where, matchedCount });
        }

        const matchedCount = 1;
        callback(null, { $where: where, matchedCount });
      }).
      catch(callback);
    return;
  }

Later in $__save() method, $__handleSave() is called.

Model.prototype.$__save = function(options, callback) {
  // ...
  if (this.$__.version && !this.$__.inserting) {   // This is the line of code before the fix
    // this.$__.version is undefined so it goes to the next if block
    // We want it go into this if block after the fix
    // ...
    if (numAffected <= 0) {
      // the update failed. pass an error back
      this.$__undoReset();
      const err = this.$__.$versionError ||
        new VersionError(this, version, this.$__.modifiedPaths);
      return callback(err);
    }
    // ...
  }
  if (result != null && numAffected <= 0) {
    // result = { $where: where, matchedCount } in the code block above
    // and hence DocumentNotFoundError instead of VersionError
    error = new DocumentNotFoundError(result.$where,
      this.constructor.modelName, numAffected, result);
    // ...
  }
}

Below is my fix of this bug, and it passes all the latest related tests, so I consider it a possible fix.
Correct me if I was wrong.

  } else {
    const optimisticConcurrency = this.$__schema.options.optimisticConcurrency
    const optionsWithCustomValues = Object.assign({}, options, saveOptions);
    const where = this.$__where();
    if (optimisticConcurrency) {
      // ...
    }
    this.constructor.exists(where, optionsWithCustomValues).
      then((documentExists) => {
        if (!documentExists) {
          if (optimisticConcurrency) { // the fix here
            this.$__.version = VERSION_INC;
          }
          const matchedCount = 0;
          return callback(null, { $where: where, matchedCount });
        }
        // ...
      }).
      catch(callback);
    return;
  }

Basically it sets this.$__.version = VERSION_INC; if document has been changed and and optimisticConcurrency is true. It makes sure that the code goes into the right if block in $__save() method and generate a VersionError when necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
confirmed-bug We've confirmed this is a bug in Mongoose and will fix it.
Projects
None yet
Development

No branches or pull requests

5 participants