Concurrent WriteConflict with MongoDB and NodeJs - node.js

I'm creating an application that serves transportation order, while I'm stress testing the application by injecting about 100 orders simultaneously, the mongodb hits WriteConflict due to the numeric sequence id (nid it is required) that I created using the hook. I have tried to add ?retryWrites=true&w=majority into the connection URI and set those in the transaction level, but none of those work. May I know any parts that I'm missing and should give a try (Knowing that is critical section, and using lock/queue should fix, but want to know any other way)? Thank you very much.
The schema for sequence and order (will keep _id that default created, and mongoose did the thing so haven't mention in the schema)
const SequenceSchema = new Schema<ISequenceDoc>(
{
collectionName: {
type: String,
trim: true,
required: true
},
nid: {
type: Number,
required: true
}
},
{
collection: 'sequences',
timestamps: true
}
);
const OrderSchema = new Schema<IOrderDoc>(
{
name: {
type: String,
trim: true,
required: true
},
nid: {
type: Number
}
},
{
collection: 'orders',
timestamps: true
}
);
Pre saving hook to assign an numeric id index of hex id to the schema
OrderSchema.pre('save', async function(next) {
if (this.isNew) {
const data = await this.db.model<ISequenceDoc>('Sequence').findOneAndUpdate(
{
collectionName: this.collection.collectionName
},
{
$inc: { nid: 1 }
},
{
upsert: true,
new: true,
session: this.$session()
}
);
this.set('nid', data.nid);
}
next();
});
The code that create the order model and save
async createOrder(orderInfo) => {
const session = await this.connection.startSession();
session.startTransaction({
readConcern: {
level: 'majority'
},
writeConcern: {
w: 'majority',
j: true
}
});
......
await order.save({session});
await session.commitTransaction();
return order;
}

The problem is that all of your transactions modify the same document in the findOneAndUpdate.
Try acquiring your sequence number before starting the transaction.

Related

Implementing post('updateOne') hook middleware with Mongoose(MongoDB)

I am trying to calculate the average value of the ratings for a Product per Document that is being rated. Instead of calculating the average rating of the Product every time when we need the value. I calculate it every time someone rates it. To accomplish this task I am implementing a post(‘updateOne’) hook middleware. Though not sure I can accomplish this by implementing this hook. Let me know if I am going into the wrong direction. Here is the error I am getting avgRating error from post updateOne hookTypeError: Cannot read property 'aggregate' of undefined.
file - Product.js
import mongoose from 'mongoose';
const { ObjectId } = mongoose.Schema;
const ParentSchema = new mongoose.Schema(
{
title: {
type: String,
trim: true,
required: true,
maxlength: 32,
text: true,
},
slug: {
type: String,
unique: true,
lowercase: true,
index: true,
},
price: {
type: Number,
required: true,
trim: true,
maxlength: 32,
},
quantity: Number,
ratings: [
{
star: Number,
postedBy: { type: ObjectId, ref: 'User' },
},
],
},
{
timestamps: true,
}
);
const avgRating = async function () {
try {
const stats = await this.aggregate([
{
$addFields: {
avgRating: { $avg: '$ratings.star' },
nRatings: { $size: '$ratings' },
},
},
]);
console.log('stats: ', stats);
} catch (err) {
console.log(`avgRating error from post updateOne hook${err}`);
}
};
// Call avgRating after updateOne
ParentSchema.post(
'updateOne',
{ document: true, query: false },
async function () {
await avgRating();
}
);
export default mongoose.models.Product ||
mongoose.model('Product', ParentSchema);
file - productServices.js
import Product from './Product.js';
const updateRating = async (existingRatingObject, star) => {
const query = {
ratings: { $elemMatch: existingRatingObject },
};
const update = { $set: { 'ratings.$.star': star } };
const option = { new: true };
try {
const doc = new Product();
const ratingUpdated = await doc.updateOne(query, update, option);
return ratingUpdated;
} catch (error) {
console.log('product model updateRating error: ', error);
}
};
Your avgRating function does not have access to the mongoose document under this. That is why you are getting the error, Cannot read property 'aggregate' of undefined, because this (the document) which you want to modify does not exists in the function.
You need to use an instance method available under mongoose. With instance method, you can call this to reference the document.
Check out:
https://www.freecodecamp.org/news/introduction-to-mongoose-for-mongodb-d2a7aa593c57/#instace-methods
https://mongoosejs.com/docs/guide.html#methods

In mongoose set a field based on the value of another field in findOneAndUpdate

I'm working on a project where in one model I need to set the value of a field based on another fields value. Let me explain with some code.
Destination model
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const DestinationSchema = new Schema({
name: {
type: String,
required: true
},
priority: {
type: Number,
default: 0,
max: 10,
required: true
}
})
DestinationSchema.statics.getPriority = function(value) {
return this.findOne({ _id: value })
}
const Destination = mongoose.model('Destination', DestinationSchema)
exports.Destination = Destination
Task model
const mongoose = require('mongoose')
const { Destination } = require('../_models/destination.model')
const Schema = mongoose.Schema;
const TaskSchema = new Schema({
priority: {
type: Number,
required: true,
min: 0,
max: 25
},
from: {
type: Schema.Types.ObjectId,
ref: 'Destination',
required: true
},
to: {
type: Schema.Types.ObjectId,
ref: 'Destination',
required: true
},
type: {
type: Number,
required: true,
min: 0,
max: 3
}
}, {
timestamps: true
})
TaskSchema.pre('save', async function () {
this.priority = await Destination.getPriority(this.from).then(doc => {
return doc.priority
})
this.priority += await Destination.getPriority(this.to).then(doc => {
return doc.priority
})
this.priority += this.type
})
Task Controller update function
exports.update = async function (req, res) {
try {
await Task.findOneAndUpdate({
_id: req.task._id
}, { $set: req.body }, {
new: true,
context: 'query'
})
.then(task =>
sendSuccess(res, 201, 'Task updated.')({
task
}),
throwError(500, 'sequelize error')
)
} catch (e) {
sendError(res)(e)
}
}
When I create a new Task, the priority gets set in the pre save hook just fine as expected. But I'm hitting a wall when I need to change Task.from or Task.to to another destination, then I need to recalculate the tasks priority again. I could do it on the client side, but this would lead to a concern where one could just simply send a priority in an update query to the server.
My question here is, how can I calculate the priority of a Task when it gets updated with new values for from and to? Do I have to query for the document which is about to get updated to get a reference to it or is there another cleaner way to do it, since this would lead to one additional hit to the database, and I'm trying to avoid it as much as possible.
In your task schema.
you have to use pre("findOneAndUpdate") mongoose middleware. It allows you to modify the update query before it is executed
Try This code:
TaskSchema.pre('findOneAndUpdate', async function(next) {
if(this._update.from || this._update.to) {
if(this._update.from) {
this._update.priority = await Destination.getPriority(this._update.from).then(doc => {
return doc.priority
});
}
if(this._update.to) {
this._update.priority += await Destination.getPriority(this._update.to).then(doc => {
return doc.priority
});
}
}
next();
});

Mongoose: findByIdAndUpdate method is not updating nor inserting

The findByIdAndUpdate method should update or insert an object in the DB, but it does nothing. Nor it does throw an error message or something.
I also tried it with the original ObjectId as _id field, but doesn't work either.
Has anybody a clue what is missing to update or insert the object into the DB?
const schema = new Schema({
_id: {
type: Number,
required: true
},
name: {
type: String,
required: true
}
}, { toJSON: { virtuals: true } });
const myJson = {
"myobject": {
"_id": 781586495786495,
"name": "MyName"
}
}
const MyModel = mongoose.model('MyModel', schema);
MyModel.findByIdAndUpdate(myJson.myobject._id, myJson.myobject, { upsert: true });
I used your code and it actually works as intended. The reason why it doesn't return any errors nor does anything might be that you're not connected to the database at the time you're executing the operation on the model.
Please consider the following snippet, it worked for me.
mongoose.connect('mongodb://localhost:27017/testingcollection').then(() => {
const schema = new mongoose.Schema({
_id: {
type: Number,
required: true
},
name: {
type: String,
required: true
}
}, { toJSON: { virtuals: true } });
const myJson = {
"myobject": {
"_id": 781586495786495,
"name": "MyName"
}
};
const MyModel = mongoose.model('MyModel', schema);
MyModel.findByIdAndUpdate(myJson.myobject._id, myJson.myobject, { upsert: true }).then(obj => {
mongoose.connection.close();
});
});

Mongoose query not returning updated information with { new: true } param

Why is my query not returning updated information?
UserSchema.findByIdAndUpdate(
{ _id: userId },
{ $set: { couponList: couponList } }, { new: true }).populate('couponList').exec().then(user => {
// user returning with the old information
}).catch(err => console.log(err));
I have 3 params:
first one is the id of the user i want to update (objectId)
second one is the information I want to update (objectId Array)
third is the flag that says I want to receive the updated information (Boolean)
My coupon schema goes like this:
import mongoose from 'mongoose';
const CouponSchema = new mongoose.Schema({
name: {
type: String,
default: 'Unknown'
},
description: {
type: String,
default: undefined
},
validity: {
type: Date,
default: null
},
code: {
type: String,
default: undefined
},
blackList: {
type: Array,
ref: 'user',
default: []
},
blackListFlag: {
type: Boolean,
default: false,
},
whiteList: {
type: Array,
ref: 'user',
default: []
},
limit: {
type: Number,
default: 0,
},
counter: {
type: Number,
default: 0,
},
amount: {
type: Number,
default: 0,
},
discountType: {
type: String,
default: undefined,
}
}, { collection: 'coupon' });
export default mongoose.model('coupon', CouponSchema);
And in my user schema I have a ref to the coupon schema:
couponList : {
type: Array,
ref: 'coupon',
default: []
},
I think you need to define the field couponList in your schema.
Edit: Just noticed the UserSchema, theoretically, you should be fine, if you are pushing correct objectIds.
findByIdAndUpdate with {new: true} must work as intended.
But I'm not aware of Your code totally and what kind of data You're sending as couponList.
So try to separate update and select operations and see what happens. In fact mongoose does the same when You call findByIdAndUpdate.
For example using express framework:
const User = mongoose.model('user');
router.put('/user/:userId/coupons', async (req, res) => {
try {
const {userId} = req.params;
const {couponList} = req.body;
await User.updateOne(
{_id: userId},
{$set: {couponList: couponList}},
{upsert: false}
);
const user = await User
.findById(userId)
.populate('couponList').lean();
res.status(200).send(user);
}
catch (error) {
console.log(error);
res.status(500).send({})
}
});
P.S. Only reason for that unexpected behavior may be that somehow (but it's not possible) it uses native driver for which {new: true} must be written as: {returnNewDocument: true}
Check this link
I found out that the problem was not with returning updated information but it was on populating the collection.
The correct reference to the coupon collection in user schema:
couponList: [ { type: mongoose.Schema.ObjectId, ref: 'coupon' } ],

SailsJS Perform sorting on populate data

Here is my current event model:
module.exports = {
attributes: {
name: {
type: 'string',
required: true,
unique: true
},
client: {
model: 'client',
required: true
},
location: {
model: 'location',
required: true
}
}
};
The client Model:
module.exports = {
attributes: {
name: {
type: 'string',
required: true,
unique: true
},
address: {
type: 'string'
},
clientContact: {
model: 'user',
required: true
}
}
};
So how do I implement sorting based on the client name and also have the skip and limit property(for pagination) to work along with it.
I tried using the following query:
Event.find({ id: ['583eb530902db3d926345215', '583eb6dd91b972ee26dd97b1'] },
{ select: ['name', 'client'] })
.populate('client', { sort: 'name DESC' })
.exec((err, resp) => resp.map(r => console.log(r.name, r.client)));
But this does not seem to do it.
Waterline doesn't support sorting a result by child records like this. If client was a collection of child records attached to an event, then your code would sort each of the returned Event record's populated client array by name, but I'm assuming in this case client is a singular association (i.e. model: 'client').
If what you want is an array of Event records sorted by the name of their client, you can do it relatively easily using Lodash after you retrieve the records:
var _ = require('lodash');
Event.find(queryCriteria).populate('client').exec((err, resp) => {
if (err) { /* handle error */ }
var sortedRecords = _.sortBy(eventRecords, (record) => { return record.client.name; });
});

Resources