Transaction 1 has been committed in MongoDB

Viewed 2076

I am trying to use transactions to update multiple documents.

One being a loading sheet document [await sheet.save({ session });]

and other being an array of stock reservation records [await Stock.bulkWrite()].

    const session = await mongoose.startSession();

    session.startTransaction({
      readPreference: 'primary',
      readConcern: { level: 'local' },
      writeConcern: { w: 'majority' },
    });

    let sheetAfterSave: any = null;

    try {
      sheetAfterSave = await sheet.save({ session });
      records.forEach(async (el: any) => {
        let updatedStockRecord = await Stock.bulkWrite(
          [
            {
              updateOne: {
                filter: {
                  index: el.index,
                  product: el.product,
                  batchNo: el.batchNo,
                  agency,
                  totalQuantity: { $gte: el.loadingTotal },
                },
                update: {
                  $push: {
                    reservations: {
                      loadingSheetId: sheetAfterSave._id,
                      reservedCaseQuantity: el.loadingCaseCount,
                      reservedUnitQuantity: el.loadingUnitCount,
                      reservedTotalQuantity: el.loadingTotal,
                    },
                  },
                },
              },
            },
          ],
          {
            session: session,
          }
        );
      });
      await session.commitTransaction();
      session.endSession();
    } catch (error) {
      console.log(error);
      await session.abortTransaction();
      session.endSession();
      throw new Error(
        `Error occured while trying to create a new loading sheet. ${error}`
      );
    }

In this operation loading sheet document gets saved in the database, but not the stock reservation records array.

It gives the error

[distribution] MongoError: Transaction 1 has been committed.
[distribution]     at MessageStream.messageHandler (/app/node_modules/mongoose/node_modules/mongodb/lib/cmap/connection.js:263:20)
[distribution]     at MessageStream.emit (node:events:376:20)
[distribution]     at processIncomingData (/app/node_modules/mongoose/node_modules/mongodb/lib/cmap/message_stream.js:144:12)
[distribution]     at MessageStream._write (/app/node_modules/mongoose/node_modules/mongodb/lib/cmap/message_stream.js:42:5)
[distribution]     at writeOrBuffer (node:internal/streams/writable:388:12)
[distribution]     at MessageStream.Writable.write (node:internal/streams/writable:333:10)
[distribution]     at TLSSocket.ondata (node:internal/streams/readable:716:22)
[distribution]     at TLSSocket.emit (node:events:376:20)
[distribution]     at addChunk (node:internal/streams/readable:305:12)
[distribution]     at readableAddChunk (node:internal/streams/readable:280:9)
[distribution]     at TLSSocket.Readable.push (node:internal/streams/readable:219:10)
[distribution]     at TLSWrap.onStreamRead (node:internal/stream_base_commons:192:23)
[distribution] [ERROR] 14:56:43 MongoError: Transaction 1 has been committed.

As I use the session in a failure like this the saved documents must be rolled back but its not happening here.

Am I missing something here?? Appreciate your help

Cheers

Do you see anything wrong with this code. I am trying to create a dynamic error inside the loop to make sure that all the the transactions are rolled back if any error occurs inside the loop?? Dynamic error outside the loop rolls back all the transactions perfectly but not the one inside the loop.

const session = await mongoose.startSession();

try {
  await session.withTransaction(
    async () => {
      sheetAfterSave = await sheet.save({ session });
      records.forEach(async (el: any) => {
        let updatedStockRecord = await Stock.bulkWrite(
          [
            {
              updateOne: {
                filter: {
                  index: el.index,
                  product: el.product,
                  batchNo: el.batchNo,
                  agency,
                  totalQuantity: { $gte: el.loadingTotal },
                },
                update: {
                  $push: {
                    reservations: {
                      loadingSheetId: sheetAfterSave._id,
                      reservedCaseQuantity: el.loadingCaseCount,
                      reservedUnitQuantity: el.loadingUnitCount,
                      reservedTotalQuantity: el.loadingTotal,
                    },
                  },
                },
              },
            },
          ],
          {
            session: session,
          }
        );
        console.log('******************');
        throw new Error('12/24/2020 ERROR INSIDE LOOP'); //  MongoError: Transaction 1 has been committed.
      });
      throw new Error('12/24/2020 ERROR OUTSIDE LOOP'); // All transactions are rolled back perfectly
    },
    {
      readPreference: 'primary',
      readConcern: { level: 'local' },
      writeConcern: { w: 'majority' },
    }
  );
} catch (error) {
  console.log('ERROR BLOCK', error);
  throw new Error(
    `Error occured while trying to create a new loading sheet. ${error}`
  );
} finally {
  session.endSession();
  await mongoose.connection.close();
  console.log('************ FINALLY *****************');
}
3 Answers

I was able to solve the issue.

Problem was not with the below code

  await session.commitTransaction(); (success)
  session.endSession();              (failure)
} catch (error) {                    (entered)
  await session.abortTransaction();  (invoked)

but it was with the records.forEach loop.

records.forEach(async (el: any) => {...});

inside the foreach when throwing an error it is not caught by the outermost try catch block since the content inside the loop is in a different functional context than the code outside of the loop.

Once I changed the loop from .forEach to

for (const el of records) {}

its working as expected. Posting the answer in case if someone faces the same in the future. Thanks for the support guys :)

This mostly happens when one or more an update or db operation is not executed before the commit transaction is called. for example:

const session = await mongoose.startSession();

    await session.withTransaction(async() => {
        await User.findOneAndUpdate({_id: user._id}, {$set: {profile: profile._id}}, {session});
        profile.set(body);
        profile.user = user._id;
        profile.save({session}); // THIS LINE WILL CAUSE SUCH ERROR BECAUSE IT RETURNS PROMISE
    })
    
    session.endSession();
    return profile;

The right thing to do is to make sure operations are executed/await before the session transaction is committed as shown below.

 const session = await mongoose.startSession();


    await session.withTransaction(async() => {
        await User.findOneAndUpdate({_id: user._id}, {$set: {profile: profile._id}}, {session});
        profile.set(body);
        profile.user = user._id;
        await profile.save({session}); //AWAIT TO ENSURE COMPLETION OF EXECUTION
    })
    
    session.endSession();
    return profile;

In the original question, ForEach() loop used is async operation which will not guarantee that all operations are completed before the transaction is committed. Substituting it with a for..of loop which is somehow sync operation will guarantee a linear other of execution and error-out instantly if anything fails

The following sequence of calls/events will attempt to abort a committed transaction:

      await session.commitTransaction(); (success)
      session.endSession();              (failure)
    } catch (error) {                    (entered)
      await session.abortTransaction();  (invoked)

See https://docs.mongodb.com/manual/core/transactions/ for correct usage.

Related