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

incorrect fastcount for a majority committed prepared transaction that is in prepare after a rollback and then committed

    XMLWordPrintable

    Details

    • Backwards Compatibility:
      Fully Compatible
    • Operating System:
      ALL
    • Steps To Reproduce:
      Hide

      (function() {
          "use strict";
          load("jstests/aggregation/extras/utils.js");
          load("jstests/core/txns/libs/prepare_helpers.js");
          load("jstests/replsets/libs/rollback_test.js");
       
          const dbName = "test";
          const collName = "rollback_reconstructs_transactions_prepared_before_stable";
       
          const rollbackTest =
              new RollbackTest(dbName, undefined, true /* expect prepared transaction after rollback */);
          let primary = rollbackTest.getPrimary();
       
          // Create collection we're using beforehand.
          const testDB = primary.getDB(dbName);
          const testColl = testDB.getCollection(collName);
       
          testDB.runCommand({drop: collName});
          assert.commandWorked(testDB.runCommand({create: collName}));
       
          // Start a session on the primary.
          let session = primary.startSession();
          const sessionID = session.getSessionId();
          let sessionDB = session.getDatabase(dbName);
          let sessionColl = sessionDB.getCollection(collName);
       
          assert.commandWorked(sessionColl.insert({_id: 0}));
       
          // Prepare the transaction on the session.
          session.startTransaction();
          assert.commandWorked(sessionColl.insert({_id: 1}));
          assert.commandWorked(sessionColl.update({_id: 0}, {$set: {a: 1}}));
          const prepareTimestamp = PrepareHelpers.prepareTransaction(session);
       
          // Fastcount reflects the insert of a prepared transaction.
          assert.eq(testColl.count(), 2);
       
          jsTestLog("Do a majority write to advance the stable timestamp past the prepareTimestamp");
          // Doing a majority write after preparing the transaction ensures that the stable timestamp is
          // past the prepare timestamp because this write must be in the committed snapshot.
          assert.commandWorked(
              testColl.runCommand("insert", {documents: [{_id: 2}]}, {writeConcern: {w: "majority"}}));
       
          // Fastcount reflects the insert of a prepared transaction.
          assert.eq(testColl.count(), 3);
       
          // Check that we have one transaction in the transactions table.
          assert.eq(primary.getDB('config')['transactions'].find().itcount(), 1);
       
          // The transaction should still be prepared after going through rollback.
          rollbackTest.transitionToRollbackOperations();
          rollbackTest.transitionToSyncSourceOperationsBeforeRollback();
          rollbackTest.transitionToSyncSourceOperationsDuringRollback();
          rollbackTest.transitionToSteadyStateOperations();
       
          // Make sure there is still one transactions in the transactions table. This is because the
          // entry in the transactions table is made durable when a transaction is prepared.
          assert.eq(primary.getDB('config')['transactions'].find().itcount(), 1);
       
          // TODO: Fix fastcount. Fastcount will return 2 instead of 3. This is inconsistent with the
          // behavior of fastcount observing the insert at line 38.
          assert.eq(testColl.count(), 3);
       
          // Make sure we cannot see the writes from the prepared transaction yet.
          arrayEq(testColl.find().toArray(), [{_id: 0}, {_id: 2}]);
       
          // Get the correct primary after the topology changes.
          primary = rollbackTest.getPrimary();
          rollbackTest.awaitReplication();
       
          // Make sure we can successfully commit the recovered prepared transaction.
          session =
              PrepareHelpers.createSessionWithGivenId(primary, sessionID, {causalConsistency: false});
          sessionDB = session.getDatabase(dbName);
          // The transaction on this session should have a txnNumber of 0. We explicitly set this
          // since createSessionWithGivenId does not restore the current txnNumber in the shell.
          session.setTxnNumber_forTesting(0);
          const txnNumber = session.getTxnNumber_forTesting();
       
          // Make sure we cannot add any operations to a prepared transaction.
          assert.commandFailedWithCode(sessionDB.runCommand({
              insert: collName,
              txnNumber: NumberLong(txnNumber),
              documents: [{_id: 10}],
              autocommit: false,
          }),
                                       ErrorCodes.PreparedTransactionInProgress);
       
          // Make sure that writing to a document that was updated in the prepared transaction causes
          // a write conflict.
          assert.commandFailedWithCode(
              sessionDB.runCommand(
                  {update: collName, updates: [{q: {_id: 0}, u: {$set: {a: 2}}}], maxTimeMS: 5 * 1000}),
              ErrorCodes.MaxTimeMSExpired);
       
          // Commit the transaction.
          assert.commandWorked(sessionDB.adminCommand({
              commitTransaction: 1,
              commitTimestamp: prepareTimestamp,
              txnNumber: NumberLong(txnNumber),
              autocommit: false,
          }));
       
          // Make sure we can see the effects of the prepared transaction.
          arrayEq(testColl.find().toArray(), [{_id: 0, a: 1}, {_id: 1}, {_id: 2}]);
          // TODO: Fix fastcount. Fastcount will return 2 instead of 3.
          assert.eq(testColl.count(), 3);
       
          rollbackTest.stop();
      }());
      

      Show
      (function() { "use strict"; load("jstests/aggregation/extras/utils.js"); load("jstests/core/txns/libs/prepare_helpers.js"); load("jstests/replsets/libs/rollback_test.js");   const dbName = "test"; const collName = "rollback_reconstructs_transactions_prepared_before_stable";   const rollbackTest = new RollbackTest(dbName, undefined, true /* expect prepared transaction after rollback */); let primary = rollbackTest.getPrimary();   // Create collection we're using beforehand. const testDB = primary.getDB(dbName); const testColl = testDB.getCollection(collName);   testDB.runCommand({drop: collName}); assert.commandWorked(testDB.runCommand({create: collName}));   // Start a session on the primary. let session = primary.startSession(); const sessionID = session.getSessionId(); let sessionDB = session.getDatabase(dbName); let sessionColl = sessionDB.getCollection(collName);   assert.commandWorked(sessionColl.insert({_id: 0}));   // Prepare the transaction on the session. session.startTransaction(); assert.commandWorked(sessionColl.insert({_id: 1})); assert.commandWorked(sessionColl.update({_id: 0}, {$set: {a: 1}})); const prepareTimestamp = PrepareHelpers.prepareTransaction(session);   // Fastcount reflects the insert of a prepared transaction. assert.eq(testColl.count(), 2);   jsTestLog("Do a majority write to advance the stable timestamp past the prepareTimestamp"); // Doing a majority write after preparing the transaction ensures that the stable timestamp is // past the prepare timestamp because this write must be in the committed snapshot. assert.commandWorked( testColl.runCommand("insert", {documents: [{_id: 2}]}, {writeConcern: {w: "majority"}}));   // Fastcount reflects the insert of a prepared transaction. assert.eq(testColl.count(), 3);   // Check that we have one transaction in the transactions table. assert.eq(primary.getDB('config')['transactions'].find().itcount(), 1);   // The transaction should still be prepared after going through rollback. rollbackTest.transitionToRollbackOperations(); rollbackTest.transitionToSyncSourceOperationsBeforeRollback(); rollbackTest.transitionToSyncSourceOperationsDuringRollback(); rollbackTest.transitionToSteadyStateOperations();   // Make sure there is still one transactions in the transactions table. This is because the // entry in the transactions table is made durable when a transaction is prepared. assert.eq(primary.getDB('config')['transactions'].find().itcount(), 1);   // TODO: Fix fastcount. Fastcount will return 2 instead of 3. This is inconsistent with the // behavior of fastcount observing the insert at line 38. assert.eq(testColl.count(), 3);   // Make sure we cannot see the writes from the prepared transaction yet. arrayEq(testColl.find().toArray(), [{_id: 0}, {_id: 2}]);   // Get the correct primary after the topology changes. primary = rollbackTest.getPrimary(); rollbackTest.awaitReplication();   // Make sure we can successfully commit the recovered prepared transaction. session = PrepareHelpers.createSessionWithGivenId(primary, sessionID, {causalConsistency: false}); sessionDB = session.getDatabase(dbName); // The transaction on this session should have a txnNumber of 0. We explicitly set this // since createSessionWithGivenId does not restore the current txnNumber in the shell. session.setTxnNumber_forTesting(0); const txnNumber = session.getTxnNumber_forTesting();   // Make sure we cannot add any operations to a prepared transaction. assert.commandFailedWithCode(sessionDB.runCommand({ insert: collName, txnNumber: NumberLong(txnNumber), documents: [{_id: 10}], autocommit: false, }), ErrorCodes.PreparedTransactionInProgress);   // Make sure that writing to a document that was updated in the prepared transaction causes // a write conflict. assert.commandFailedWithCode( sessionDB.runCommand( {update: collName, updates: [{q: {_id: 0}, u: {$set: {a: 2}}}], maxTimeMS: 5 * 1000}), ErrorCodes.MaxTimeMSExpired);   // Commit the transaction. assert.commandWorked(sessionDB.adminCommand({ commitTransaction: 1, commitTimestamp: prepareTimestamp, txnNumber: NumberLong(txnNumber), autocommit: false, }));   // Make sure we can see the effects of the prepared transaction. arrayEq(testColl.find().toArray(), [{_id: 0, a: 1}, {_id: 1}, {_id: 2}]); // TODO: Fix fastcount. Fastcount will return 2 instead of 3. assert.eq(testColl.count(), 3);   rollbackTest.stop(); }());
    • Sprint:
      Storage NYC 2019-04-08, Storage NYC 2019-04-22
    • Linked BF Score:
      0

      Description

      This came out of SERVER-39689, where we test that a transaction prepared before the stable timestamp will still be in prepare after a rollback. When we commit this transaction, the counts do not reflect the insert of the prepared transaction.

      My understanding is that we add to the count when we put a transaction into prepare. We then subtract from the count when we abort the storage transaction of a prepared transaction during rollback.

      We do not change the counts when reconstructing the prepared transaction during recovery. This is not a problem if the prepared transaction is eventually aborted. However, if this transaction is eventually committed, the counts are incorrect.

        Attachments

          Issue Links

            Activity

              People

              Assignee:
              louis.williams Louis Williams
              Reporter:
              pavithra.vetriselvan Pavithra Vetriselvan
              Participants:
              Votes:
              0 Vote for this issue
              Watchers:
              5 Start watching this issue

                Dates

                Created:
                Updated:
                Resolved: