[SERVER-37792] Strange behavior of Multi-Document Transaction Created: 28/Oct/18  Updated: 14/Nov/18  Resolved: 14/Nov/18

Status: Closed
Project: Core Server
Component/s: JavaScript, Replication
Affects Version/s: 4.0.2
Fix Version/s: None

Type: Question Priority: Major - P3
Reporter: Ilya Assignee: Danny Hatcher (Inactive)
Resolution: Done Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Participants:

 Description   

First of all, sorry for my english.
I'm trying to implement a simple example of ACID transaction in MongoDB, using NodeJS driver. I also use Mongoose. I'm just experimenting with transaction to understand how it works and to foresee and prevent all possible errors.
I have a collection 'testcollection', and i have a single document in it, like:

 

var testcollection = new mongoose.Schema({
 id: Number,
 name: String,
 number: Number
});

This is code:

 

var mongoDB = require('./libs/db');
var _ = require('underscore');
 
mongoDB.connect("mongodb://testuser:name32@localhost:27018/testbase");
 
var testcoll = require('./libs/db/models/testcollection');
 
var _session = mongoose.startSession({readPreference: {mode: "primary"}});
(async () => _session = await _session)(); // wait for ClientSession
 
async function transactionOperations(id, session){
 
     async function transactionOperations(id, session){ await      testcoll.updateOne({name: "ilya", id: 1}, {$inc: {number: 2}}).session(session);      // change document as transaction !!!
 
     await testcoll.updateOne({name: "ilya", id: 1}, {$inc: {number: 3}}); 
// this is not transaction!!!
}
 
var performTransaction = function(id){    
    async function commit() {
        try {
            await _session.commitTransaction();
        } catch (error) {
            if (error.errorLabels && error.errorLabels.indexOf('UnknownTransactionCommitResult') >= 0) {
                console.warn('UnknownTransactionCommitResult, retrying commit operation.', error);
                await commit();
            } else {
                console.error('Transaction aborted. Caught exception during transaction.', error);
            }
        }
    }
    
    async function runTransactionWithRetry(txnFunc, _transactionOperations) {
        try {
            await txnFunc(_transactionOperations);
        } catch (error) {
            if (error.errorLabels && error.errorLabels.indexOf('TransientTransactionError') >= 0) {
                console.warn('TransientTransactionError, retrying transaction.', error);
                await _session.abortTransaction();
                await runTransactionWithRetry(txnFunc, _transactionOperations);
            } else {
                if(_session.inTransaction()){
                    await runTransactionWithRetry(txnFunc, _transactionOperations);
                } else {
                    await _session.abortTransaction();
                    console.error('Transaction aborted. Caught exception during transaction.', error);
                }
            }
        }
    }
 
    async function createTransaction(_transactionOperations) {        
        // wait for ClientSession
        _session = await _session;
        console.log("START " + id + " TRANSACTION");
        _session.startTransaction({
            readConcern: { level: 'snapshot' },
            writeConcern: { w: 'majority',}
        });
        // ----- all transaction operations here -----        
        await _transactionOperations(id, _session);         
        // ----- all transaction operations here -----        
 
        try {
            await commit();
        } catch (error) {
            await _session.abortTransaction();
            console.error('Transaction aborted. Caught exception during transaction.', error);
        }
    }    runTransactionWithRetry(createTransaction, transactionOperations);
}

In my transaction i'm trying to simulate the situation, when at first i change document as a transaction (increment field 'number' +2), and than change it not as a transaction (increment same field +3 in same doc, but i don't pass session parameter in query function). So it must cuase some write conflict, and throw some error.

When i running this transaction, it waits for a 60 seconds (as i understood, max txn time), and than throw an error:

  errorLabels: [ 'TransientTransactionError' ],
  operationTime: Timestamp { _bsontype: 'Timestamp', low_: 1, high_: 1540719109 },
  ok: 0,
  errmsg: 'Transaction 1 has been aborted.',
  code: 251,
  codeName: 'NoSuchTransaction'

It seems, that transaction runs, can't perform update operation because of write conflict and than just waiting for txn timeout. If i'm not mistaking, this is a specific write-conflict error, so why it wait 60 seconds and throws 'NoSuchTransaction' MongoError?

BUT!!! Somitimes (for example, when i add some console.log-s in transaction body) it throws another error, such as:

  errorLabels: [ 'TransientTransactionError' ],
  operationTime: Timestamp { _bsontype: 'Timestamp', low_: 1, high_: 1540719759 },
  ok: 0,
  errmsg: 'Unable to acquire lock \'{8613801417341912551: Database, 1696272389700830695}\' within a max lock request timeout of \'5ms\' milliseconds.',
  code: 24,
  codeName: 'LockTimeout'

Here transaction don't have enough time (5ms) to lock the document. As i understood, transactions in mongo locks document not in the time, when calls session.startTransaction(), but when the operations, that are assosiated with this session and transaction, are executing.

 

So, what i should do with this problem? How can i handle correctly this write conflicts? Why in one situation i catch one MongoError, and in other situation - another, but both errors is caused by single reason?
Thanks a lot.



 Comments   
Comment by Matt Broadstone [ 29/Oct/18 ]

Hi sinilya! It looks like you are not correctly passing the session into your first updateOne call above. It should read:

await testcoll.updateOne({ name: 'ilya', id: 1 }, { $inc: { number: 2 } }, { session });

I think this is your primary issue, so please try it again and let's continue the discussion from there.

Additionally there should be no need to await the client session above, they are created locally and synchronously. Finally, it's a bit difficult to follow your code sample, could you please make sure this accurately reflects what you are trying to run? For instance, _transactionOperations is called from within a transactionOperations async method, performTransaction is defined but never called, etc.

Generated at Thu Feb 08 04:47:02 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.