-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
BulkSave on entities with virtual properties *and* a transaction: massive performance hit and OOM crashes #14394
Comments
I'm unable to repro, the following script shows that const mongoose = require('mongoose');
void async function main() {
await mongoose.connect('mongodb://127.0.0.1:27017,127.0.0.1:27018/mongoose_test');
const ChildSchema = new mongoose.Schema({ name: String, parentId: 'ObjectId' });
const ChildModel = mongoose.model('Child', ChildSchema);
await ChildModel.deleteMany({});
const ParentSchema = new mongoose.Schema({
name: String
});
ParentSchema.virtual('child1', { ref: 'Child', localField: '_id', foreignField: 'parentId' });
ParentSchema.virtual('child2', { ref: 'Child', localField: '_id', foreignField: 'parentId' });
ParentSchema.virtual('child3', { ref: 'Child', localField: '_id', foreignField: 'parentId' });
ParentSchema.virtual('child4', { ref: 'Child', localField: '_id', foreignField: 'parentId' });
const ParentModel = mongoose.model('Parent', ParentSchema);
await ParentModel.deleteMany({});
const parents = [];
const children = [];
for (let i = 0; i < 100; ++i) {
const parent = await ParentModel.create({ name: 'test parent ' + i });
const child = await ChildModel.create({ name: 'test child ' + i, parentId: parent._id });
parents.push(parent);
children.push(child);
}
await mongoose.connection.transaction(async session => {
const docs = await ParentModel.find().session(session).populate(['child1', 'child2', 'child3', 'child4']);
for (const doc of docs) {
doc.name = 'test parent';
}
const start = Date.now();
await ParentModel.bulkSave(docs, { session });
console.log('BulkSave ms:', Date.now() - start);
});
console.log('Done');
}(); Can you try modifying the above script to demonstrate the issue you're seeing? |
Hi Vitali TL;DR; After realizing it was on us, heading over to Open Collective and making a donation for your lost time was the least I could do. Long version: Thanks to your script, I could narrow down the issue, and ... it's us :( We have a until function that creates a snapshot of our entities in case of a transaction using Removing the |
Thanks for looking into this, I'll check this out and see if I can repro. That does sound extremely slow. |
@hardcodet do your populated subdocs have other virtuals or other populated properties? Because I'm still unable to repro with the following, running in about 15ms on my laptop, no indication of anything that would cause the 'use strict';
const mongoose = require('mongoose');
void async function main() {
await mongoose.connect('mongodb://127.0.0.1:27017,127.0.0.1:27018/mongoose_test');
const ChildSchema = new mongoose.Schema({ name: String, parentId: 'ObjectId' });
const ChildModel = mongoose.model('Child', ChildSchema);
await ChildModel.deleteMany({});
const ParentSchema = new mongoose.Schema({
name: String
});
ParentSchema.virtual('child1', { ref: 'Child', localField: '_id', foreignField: 'parentId' });
ParentSchema.virtual('child2', { ref: 'Child', localField: '_id', foreignField: 'parentId' });
ParentSchema.virtual('child3', { ref: 'Child', localField: '_id', foreignField: 'parentId' });
ParentSchema.virtual('child4', { ref: 'Child', localField: '_id', foreignField: 'parentId' });
const ParentModel = mongoose.model('Parent', ParentSchema);
await ParentModel.deleteMany({});
const parents = [];
const children = [];
for (let i = 0; i < 100; ++i) {
const parent = await ParentModel.create({ name: 'test parent ' + i });
const child = await ChildModel.create({ name: 'test child ' + i, parentId: parent._id });
parents.push(parent);
children.push(child);
}
await mongoose.connection.transaction(async session => {
const docs = await ParentModel.find().session(session).populate(['child1', 'child2', 'child3', 'child4']);
for (const doc of docs) {
doc.name = 'test parent';
}
const start = Date.now();
docs.forEach(doc => doc.toObject({ virtuals: true }));
console.log('BulkSave and toObject ms:', Date.now() - start);
});
console.log('Done');
}(); I'd love to try to repro this slowdown if possible |
It's only one entity type that has virtuals, but those are a few:
Also, we assign the virtuals in our code, so there's no for (const foo of foos) {
foo.parent = parentSetMap.get(foo.parentId);
foo.parentBar = foo.parent.bar;
foo.childs = [ ... ]
foo.childAbstractions = foo.childs.map(c => { child: c, xxx: ... } )
} We already noticed in other places (not related to If it helps, I could send you the actual schemas (would have to be in a DM though) |
You're right, with many children this gets very slow. The following takes 2.5sec: 'use strict';
const mongoose = require('mongoose');
void async function main() {
await mongoose.connect('mongodb://127.0.0.1:27017,127.0.0.1:27018/mongoose_test');
const ChildSchema = new mongoose.Schema({ name: String, parentId: 'ObjectId' });
const ChildModel = mongoose.model('Child', ChildSchema);
await ChildModel.deleteMany({});
const ParentSchema = new mongoose.Schema({
name: String
});
ParentSchema.virtual('child1', { ref: 'Child', localField: '_id', foreignField: 'parentId' });
ParentSchema.virtual('child2', { ref: 'Child', localField: '_id', foreignField: 'parentId' });
ParentSchema.virtual('child3', { ref: 'Child', localField: '_id', foreignField: 'parentId' });
ParentSchema.virtual('child4', { ref: 'Child', localField: '_id', foreignField: 'parentId' });
const ParentModel = mongoose.model('Parent', ParentSchema);
await ParentModel.deleteMany({});
const parents = [];
for (let i = 0; i < 200; ++i) {
const parent = await ParentModel.create({ name: 'test parent ' + i });
const children = [];
console.log(`${i} / 200`);
for (let j = 0; j < 200; ++j) {
children.push({ name: 'test child ' + i + '_' + j, parentId: parent._id });
}
await ChildModel.create(children);
parents.push(parent);
}
await mongoose.connection.transaction(async session => {
const docs = await ParentModel.find().session(session).populate(['child1', 'child2', 'child3', 'child4']);
for (const doc of docs) {
doc.name = 'test parent';
}
const start = Date.now();
docs.forEach(doc => doc.toObject({ virtuals: true }));
console.log('BulkSave and toObject ms:', Date.now() - start);
});
console.log('Done');
}(); I'll take a look and see if we can fix this slowdown |
Prerequisites
Mongoose version
8.2.0
Node.js version
20.x
MongoDB server version
Atlas 7.x
Typescript version (if applicable)
5.2.2
Description
SOLVED - problem due to calls to
toObject
on complex hydrated documents, not the transactions.We noticed massive performance issues in PROD on rather trivial bulk updates. I ran some local tests and narrowed it down to virtual properties in combination with transactions/sessions).
This is the
bulkSave
we perform to update our entities.Our entities have 4 virtual properties, e.g.:
For my tests, I ran the
bulkSave
undefined
before running the updateundefined
before running the update.2 and 3 resulted in the same performance, so I'll just list measures for 1 and 2 going forward.
Test Results
Here's some measurements with a clear outlier:
With bigger arrays, the performance degradation is absolutely fatal, resulting in timeouts etc.
A test with 10K entities crashed my process because the JS heap ran out of memory (
V8::FatalProcessOutOfMemory+662
))This looks like a bug to me, as the virtual properties shouldn't even be touched. My guess is that mongoose iterates through those virtual properties in order to detect changes rather than skipping them, which seems to introduce a massive overhead. That wouldn't explain why it only occurs with a transaction/session in the mix.
Right now, this really is a show stopper for us. Is there a better short-time solution than setting those properties to undefined before every update, and then reassigning them?
Thanks!
Steps to Reproduce
bulkSave
in order to persist the changes.Note in case it matters: some the virtual properties in our case are related mongoose entities.
Expected Behavior
No performance degradation at all from virtual properties. Also, virtual properties and transactions should not affect each other.
The text was updated successfully, but these errors were encountered: