Search code examples
javascriptnode.jsmongodbmongoosetransactions

Transactions are not aborting even if there occurs an error in one of the transaction in mongodb?


router.put('/', async (req, res) => {
    try {
    const today = new Date();
    var month = (today.getMonth()) + 1;
    var year = today.getFullYear();
    var field = year + "-" + month;
    var category = req.body.category;
    var date = today.getDate();
    const session =  mongoose.startSession();
    const transactionOptions = {
        readPreference: 'primary',
        readConcern: { level: 'local' },
        writeConcern: { w: 'majority' }
    };
   
        const news = await News.findById(req.body.id);
        var newsDate = new Date(news.createdAt).getDate();
        var newsMonth = new Date(news.createdAt).getMonth() + 1;
        var newsYear = new Date(news.createdAt).getFullYear();
        // (await session).startTransaction();


        // (await Publisher.findOneAndUpdate({ registrationNumber: req.body.registrationNumber }, { $inc: { [`monthlyViews.${field}`]: 1, [`categoriesViews.${category}`]: 1 } })).$session(session);
        // if (newsDate == date && newsMonth == month && newsYear == year) {
        //     (await News.findOneAndUpdate({ _id: req.body.id }, { $inc: { [`views.${field}`]: 1, [`totalViews`]: 1, ['publishedDateViews']: 1 } })).$session(session);
        // } else {
        //     (await News.findOneAndUpdate({ _id: req.body.id }, { $inc: { [`views.${field}`]: 1, [`totalViews`]: 1 } })).$session(session);
        // }

        // (await session).commitTransaction();
        // res.status(200).send({ status: 1 });
        const transactionResult = (await session).withTransaction(async () => {
            
            var newsResult;
            if (newsDate == date && newsMonth == month && newsYear == year) {
                newsResult = (await News.findOneAndUpdate({ _id: req.body.id }, { $inc: { [`views.${field}`]: 1, [`totalViews`]: 1, ['publishedDateViews']: 1 } })).$session(session);
            } else {
                newsResult = (await News.findOneAndUpdate({ _id: req.body.id }, { $inc: { [`views.${field}`]: 1, [`totalViews`]: 1 } })).$session(session);
            }
            if (!newsResult) {
                return (await session).abortTransaction();

            }
            const pubresult = (await Publisher.findOneAndUpdate({ registrationNumber: req.body.registrationNumber }, { $inc: { [`monthlyViews.${field}`]: 1, [`categoriesViews.${category}`]: 1 } })).$session(session);
            if (!pubresult) {
                return (await session).abortTransaction();

            }



        }, transactionOptions).then(result => { res.status(200).send({ status: 1 }); }).catch(e => {
            console.log(e);
            res.status(400).send({ status: 0 });

        });
    } catch (error) {
        // (await session).abortTransaction();
        console.error(error);
        res.status(500).send({ status: 0, message: "Internal Server error" });
    } finally {
        (await session).endSession();
    }


});

Whenever one of the transaction fails then also it did not aborts the transaction and transaction got partially committed. for example: when I send wrong registration number using postman then Publisher.findoneandUpdate an returns "TypeError: Cannot read property '$session' of null" and the transaction should be aborted but it got partially committed like the code above Publisher line save it in the document. i am using mongodb atlas


Solution

  • My understanding of your problem is that when an error is thrown inside your transaction, the transaction isn't aborted?

    When reviewing your code you seem to mix async/await with promises which leads to it being less readable and also things work slightly differently.

    When using async/await, any return statement will cause the promise to resolve and not reject, whereas when using a throw statement, the promise will be rejected instead of resolved.

    Now, you catch any error that is thrown inside your callback using .catch, however, if the method throws because $session cannot exist on null then your (await session).abortTransaction() isn't called.

    You use try/catch/finally, however, both catch and finally aren't executed as no error is thrown. You've caught any error inside your callback already.

    Instead, use async/await purely and see if makes any difference (excuse the auto-formatted code):

    
    router.put('/', async (req, res) => {
      try {
        const today = new Date();
        var month = today.getMonth() + 1;
        var year = today.getFullYear();
        var field = year + '-' + month;
        var category = req.body.category;
        var date = today.getDate();
        const session = await mongoose.startSession(); // await here
        const transactionOptions = {
          readPreference: 'primary',
          readConcern: { level: 'local' },
          writeConcern: { w: 'majority' },
        };
    
        const news = await News.findById(req.body.id);
        var newsDate = new Date(news.createdAt).getDate();
        var newsMonth = new Date(news.createdAt).getMonth() + 1;
        var newsYear = new Date(news.createdAt).getFullYear();
        // (await session).startTransaction();
    
        // (await Publisher.findOneAndUpdate({ registrationNumber: req.body.registrationNumber }, { $inc: { [`monthlyViews.${field}`]: 1, [`categoriesViews.${category}`]: 1 } })).$session(session);
        // if (newsDate == date && newsMonth == month && newsYear == year) {
        //     (await News.findOneAndUpdate({ _id: req.body.id }, { $inc: { [`views.${field}`]: 1, [`totalViews`]: 1, ['publishedDateViews']: 1 } })).$session(session);
        // } else {
        //     (await News.findOneAndUpdate({ _id: req.body.id }, { $inc: { [`views.${field}`]: 1, [`totalViews`]: 1 } })).$session(session);
        // }
    
        // (await session).commitTransaction();
        // res.status(200).send({ status: 1 });
        await session.withTransaction(async () => {
          var newsResult;
          // if you haven't already, learn about strict equality
          if (newsDate == date && newsMonth == month && newsYear == year) {
            newsResult = (
              await News.findOneAndUpdate(
                { _id: req.body.id },
                {
                  $inc: {
                    [`views.${field}`]: 1,
                    [`totalViews`]: 1,
                    ['publishedDateViews']: 1,
                  },
                }
              )
            ).$session(session);
          } else {
            newsResult = (
              await News.findOneAndUpdate(
                { _id: req.body.id },
                { $inc: { [`views.${field}`]: 1, [`totalViews`]: 1 } }
              )
            ).$session(session);
          }
          if (!newsResult) {
            // if not found, catch {} will catch it
            throw new Error('news result does not exist');
          }
          const pubresult = (
            await Publisher.findOneAndUpdate(
              { registrationNumber: req.body.registrationNumber },
              {
                $inc: {
                  [`monthlyViews.${field}`]: 1,
                  [`categoriesViews.${category}`]: 1,
                },
              }
            )
          ).$session(session);
          if (!pubresult) {
            throw new Error('publisher does not exist');
          }
        }, transactionOptions);
    
        res.status(200).send({ status: 1 });
      } catch (error) {
        session.abortTransaction(); // await if async
    
        if (
          error.message === 'publisher does not exist' ||
          error.message === 'news result does not exist'
        ) {
          res.status(404).send({ status: 0 }); // not found
          return;
        }
        // handle validation errors or whatever you used 400 for
        res.status(500).send({ status: 0, message: 'Internal Server error' });
      } finally {
        session.endSession();
      }
    });
    

    Please note: I haven't tested this, however, try it and see if your issue has been corrected. If abortTransaction and endSession are asynchronous then use await as required.