Lahiru Chandima
Lahiru Chandima

Reputation: 24068

Mongoose startSession() hangs

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

Answers (4)

moez cherif
moez cherif

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

Chandan Kumar
Chandan Kumar

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

Mayur Prajapati
Mayur Prajapati

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

Lahiru Chandima
Lahiru Chandima

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

Related Questions