Reputation: 24068
I am using mongoose to connect to my Mongodb Atlas cluster through my nodejs server.
There is a certain operation which is done as a transaction. Mongoose needs mongoose.startSession()
to be called to start a transaction. Very infrequently, this mongoose.startSession()
call hangs indefinitely. There is no certain way to reproduce this.
log.info('starting lock session');
const mongoSession = await mongoose.startSession();
log.info('lock session started');
In above code, starting lock session.
gets logged, but lock session started
doesn't get logged when issue occurs.
I connect to the db like below:
const dburl = 'mongodb+srv://myuser:[email protected]/mydb?retryWrites=true&w=majority';
mongoose.connect(dburl, {useNewUrlParser: true}, err => {
if (err) {
log.warn('Error occurred when connecting to database. ' + err);
}
});
What could be the reason for this? Could this be due to something wrong with the database? Is there any way I can further troubleshoot this?
Upvotes: 10
Views: 7762
Reputation: 101
This solution is working for me :
// package.json :
"mongodb": "^6.9.0",
"mongoose": "^8.7.0",
// utils/mongoDbConnection.js :
const moongoose = require('mongoose')
function setupMongoose() {
const uri = process.env.MONGO_CONNECTION_URI;
const options = {};
const conn = moongoose.createConnection(uri, options);
conn.on('connected', function () {
console.info(`${conn.name} default connection is open`);
});
conn.on('disconnected', function () {
console.info(`${conn.name} default connection is disconnected`);
});
return conn
}
const appMongoConnection = setupMongoose();
module.exports = {
appMongoConnection
};
// model/account.js :
const mongoose = require('mongoose');
const {appMongoConnection} = require('../utils/mongoDbConnection.js')
const accountSchema = mongoose.Schema({
name: {
type: String,
required: true
},
amount: {
type: Number,
required: true
}
});
// modelname, schema, collectionname
module.exports = appMongoConnection.model('Account', accountSchema, 'accounts');
//services/account.js :
const updateAccount = async (query, update) => {
// Récupérer le client MongoDB
const mongoClient = appMongoConnection.getClient()
// Démarrer une nouvelle session
const session = mongoClient.startSession()
session.startTransaction()
try {
//Your code here ...
await session.commitTransaction()
} catch (e) {
await session.abortTransaction()
throw e
} finally {
session.endSession()
}
}
// __ test __/account.test.js :
const {appMongoConnection} = require('../utils/mongoDbConnection.js')
const sessionMock = {
startTransaction: jest.fn(),
commitTransaction: jest.fn(),
abortTransaction: jest.fn(),
endSession: jest.fn(),
}
const mongoClientMock = {
startSession: jest.fn().mockReturnValue(sessionMock),
}
jest.spyOn(appMongoConnection, 'getClient').mockReturnValue(mongoClientMock)
Upvotes: 0
Reputation: 21
In my case, when I was creating a session, it used to hang but later on I did the following and I was able to successfully create a mongodb transaction:
The example is, transfer some amount from one account to another:
Create my Account Schema:
const mongoose = require('mongoose');
const {appMongoConnection} = require('../drivers/mongo/mongo.init')
const accountSchema = mongoose.Schema({
name: {
type: String,
required: true
},
amount: {
type: Number,
required: true
}
});
// modelname, schema, collectionname
module.exports = appMongoConnection.model('Account', accountSchema, 'accounts');
Let's initialize the mongodb connection:
const moongoose = require('mongoose')
function setupMongoose() {
const uri = process.env.MONGO_CONNECTION_URI;
const options = {};
const conn = moongoose.createConnection(uri, options);
conn.on('connected', function () {
console.info(`${conn.name} default connection is open`);
});
conn.on('disconnected', function () {
console.info(`${conn.name} default connection is disconnected`);
});
return conn
}
const appMongoConnection = setupMongoose();
module.exports = {
appMongoConnection
};
Now my router looks like:
const Account = require('../models/account.model')
const express = require('express');
const mongoose = require('mongoose');
const {appMongoConnection} = require('../drivers/mongo/mongo.init')
const router = express.Router();
router.post('/transfer', async function handle(req, res, next) {
console.log("Request hit 2");
let mongoClient = appMongoConnection.getClient();
const session = await mongoClient.startSession();
const txnOptions = {
readPreference: "primary",
readConcern: { level: "local" },
writeConcern: { w: "majority" }
};
try {
// Since, we have written our code inside this block, if update fails, we need not
// explicitly call await session.abortTransaction(); to rollback transaction
await session.withTransaction(async () => {
const opts = { session };
let { from, to, amount } = req.body;
const sender = await Account.findById(from).session(session);
if (!sender) throw new Error('Sender account not found');
const receiver = await Account.findById(to).session(session);
if (!receiver) throw new Error('Receiver account not found');
sender.amount -= amount;
receiver.amount += amount;
await sender.save(opts);
// if (sender !== null) {
// throw new Error("Intentionally raising exception");
// }
await receiver.save(opts);
// Commit transaction
await session.commitTransaction();
res.status(200).json({ message: 'Transaction successful' });
}, txnOptions);
} catch (error) {
console.error('Transaction error:', error);
res.status(500).json({ message: 'Transaction failed' });
} finally {
await session.endSession();
}
});
module.exports = router;
Done
Upvotes: 1
Reputation: 627
I resolved this issue by using connection object coming from
mongoose.createConnection(uri, options)
const connection = mongoose.createConnection(uri, options);
const session = await connection.startSession();
session.startTransaction();
await MySchema.create({
value: "Created?",
session: session, // giving session here
});
await session.commitTransaction();
session.endSession();
mongoose.connection
is different than this connection object.
Upvotes: 0
Reputation: 24068
This looks like a bug in mongoose, and I reported it to mongoose but still didn't get a response.
https://github.com/Automattic/mongoose/issues/8325
I wrote following function which I can use to wait until the mongoose connection is ready before calling startSession()
, and it fixes my problem.
function waitForMongooseConnection(mongoose) {
return new Promise((resolve) => {
const connection = mongoose.connection;
if (connection.readyState === 1) {
resolve();
return;
}
console.log('Mongoose connection is not ready. Waiting for open or reconnect event.');
let resolved = false;
const setResolved = () => {
console.log('Mongoose connection became ready. promise already resolved: ' + resolved);
if (!resolved) {
console.log('Resolving waitForMongooseConnection');
resolved = true;
resolve();
}
};
connection.once('open', setResolved);
connection.once('reconnect', setResolved);
});
}
With above function, I can start session like below:
log.info('starting session');
await waitForMongooseConnection(mongoose);
const mongoSession = await mongoose.startSession();
log.info('session started');
Note that I had to turn off useUnifiedTopology
. Otherwise, 'reconnect' didn't get called.
mongoose.connect(config.db, {useNewUrlParser: true, useUnifiedTopology: false}, err => {
if (err) {
log.warn('Error occurred when connecting to database. ' + err);
}
});
Upvotes: 4