From 29a02a4416fbaf139df522071f176755e32f6092 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 13 Feb 2025 09:44:09 -0500 Subject: [PATCH] perf(document): only call `undoReset()` 1x/document Fix #15255 --- lib/document.js | 3 +++ test/document.test.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/lib/document.js b/lib/document.js index ffba23c071f..cee62ccfe9c 100644 --- a/lib/document.js +++ b/lib/document.js @@ -3540,6 +3540,9 @@ Document.prototype.$__reset = function reset() { */ Document.prototype.$__undoReset = function $__undoReset() { + if (this.$isSubdocument) { + return; + } if (this.$__.backup == null || this.$__.backup.activePaths == null) { return; } diff --git a/test/document.test.js b/test/document.test.js index 969d0fa8d54..aff51d64093 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12,6 +12,7 @@ const ArraySubdocument = require('../lib/types/arraySubdocument'); const Query = require('../lib/query'); const assert = require('assert'); const idGetter = require('../lib/helpers/schema/idGetter'); +const sinon = require('sinon'); const util = require('./util'); const utils = require('../lib/utils'); @@ -14296,6 +14297,47 @@ describe('document', function() { delete mongoose.Schema.Types.CustomType; }); + + it('handles undoReset() on deep recursive subdocuments (gh-15255)', async function() { + const RecursiveSchema = new mongoose.Schema({}); + + const s = [RecursiveSchema]; + RecursiveSchema.path('nested', s); + + const generateRecursiveDocument = (depth, curr = 0) => { + return { + name: `Document of depth ${curr}`, + nested: depth > 0 ? new Array(2).fill().map(() => generateRecursiveDocument(depth - 1, curr + 1)) : [], + __v: 5 + }; + }; + const TestModel = db.model('Test', RecursiveSchema); + const data = generateRecursiveDocument(10); + const doc = new TestModel(data); + await doc.save(); + + sinon.spy(Document.prototype, '$__undoReset'); + + try { + const d = await TestModel.findById(doc._id); + d.increment(); + d.data = 'asd'; + // Force a version error by updating the document directly + await TestModel.collection.updateOne({ _id: doc._id }, { $inc: { __v: 1 } }); + const err = await d.save().then(() => null, err => err); + assert.ok(err); + assert.equal(err.name, 'VersionError'); + // `$__undoReset()` should be called 1x per subdoc, plus 1x for top-level doc. Without fix for gh-15255, + // this would fail because `$__undoReset()` is called nearly 700k times for only 2046 subdocs + assert.strictEqual(Document.prototype.$__undoReset.getCalls().length, d.$getAllSubdocs().length + 1); + assert.ok(Document.prototype.$__undoReset.getCalls().find(call => call.thisValue === d), 'top level doc was not reset'); + for (const subdoc of d.$getAllSubdocs()) { + assert.ok(Document.prototype.$__undoReset.getCalls().find(call => call.thisValue === subdoc), `${subdoc.name} was not reset`); + } + } finally { + sinon.restore(); + } + }); }); describe('Check if instance function that is supplied in schema option is available', function() {