Uploaded image for project: 'Core Server'
  1. Core Server
  2. SERVER-37792

Strange behavior of Multi-Document Transaction

    • Type: Icon: Question Question
    • Resolution: Done
    • Priority: Icon: Major - P3 Major - P3
    • None
    • Affects Version/s: 4.0.2
    • Component/s: JavaScript, Replication
    • Labels:
      None

      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.

            Assignee:
            daniel.hatcher@mongodb.com Danny Hatcher (Inactive)
            Reporter:
            sinilya Ilya
            Votes:
            0 Vote for this issue
            Watchers:
            10 Start watching this issue

              Created:
              Updated:
              Resolved: