diff options
Diffstat (limited to 'jstests/replsets/recover_prepared_transaction_state.js')
-rw-r--r-- | jstests/replsets/recover_prepared_transaction_state.js | 331 |
1 files changed, 164 insertions, 167 deletions
diff --git a/jstests/replsets/recover_prepared_transaction_state.js b/jstests/replsets/recover_prepared_transaction_state.js index f87d35496c8..1b054718778 100644 --- a/jstests/replsets/recover_prepared_transaction_state.js +++ b/jstests/replsets/recover_prepared_transaction_state.js @@ -15,185 +15,182 @@ * @tags: [uses_transactions, uses_prepare_transaction] */ (function() { - "use strict"; +"use strict"; - load("jstests/aggregation/extras/utils.js"); - load("jstests/core/txns/libs/prepare_helpers.js"); - load("jstests/replsets/libs/rollback_test.js"); +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 = "recover_prepared_transaction_state_after_rollback"; +const dbName = "test"; +const collName = "recover_prepared_transaction_state_after_rollback"; - const rollbackTest = new RollbackTest(dbName); - let primary = rollbackTest.getPrimary(); +const rollbackTest = new RollbackTest(dbName); +let primary = rollbackTest.getPrimary(); - // Create collection we're using beforehand. - const testDB = primary.getDB(dbName); - const testColl = testDB.getCollection(collName); +// Create collection we're using beforehand. +const testDB = primary.getDB(dbName); +const testColl = testDB.getCollection(collName); - assert.commandWorked(testDB.runCommand({create: collName})); +assert.commandWorked(testDB.runCommand({create: collName})); - // Start two different sessions on the primary. - let session1 = primary.startSession({causalConsistency: false}); - let session2 = primary.startSession({causalConsistency: false}); +// Start two different sessions on the primary. +let session1 = primary.startSession({causalConsistency: false}); +let session2 = primary.startSession({causalConsistency: false}); - // Save both session IDs so we can later start sessions with the same IDs and commit or - // abort a prepared transaction on them. - const sessionID1 = session1.getSessionId(); - const sessionID2 = session2.getSessionId(); +// Save both session IDs so we can later start sessions with the same IDs and commit or +// abort a prepared transaction on them. +const sessionID1 = session1.getSessionId(); +const sessionID2 = session2.getSessionId(); - let sessionDB1 = session1.getDatabase(dbName); - const sessionColl1 = sessionDB1.getCollection(collName); +let sessionDB1 = session1.getDatabase(dbName); +const sessionColl1 = sessionDB1.getCollection(collName); - let sessionDB2 = session2.getDatabase(dbName); - const sessionColl2 = sessionDB2.getCollection(collName); +let sessionDB2 = session2.getDatabase(dbName); +const sessionColl2 = sessionDB2.getCollection(collName); - assert.commandWorked(sessionColl1.insert({_id: 1})); - assert.commandWorked(sessionColl1.insert({_id: 2})); +assert.commandWorked(sessionColl1.insert({_id: 1})); +assert.commandWorked(sessionColl1.insert({_id: 2})); - rollbackTest.awaitLastOpCommitted(); +rollbackTest.awaitLastOpCommitted(); - // Prepare a transaction on the first session whose commit will be rolled-back. - session1.startTransaction(); - assert.commandWorked(sessionColl1.insert({_id: 3})); - assert.commandWorked(sessionColl1.update({_id: 1}, {$set: {a: 1}})); - const prepareTimestamp = PrepareHelpers.prepareTransaction(session1); +// Prepare a transaction on the first session whose commit will be rolled-back. +session1.startTransaction(); +assert.commandWorked(sessionColl1.insert({_id: 3})); +assert.commandWorked(sessionColl1.update({_id: 1}, {$set: {a: 1}})); +const prepareTimestamp = PrepareHelpers.prepareTransaction(session1); - // Prevent the stable timestamp from moving beyond the following prepared transactions so - // that when we replay the oplog from the stable timestamp, we correctly recover them. - assert.commandWorked( - primary.adminCommand({configureFailPoint: 'disableSnapshotting', mode: 'alwaysOn'})); - - // The following transactions will be prepared before the common point, so they must be in - // prepare after rollback recovery. - - // Prepare another transaction on the second session whose abort will be rolled-back. - session2.startTransaction(); - assert.commandWorked(sessionColl2.insert({_id: 4})); - assert.commandWorked(sessionColl2.update({_id: 2}, {$set: {b: 2}})); - const prepareTimestamp2 = PrepareHelpers.prepareTransaction(session2, {w: 1}); - - // Check that we have two transactions in the transactions table. - assert.eq(primary.getDB('config')['transactions'].find().itcount(), 2); - - // This characterizes the current behavior of fastcount, which is that the two open transaction - // count toward the value. - assert.eq(testColl.count(), 4); - - // The following commit and abort will be rolled back. - rollbackTest.transitionToRollbackOperations(); - PrepareHelpers.commitTransaction(session1, prepareTimestamp); - assert.commandWorked(session2.abortTransaction_forTesting()); - - // The fastcount should be accurate because there are no open transactions. - assert.eq(testColl.count(), 3); - - rollbackTest.transitionToSyncSourceOperationsBeforeRollback(); - rollbackTest.transitionToSyncSourceOperationsDuringRollback(); - try { - rollbackTest.transitionToSteadyStateOperations({skipDataConsistencyChecks: true}); - } finally { - assert.commandWorked( - primary.adminCommand({configureFailPoint: 'disableSnapshotting', mode: 'off'})); - } - - // Make sure there are two transactions in the transactions table after rollback recovery. - assert.eq(primary.getDB('config')['transactions'].find().itcount(), 2); - - // Make sure we can only see the first write and cannot see the writes from the prepared - // transactions or the write that was rolled back. - arrayEq(sessionColl1.find().toArray(), [{_id: 1}, {_id: 2}]); - arrayEq(testColl.find().toArray(), [{_id: 1}, {_id: 2}]); - - // This check characterizes the current behavior of fastcount after rollback. It will not be - // correct, but reflects the count at the point where both transactions are not yet committed or - // aborted (because the operations were not majority committed). The count will eventually be - // correct once the commit and abort are retried. - assert.eq(sessionColl1.count(), 4); - assert.eq(testColl.count(), 4); - - // Get the correct primary after the topology changes. - primary = rollbackTest.getPrimary(); - rollbackTest.awaitReplication(); - - // Make sure we can successfully commit the first rolled back prepared transaction. - session1 = - PrepareHelpers.createSessionWithGivenId(primary, sessionID1, {causalConsistency: false}); - sessionDB1 = session1.getDatabase(dbName); - // The next 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. - session1.setTxnNumber_forTesting(0); - const txnNumber1 = session1.getTxnNumber_forTesting(); - - // Make sure we cannot add any operations to a prepared transaction. - assert.commandFailedWithCode(sessionDB1.runCommand({ - insert: collName, - txnNumber: NumberLong(txnNumber1), - documents: [{_id: 10}], - autocommit: false, - }), - ErrorCodes.PreparedTransactionInProgress); - - // Make sure that writing to a document that was updated in the first prepared transaction - // causes a write conflict. - assert.commandFailedWithCode( - sessionDB1.runCommand( - {update: collName, updates: [{q: {_id: 1}, u: {$set: {a: 2}}}], maxTimeMS: 5 * 1000}), - ErrorCodes.MaxTimeMSExpired); - - const commitTimestamp = Timestamp(prepareTimestamp.getTime(), prepareTimestamp.getInc() + 1); - assert.commandWorked(sessionDB1.adminCommand({ - commitTransaction: 1, - commitTimestamp: commitTimestamp, - txnNumber: NumberLong(txnNumber1), - autocommit: false, - })); - // Retry the commitTransaction command after rollback. - assert.commandWorked(sessionDB1.adminCommand({ - commitTransaction: 1, - commitTimestamp: commitTimestamp, - txnNumber: NumberLong(txnNumber1), - autocommit: false, - })); - - // Make sure we can successfully abort the second recovered prepared transaction. - session2 = - PrepareHelpers.createSessionWithGivenId(primary, sessionID2, {causalConsistency: false}); - sessionDB2 = session2.getDatabase(dbName); - // The next 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. - session2.setTxnNumber_forTesting(0); - const txnNumber2 = session2.getTxnNumber_forTesting(); - - // Make sure we cannot add any operations to a prepared transaction. - assert.commandFailedWithCode(sessionDB2.runCommand({ - insert: collName, - txnNumber: NumberLong(txnNumber2), - documents: [{_id: 10}], - autocommit: false, - }), - ErrorCodes.PreparedTransactionInProgress); - - // Make sure that writing to a document that was updated in the second prepared transaction - // causes a write conflict. - assert.commandFailedWithCode( - sessionDB2.runCommand( - {update: collName, updates: [{q: {_id: 2}, u: {$set: {b: 3}}}], maxTimeMS: 5 * 1000}), - ErrorCodes.MaxTimeMSExpired); - - assert.commandWorked(sessionDB2.adminCommand({ - abortTransaction: 1, - txnNumber: NumberLong(txnNumber2), - autocommit: false, - })); - - rollbackTest.awaitReplication(); - - // Make sure we can see the result of the committed prepared transaction and cannot see the - // write from the aborted transaction. - arrayEq(testColl.find().toArray(), [{_id: 1, a: 1}, {_id: 2}, {_id: 3}]); - assert.eq(testColl.count(), 3); - - rollbackTest.stop(); +// Prevent the stable timestamp from moving beyond the following prepared transactions so +// that when we replay the oplog from the stable timestamp, we correctly recover them. +assert.commandWorked( + primary.adminCommand({configureFailPoint: 'disableSnapshotting', mode: 'alwaysOn'})); + +// The following transactions will be prepared before the common point, so they must be in +// prepare after rollback recovery. + +// Prepare another transaction on the second session whose abort will be rolled-back. +session2.startTransaction(); +assert.commandWorked(sessionColl2.insert({_id: 4})); +assert.commandWorked(sessionColl2.update({_id: 2}, {$set: {b: 2}})); +const prepareTimestamp2 = PrepareHelpers.prepareTransaction(session2, {w: 1}); + +// Check that we have two transactions in the transactions table. +assert.eq(primary.getDB('config')['transactions'].find().itcount(), 2); +// This characterizes the current behavior of fastcount, which is that the two open transaction +// count toward the value. +assert.eq(testColl.count(), 4); + +// The following commit and abort will be rolled back. +rollbackTest.transitionToRollbackOperations(); +PrepareHelpers.commitTransaction(session1, prepareTimestamp); +assert.commandWorked(session2.abortTransaction_forTesting()); + +// The fastcount should be accurate because there are no open transactions. +assert.eq(testColl.count(), 3); + +rollbackTest.transitionToSyncSourceOperationsBeforeRollback(); +rollbackTest.transitionToSyncSourceOperationsDuringRollback(); +try { + rollbackTest.transitionToSteadyStateOperations({skipDataConsistencyChecks: true}); +} finally { + assert.commandWorked( + primary.adminCommand({configureFailPoint: 'disableSnapshotting', mode: 'off'})); +} + +// Make sure there are two transactions in the transactions table after rollback recovery. +assert.eq(primary.getDB('config')['transactions'].find().itcount(), 2); + +// Make sure we can only see the first write and cannot see the writes from the prepared +// transactions or the write that was rolled back. +arrayEq(sessionColl1.find().toArray(), [{_id: 1}, {_id: 2}]); +arrayEq(testColl.find().toArray(), [{_id: 1}, {_id: 2}]); + +// This check characterizes the current behavior of fastcount after rollback. It will not be +// correct, but reflects the count at the point where both transactions are not yet committed or +// aborted (because the operations were not majority committed). The count will eventually be +// correct once the commit and abort are retried. +assert.eq(sessionColl1.count(), 4); +assert.eq(testColl.count(), 4); + +// Get the correct primary after the topology changes. +primary = rollbackTest.getPrimary(); +rollbackTest.awaitReplication(); + +// Make sure we can successfully commit the first rolled back prepared transaction. +session1 = PrepareHelpers.createSessionWithGivenId(primary, sessionID1, {causalConsistency: false}); +sessionDB1 = session1.getDatabase(dbName); +// The next 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. +session1.setTxnNumber_forTesting(0); +const txnNumber1 = session1.getTxnNumber_forTesting(); + +// Make sure we cannot add any operations to a prepared transaction. +assert.commandFailedWithCode(sessionDB1.runCommand({ + insert: collName, + txnNumber: NumberLong(txnNumber1), + documents: [{_id: 10}], + autocommit: false, +}), + ErrorCodes.PreparedTransactionInProgress); + +// Make sure that writing to a document that was updated in the first prepared transaction +// causes a write conflict. +assert.commandFailedWithCode( + sessionDB1.runCommand( + {update: collName, updates: [{q: {_id: 1}, u: {$set: {a: 2}}}], maxTimeMS: 5 * 1000}), + ErrorCodes.MaxTimeMSExpired); + +const commitTimestamp = Timestamp(prepareTimestamp.getTime(), prepareTimestamp.getInc() + 1); +assert.commandWorked(sessionDB1.adminCommand({ + commitTransaction: 1, + commitTimestamp: commitTimestamp, + txnNumber: NumberLong(txnNumber1), + autocommit: false, +})); +// Retry the commitTransaction command after rollback. +assert.commandWorked(sessionDB1.adminCommand({ + commitTransaction: 1, + commitTimestamp: commitTimestamp, + txnNumber: NumberLong(txnNumber1), + autocommit: false, +})); + +// Make sure we can successfully abort the second recovered prepared transaction. +session2 = PrepareHelpers.createSessionWithGivenId(primary, sessionID2, {causalConsistency: false}); +sessionDB2 = session2.getDatabase(dbName); +// The next 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. +session2.setTxnNumber_forTesting(0); +const txnNumber2 = session2.getTxnNumber_forTesting(); + +// Make sure we cannot add any operations to a prepared transaction. +assert.commandFailedWithCode(sessionDB2.runCommand({ + insert: collName, + txnNumber: NumberLong(txnNumber2), + documents: [{_id: 10}], + autocommit: false, +}), + ErrorCodes.PreparedTransactionInProgress); + +// Make sure that writing to a document that was updated in the second prepared transaction +// causes a write conflict. +assert.commandFailedWithCode( + sessionDB2.runCommand( + {update: collName, updates: [{q: {_id: 2}, u: {$set: {b: 3}}}], maxTimeMS: 5 * 1000}), + ErrorCodes.MaxTimeMSExpired); + +assert.commandWorked(sessionDB2.adminCommand({ + abortTransaction: 1, + txnNumber: NumberLong(txnNumber2), + autocommit: false, +})); + +rollbackTest.awaitReplication(); + +// Make sure we can see the result of the committed prepared transaction and cannot see the +// write from the aborted transaction. +arrayEq(testColl.find().toArray(), [{_id: 1, a: 1}, {_id: 2}, {_id: 3}]); +assert.eq(testColl.count(), 3); + +rollbackTest.stop(); }()); |