MongoError: WriteConflict - node.js

When i send multi request from frontend (react) to backend (nodejs) simultaneously, the server gives me this error MongoError: WriteConflict. i am using transaction when i want to update some data in database:
code:
const db = require("../../utils/database-connection/database");
const FollowersFollowing = require("../../models/followers-following/followers-following-models");
const session = db.client().startSession();
session.startTransaction();
let whoFollowed;
try {
whoFollowed = await FollowersFollowing.updateFollow(
userId,
targetUserId,
{ session },
"follow"
);
await Notice.updateNotice(
userId,
targetUserId,
{ session },
"showUserOnTargetUserNotice"
);
await session.commitTransaction();
session.endSession();
} catch (error) {
console.log(error);
await session.abortTransaction();
return next(
new HttpError("could not follow the user, please try again.", 500)
);
}
all things works fine but when i want to send multi request (just for testing) the server throw this error.

See https://docs.mongodb.com/manual/core/transactions/ for the correct usage patterns. You must retry on those errors which withTransaction helper does for you.

Related

Sequelize not retrieving all data after insert

I have noticed that my backend is not retrieving the expected data after an insert.
In my React application, I have one function which inserts data into the database and after getting a response, a new request is sent to update the current component state with the newly fetched data.
All my functions are using await/async and in the backend, all transactions are correctly used and committed in order.
My client is calling the following endpoints:
-POST: api/ticket ( INSERT AN ITEM)
-GET: api/ticket (GET ALL ITEMS)
Here is what the backend is showing which looks correct to me, the problem is that in the 'SELECT' statement, the inserted item is not retrieved.
The transactions are started from two different routes but I don't see why it should be an issue.
In addition, I tried to change the AddItem function to output the same findAll statement which is called when using the GET method and the data returned are correct.
So why if I separate these two flows I do not get all the items? I always need to refresh the page to get the added item.
START TRANSACTION;
Executing (a9d14d5c-c0ac-4821-9b88-293b086debaa): INSERT INTO `messages` (`id`,`message`,`createdAt`,`updatedAt`,`ticketId`,`userId`) VALUES (DEFAULT,?,?,?,?,?);
Executing (a9d14d5c-c0ac-4821-9b88-293b086debaa): COMMIT;
Executing (9ee9ddaa-294e-41d1-9e03-9f02a2737030): START TRANSACTION;
Executing (9ee9ddaa-294e-41d1-9e03-9f02a2737030): SELECT `ticket`.`id`, `ticket`.`subject`, `ticket`.`status`, `ticket`.`createdAt`, `ticket`.`updatedAt`, `ticket`.`deletedAt`, `ticket`.`userId`, `messages`.`id` AS `messages.id`, `messages`.`message` AS `messages.message`, `messages`.`sender` AS `messages.sender`, `messages`.`createdAt` AS `messages.createdAt`, `messages`.`updatedAt` AS `messages.updatedAt`, `messages`.`deletedAt` AS `messages.deletedAt`, `messages`.`ticketId` AS `messages.ticketId`, `messages`.`userId` AS `messages.userId`, `messages->user`.`id` AS `messages.user.id`, `messages->user`.`firstname` AS `messages.user.firstname`, `messages->user`.`surname` AS `messages.user.surname`, `messages->user`.`email` AS `messages.user.email`, `messages->user`.`password` AS `messages.user.password`, `messages->user`.`stripeId` AS `messages.user.stripeId`, `messages->user`.`token` AS `messages.user.token`, `messages->user`.`birthDate` AS `messages.user.birthDate`, `messages->user`.`status` AS `messages.user.status`, `messages->user`.`confirmationCode` AS `messages.user.confirmationCode`, `messages->user`.`createdAt` AS `messages.user.createdAt`, `messages->user`.`updatedAt` AS `messages.user.updatedAt`, `messages->user`.`deletedAt` AS `messages.user.deletedAt` FROM `tickets` AS `ticket` LEFT OUTER JOIN `messages` AS `messages` ON `ticket`.`id` = `messages`.`ticketId` AND (`messages`.`deletedAt` IS NULL) LEFT OUTER JOIN `users` AS `messages->user` ON `messages`.`userId` = `messages->user`.`id` AND (`messages->user`.`deletedAt` IS NULL) WHERE (`ticket`.`deletedAt` IS NULL);
Executing (9ee9ddaa-294e-41d1-9e03-9f02a2737030): COMMIT;
-- POST '/api/ticket
exports.addMessage = async (req, res) => {
try {
const result = await sequelize.transaction(async (t) => {
var ticketId = req.body.ticketId;
const userId = req.body.userId;
const message = req.body.message;
const subject = req.body.subject;
// Validate input - If new ticket, a subject must be provided
if (!ticketId && !subject) {
return res
.status(400)
.send({ message: "New ticket must have a subject" });
}
// Validate input - If ticket exists, userId and message must be provided
if (!userId && !message && ticketId) {
return res
.status(400)
.send({ message: "UserID and message are required" });
}
// Create ticket is no ticketID was provided
if (!ticketId) {
const [ticket, created] = await Ticket.findOrCreate({
where: {
subject: subject,
userId: userId,
},
transaction: t,
});
ticketId = ticket.id;
}
// Create a new message object
const messageObject = await db.message.create(
{
message: message,
userId: userId,
ticketId: ticketId,
},
{ transaction: t }
);
// Output message object
return res.send(messageObject);
});
} catch (err) {
console.log(err);
return res.status(500).send({
message:
err.message || "Some error occurred while creating the ticket message.",
});
}
};
-- GET: api/ticket
exports.findAll = async (req, res) => {
try {
const result = await sequelize.transaction(async (t) => {
const tickets = await db.ticket.findAll(
{
include: [{ model: db.message, include: [db.user] }],
},
{ transaction: t }
);
tickets.forEach((ticket) => {
console.log(JSON.stringify(ticket.messages.length));
});
return res.send(tickets);
});
} catch (err) {
console.log(err);
res.status(500).send({
message: err.message || "Some error occurred while retrieving Tickets.",
});
}
};
You sent a response to a client before the transaction actually was committed. You just need to move res.send(messageObject); outside the transaction call.
You can try to look what's going on in the current version of your code if you add several console.log with messages to see what the actual order of actions is (I mean a couple of messages in POST (the last statement inside transaction and after transaction before res.send) and at least one at the beginning of GET).
Actually if the transaction was rolled back you'd send an uncommited and already removed object/record that I suppose is not your goal.

Mongoose session.abortTransaction is not rolling back the actions

Here I am building the rental REST API and I want to perform transaction but it not rolling back the changes ,and it not giving any error problem is only changes are not rolling back.
const session = await startSession();
const { error } = validateRental(req.body);
if (error?.details[0].message)
return res.status(400).send(error.details[0].message);
const movie = await Movies.findById(req.body.movieID, null, {
$session: session,
}); // here i am adding the session
const customer = await Customer.findById(req.body.customerID, null, {
$session: session,
}); // same
if (!movie || !customer)
return res.status(400).send("Please check the customer or Movie ID");
try {
session.startTransaction();
movie.numberOfStock--;
customer.numberOfRental++;
await movie.save();
await customer.save();
await session.commitTransaction();
} catch (e) {
console.log(e);
await session.abortTransaction();
res.status(500).send("Internal System error");
} finally {
session.endSession();
}
After spending much time i got to know that my mongodb server is standalone , for write property we need to convert into Replica Set.
Follow the official documentation
Convert Standalone to a Replica set
Main point you should first close your server
My OS is window so - net stop mongob
For ios you can find at out yourself

How to start a transaction in MongoDb?

I'm trying to prevent concurrent requests to a specific record, see the following example:
function addMoney(orderID,orderID){
const status = Database.getOrder(orderID);
if (status === 1){
return "Money Already Added";
}
Database.udateOrder(orderID, {status: 1});
Database.addMoney(userID, 300);
return true;
}
Assume someone made this request exactly at the same time, therefore the "status" check passed, they'd be able to get Database.addMoney run twice.
Using MySQL, I'd start a transction to lock the row but not sure how to do so using MongoDB.
You can do the transactions in mongodb like MySQL. Consider having an order document with id:123 and status:0. Then you can check for status in a transaction and return if it's already paid or fall through in order to add money document and update order status.
If you face any issue like Transaction numbers are only allowed on a replica set member or mongos this link might help.
In order to use transactions, you need a MongoDB replica set, and
starting a replica set locally for development is an involved process.
The new run-rs npm module makes starting replica sets easy.
const uri = 'mongodb://localhost:27017';
const dbName = 'myDB';
const MongoClient = require('mongodb').MongoClient;
async function main() {
const client = new MongoClient(uri);
await client.connect();
const session = client.startSession();
try {
await session.withTransaction(async () => {
const orders = client.db(dbName).collection('orders');
const money = client.db(dbName).collection('money');
let doc = await orders.findOne({orderID: 123});
if (doc && doc.status === 1) {
console.log("Money Already Added");
return
}
await orders.updateOne({orderID: 123}, {'$set': {status: 1}});
await money.insertOne({orderID: 123, userID: 100, amount: 300}, {session});
console.log("Money added");
});
await session.commitTransaction();
} catch (e) {
console.log(e);
} finally {
await session.endSession();
await client.close();
}
}
main()
The code above may need improvement because I couldn't test it on MongoDB with replica set.

MongoError: Topology is closed, please connect despite established database connection

I am writing a web application that uses asynchronous database requests as a part of the api. Currently, I have an async express route that awaits function returns from async functions. Both of these functions return booleans and both query the database. One works correctly, however the second one does not.
Here is the MongoClient setup:
const MongoClient = require('mongodb').MongoClient;
const uri = config.uri; // Contains custom url for accessing database
const client = new MongoClient(uri, { useUnifiedTopology: true}, { useNewUrlParser: true }, { connectTimeoutMS: 30000 }, { keepAlive: 1});
where config is from a file imported as.
const config = require("./config.js");
and functions properly.
Here is the express setup:
app.post("/signup", async function(request, response) {
log("POST request at /signup");
log("BEFORE UNIQUE USER");
const isUniqueUser = await validateUniqueUser(request.body.email, request.body.password);
log(isUniqueUser);
const status = {
status: null
};
if (isUniqueUser) {
log("AFTER UNIQUE USER");
let userCreated = await createPracticeProfile(request.body.email, request.body.password);
log("user created: " + userCreated);
if (userCreated) {
status.status = "user_created";
}
response.json(status);
} else {
response.json(status);
}
console.log("********************************end");
});
The console outputs:
BEFORE UNIQUE USER
true (which it should be)
AFTER UNIQUE USER
MongoError: Topology is closed.
user created: undefined
***...***end
Here is the function for validating that a user is unique:
/* VALIDATE_UNIQUE_USER
USE: ensure user does not have existing profile
PARAMS: email (string), password (string)
RETURN: isUniqueUser (bool)
*/
async function validateUniqueUser(email, password) {
// connect to database
const database = await client.connect().catch(err => {
log("ERROR while connecting to database at: validateUniqueUser");
console.log(err);
client.close();
});
// database connection failed
if (!database) {
return false;
}
// connection successful => find user
let user;
try {
user = await database.db("guitar-practice-suite").collection("users").findOne({email: email});
} catch(err) {
log("ERROR while finding user in database at: validateUniqueUser");
console.log(err);
client.close();
return false;
} finally {
client.close();
// user not found (unique)
if (user === null || user === undefined) {
return true;
}
return false;
}
}
Here is the function for inserting the user into the collections:
/* CREATE_PRACTICE_PROFILE
USE: insert a practice profile into the database
PARAMS: email (string), password (string)
RETURN: userCreated (bool)
*/
async function createPracticeProfile(email, password) {
// hash password
let hashedPassword;
try {
hashedPassword = await new Promise((resolve, reject) => {
bcrypt.hash(password, null, null, function(err, hash) {
if (err) {
reject(err);
}
resolve(hash)
});
});
} catch(err) {
log("ERROR while hashing password at: createPracticeProfile");
console.log(err);
return false;
}
// connect to database
const database = await client.connect().catch(err => {
log("ERROR while connecting to database at: validateUniqueUser");
console.log(err);
client.close();
});
// database connection failed
if (!database) {
return false;
}
// database connection successful => insert user into database
let insertUserToUsers;
let insertUserToExercises;
let insertUserToCustomExercises;
try {
insertUserToUsers = await database.db("guitar-practice-suite").collection("users").insertOne({email: email, password: hashedPassword});
insertUserToExercises = await database.db("guitar-practice-suite").collection("exercises").insertOne({email: email});
insertUserToCustomExercises = await database.db("guitar-practice-suite").collection("custom-exercises").insertOne({email: email, exercises: []});
} catch(err) {
log("ERROR while inserting user into database at: createPracticeProfile");
console.log(err);
client.close();
return false;
} finally {
client.close();
return insertUserToUsers && insertUserToExercises && insertUserToCustomExercises;
}
}
I've found the solution to the problem, but I'm not sure I understand the reasoning.
The client.close() in the finally block of the validateUniqueUser function. It was closing the connection before the connection in the createPracticeProfile function was finished inserting the user.
When that line is taken out, the function works.
The issue is client variable needs to be reinstantiated again,
const client = new MongoClient(uri, { useUnifiedTopology: true}, { useNewUrlParser: true }, { connectTimeoutMS: 30000 }, { keepAlive: 1});
Try putting this in start of createPracticeProfile, validateUniqueUser and other functions
I was getting the error
MongoError: Topology is closed
because of the authentication problem
MongoEror: Authentication failed
In my case, the problem was with the password of my database. My password only contained numerical digits.
I changed the password to all characters and both the errors were solved.
Configure your client connection like below example
var MongoClient = require('mongodb').MongoClient;
var Server = require('mongodb').Server;
var mongoClient = new MongoClient(new Server('localhost', 27017));
mongoClient.open(function(err, mongoClient) {
var db1 = mongoClient.db("mydb");
mongoClient.close();
});
In my case - connecting to AtlasDB using the MongoClient - I had to whitelist the IP i was accessing the cluster from
I think your mongodb service is stopped, to start it
Task Manager -> Services -> Mongodb -> RightClick -> Start
My code has been working fine for a long time and hasn't thrown this error before: MongoError: Topology is closed.
But due to the fact that my laptop was turned on for a long time and I was simultaneously developing other projects on it, while the main one was running in the terminal, mongo most likely did not close one of the connections to the database and opened another in parallel, creating some kind of collision.
In general, in my case, the usual restart of the computer helped and a similar error did not occur again.

Transaction error using node-mssql library

Hello I'm using the mssql library to insert some data into an SQL Server DB, the call is being executed inside a queue so if it fails it will retry in 1 min * retryAttemp up until 5 attempts
however I keep getting this weird behavior where I get this error message:
ERROR: Error inserting into BASIS OLAP TransactionError: Can't acquire connection for the request. There is another request in progress.
and I don't know why this is happening... this is my code:
static async insertPresale(req, res) {
logger.info('Inserting new Pre-Sale');
const { CAB, DET } = req.body.basisOlapStructure;
const { country } = req.body;
if (country.toLowerCase() !== 'cl') return res.status(404).json({ success: false, message: 'Other countries are not currently supported' });
let pool;
let transaction;
try {
pool = await Databases.ClSQLServerPreventa;
if (!pool) throw new errors.UNEXPECTED_ERROR({ message: 'Error inserting pre-sale in BASIS OLAP. Could not get a connection pool' });
transaction = new mssql.Transaction(pool);
await new Promise((resolve, reject) => transaction.begin(err => (err ? reject(err) : resolve())));
const request = new mssql.Request(transaction);
logger.info('Inserting data into PVRCabecera');
let queryStr = msql.insert().into(bTables.PVRCabecera.toString()).setFields(CAB);
logger.info(queryStr.toString());
let result = await request.query(queryStr.toString());
if (result.rowsAffected > 0) logger.info('Data successfully inserted into PVRCabecera');
logger.info('Inserting data into PVRDetalle');
const queries = DET.map((d) => {
queryStr = msql.insert().into(bTables.PVRDetalle.toString()).setFields(d);
logger.info(queryStr.toString());
return request.query(queryStr.toString());
});
result = await Promise.all(queries);
if (result[0].rowsAffected > 0) logger.info('Data successfully inserted into PVRDetalle');
await transaction.commit();
logger.info('BASIS_OLAP Transaction completed successfully');
return res.status(201).json({ success: true });
} catch (error) {
logger.error(`Error inserting into BASIS OLAP ${error}`);
if (transaction) {
await transaction.rollback();
}
logger.info('Transaction has been rolled back');
throw new errors.UNEXPECTED_ERROR({ message: `Error inserting pre-sale in BASIS OLAP. ${error.toString()}` });
}
}
what can I do to solve this problem?

Resources